refactor: 项目稳定性优化

This commit is contained in:
2026-01-17 17:00:15 +08:00
parent 7abd445039
commit c276e9e2b9
7 changed files with 135 additions and 110 deletions

View File

@@ -10,17 +10,13 @@ namespace TimerApp
Resting
}
public class ActivityMonitor
public sealed class ActivityMonitor : IDisposable
{
private System.Windows.Forms.Timer _timer;
private DateTime _lastWorkStartTime;
private readonly System.Windows.Forms.Timer _timer;
private TimeSpan _accumulatedWorkTime;
private DateTime _restStartTime;
private int _restElapsedSeconds; // 休息已过秒数(用于避免时间计算导致的跳变)
private bool _isPaused = false; // 暂停状态
private DateTime _pauseStartTime; // 暂停开始时间
private TimeSpan _pauseDuration = TimeSpan.Zero; // 累计暂停时间(用于工作计时)
private int _restPauseStartSeconds = 0; // 休息暂停时的已过秒数
private int _restElapsedSeconds;
private bool _isPaused;
private bool _disposed;
// 配置 (默认值)
public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20);
@@ -31,11 +27,11 @@ namespace TimerApp
public bool IsPaused { get; private set; } = false;
// 事件
public event EventHandler<TimeSpan> WorkProgressChanged; // 剩余工作时间
public event EventHandler<TimeSpan> RestProgressChanged; // 剩余休息时间
public event EventHandler RestStarted;
public event EventHandler RestEnded;
public event EventHandler StateChanged;
public event EventHandler<TimeSpan>? WorkProgressChanged; // 剩余工作时间
public event EventHandler<TimeSpan>? RestProgressChanged; // 剩余休息时间
public event EventHandler? RestStarted;
public event EventHandler? RestEnded;
public event EventHandler? StateChanged;
public ActivityMonitor()
{
@@ -58,7 +54,6 @@ namespace TimerApp
private void ResetWork()
{
_accumulatedWorkTime = TimeSpan.Zero;
_lastWorkStartTime = DateTime.Now;
ChangeState(MonitorState.Idle);
}
@@ -71,7 +66,7 @@ namespace TimerApp
}
}
private void Timer_Tick(object sender, EventArgs e)
private void Timer_Tick(object? sender, EventArgs e)
{
// 如果处于暂停状态,不处理计时逻辑
if (_isPaused)
@@ -142,7 +137,6 @@ namespace TimerApp
{
// 从空闲变为工作
ChangeState(MonitorState.Working);
_lastWorkStartTime = DateTime.Now;
}
// 累加工作时间
@@ -155,7 +149,6 @@ namespace TimerApp
if (remainingWork <= TimeSpan.Zero)
{
// 触发休息
_restStartTime = DateTime.Now;
_restElapsedSeconds = 0; // 重置休息计数器
ChangeState(MonitorState.Resting);
RestStarted?.Invoke(this, EventArgs.Empty);
@@ -171,7 +164,6 @@ namespace TimerApp
// 用于强制重置或测试
public void ForceRest()
{
_restStartTime = DateTime.Now;
_restElapsedSeconds = 0; // 重置休息计数器
ChangeState(MonitorState.Resting);
RestStarted?.Invoke(this, EventArgs.Empty);
@@ -198,10 +190,8 @@ namespace TimerApp
public void Restart()
{
_accumulatedWorkTime = TimeSpan.Zero;
_lastWorkStartTime = DateTime.Now;
_isPaused = false;
IsPaused = false;
_pauseDuration = TimeSpan.Zero;
// Ensure timer is running
if (!_timer.Enabled) _timer.Start();
@@ -219,13 +209,6 @@ namespace TimerApp
{
_isPaused = true;
IsPaused = true;
_pauseStartTime = DateTime.Now;
// 如果正在休息,记录当前已过秒数
if (CurrentState == MonitorState.Resting)
{
_restPauseStartSeconds = _restElapsedSeconds;
}
StateChanged?.Invoke(this, EventArgs.Empty);
}
@@ -238,25 +221,19 @@ namespace TimerApp
_isPaused = false;
IsPaused = false;
// 计算暂停时长
TimeSpan pauseTime = DateTime.Now - _pauseStartTime;
// 如果正在工作,将暂停时间累加到暂停总时长中
// 这样工作时间就不会因为暂停而减少
if (CurrentState == MonitorState.Working)
{
_pauseDuration += pauseTime;
}
// 如果正在休息,调整已过秒数,使剩余时间保持不变
else if (CurrentState == MonitorState.Resting)
{
// 保持已过秒数不变,这样恢复后剩余时间不会变化
// _restElapsedSeconds 保持不变
}
StateChanged?.Invoke(this, EventArgs.Empty);
RefreshStatus();
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_timer.Stop();
_timer.Tick -= Timer_Tick;
_timer.Dispose();
}
}
}

View File

@@ -11,16 +11,26 @@ namespace TimerApp
public int IdleThresholdSeconds { get; set; } = 30;
public bool IsDarkMode { get; set; } = true;
private static string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
private static string LegacyConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
private static string ConfigPath
{
get
{
string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TimerApp");
return Path.Combine(dir, "settings.json");
}
}
public static AppSettings Load()
{
try
{
if (File.Exists(ConfigPath))
string path = File.Exists(ConfigPath) ? ConfigPath : LegacyConfigPath;
if (File.Exists(path))
{
string json = File.ReadAllText(ConfigPath);
return JsonSerializer.Deserialize<AppSettings>(json);
string json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
}
catch
@@ -34,6 +44,7 @@ namespace TimerApp
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(ConfigPath)!);
string json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(ConfigPath, json);
}

6
MainForm.Designer.cs generated
View File

@@ -13,9 +13,11 @@ namespace TimerApp
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
if (disposing)
{
components.Dispose();
_monitor?.Dispose();
_restForm?.Dispose();
components?.Dispose();
}
base.Dispose(disposing);
}

View File

@@ -7,9 +7,9 @@ namespace TimerApp
{
public partial class MainForm : Form
{
private ActivityMonitor _monitor;
private ActivityMonitor _monitor = null!;
private AppSettings _settings;
private RestForm _restForm;
private RestForm? _restForm;
// Colors
private Color _darkBg = Color.FromArgb(30, 30, 30);
@@ -106,8 +106,8 @@ namespace TimerApp
// Manual input validation
txtWork.KeyPress += ValidateDigitInput;
txtRest.KeyPress += ValidateDigitInput;
txtWork.Leave += (s, ev) => ValidateRange((TextBox)s, 1, 120);
txtRest.Leave += (s, ev) => ValidateRange((TextBox)s, 1, 30);
txtWork.Leave += (s, ev) => ValidateRange((TextBox)s!, 1, 120);
txtRest.Leave += (s, ev) => ValidateRange((TextBox)s!, 1, 30);
// Focus handling (remove custom caret)
txtWork.KeyDown += TextBox_KeyDown;
@@ -163,7 +163,7 @@ namespace TimerApp
}
}
private void ValidateDigitInput(object sender, KeyPressEventArgs e)
private void ValidateDigitInput(object? sender, KeyPressEventArgs e)
{
// Allow control keys (backspace, etc.) and digits
if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar))
@@ -216,7 +216,7 @@ namespace TimerApp
// CustomCaret removed to use system caret with centered text
private void TextBox_KeyDown(object sender, KeyEventArgs e)
private void TextBox_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
@@ -407,11 +407,11 @@ namespace TimerApp
_monitor.IdleThreshold = TimeSpan.FromSeconds(_settings.IdleThresholdSeconds);
}
private void Monitor_StateChanged(object sender, EventArgs e)
private void Monitor_StateChanged(object? sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new Action<object, EventArgs>(Monitor_StateChanged), sender, e);
Invoke(new Action<object?, EventArgs>(Monitor_StateChanged), sender, e);
return;
}
UpdateStatusUI();
@@ -456,11 +456,11 @@ namespace TimerApp
}
}
private void Monitor_WorkProgressChanged(object sender, TimeSpan remaining)
private void Monitor_WorkProgressChanged(object? sender, TimeSpan remaining)
{
if (InvokeRequired)
{
Invoke(new Action<object, TimeSpan>(Monitor_WorkProgressChanged), sender, remaining);
Invoke(new Action<object?, TimeSpan>(Monitor_WorkProgressChanged), sender, remaining);
return;
}
lblTimeLeft.Text = $"{remaining.Minutes:D2}:{remaining.Seconds:D2}";
@@ -476,11 +476,11 @@ namespace TimerApp
}
}
private void Monitor_RestStarted(object sender, EventArgs e)
private void Monitor_RestStarted(object? sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new Action<object, EventArgs>(Monitor_RestStarted), sender, e);
Invoke(new Action<object?, EventArgs>(Monitor_RestStarted), sender, e);
return;
}
@@ -494,13 +494,13 @@ namespace TimerApp
_restForm.Show();
}
private void RestForm_SkipRequested(object sender, EventArgs e)
private void RestForm_SkipRequested(object? sender, EventArgs e)
{
_monitor.Stop();
_monitor.Start();
}
private void Monitor_RestProgressChanged(object sender, TimeSpan remaining)
private void Monitor_RestProgressChanged(object? sender, TimeSpan remaining)
{
if (_restForm != null && !_restForm.IsDisposed && _restForm.Visible)
{
@@ -508,11 +508,11 @@ namespace TimerApp
}
}
private void Monitor_RestEnded(object sender, EventArgs e)
private void Monitor_RestEnded(object? sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new Action<object, EventArgs>(Monitor_RestEnded), sender, e);
Invoke(new Action<object?, EventArgs>(Monitor_RestEnded), sender, e);
return;
}
@@ -555,12 +555,12 @@ namespace TimerApp
}
}
private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
private void notifyIcon1_MouseDoubleClick(object? sender, MouseEventArgs e)
{
ShowForm();
}
private void toolStripMenuItemShow_Click(object sender, EventArgs e)
private void toolStripMenuItemShow_Click(object? sender, EventArgs e)
{
ShowForm();
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.Media.Control;
namespace TimerApp
@@ -26,22 +28,17 @@ namespace TimerApp
/// <returns></returns>
public static long GetIdleTime()
{
LASTINPUTINFO lastInputInfo = new LASTINPUTINFO();
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
lastInputInfo.dwTime = 0;
LASTINPUTINFO lastInputInfo = new LASTINPUTINFO
{
cbSize = (uint)Marshal.SizeOf<LASTINPUTINFO>(),
dwTime = 0
};
if (GetLastInputInfo(ref lastInputInfo))
{
// Environment.TickCount 可能会在大约 24.9 天后翻转为负数,
// 但 GetLastInputInfo 返回的也是 uint (DWORD),所以我们统一转为 long 处理差值
// 或者直接使用 unchecked 减法处理溢出
// 更稳健的做法是使用 GetTickCount64 (Vista+),但 Environment.TickCount 在 .NET Core 3.1+ 已经是 64位了(Environment.TickCount64)
// 这里为了兼容性,我们简单处理。注意 GetLastInputInfo 返回的是 uint 毫秒数。
long envTicks = Environment.TickCount;
// 处理 TickCount 翻转问题 (Environment.TickCount 是 intGetLastInputInfo 是 uint)
// 简单的做法:
return (long)GetTickCount() - (long)lastInputInfo.dwTime;
uint tickCount = GetTickCount();
uint idleMs = unchecked(tickCount - lastInputInfo.dwTime);
return idleMs;
}
return 0;
@@ -56,9 +53,34 @@ namespace TimerApp
/// <returns>如果有媒体正在播放返回 true否则返回 false</returns>
public static bool IsMediaPlaying()
{
EnsureMediaPlaybackStatusFresh();
return Volatile.Read(ref _mediaPlaying);
}
private static bool _mediaPlaying;
private static long _mediaLastUpdateMs = -1;
private static int _mediaUpdateInProgress;
private static void EnsureMediaPlaybackStatusFresh()
{
long now = Environment.TickCount64;
long last = Interlocked.Read(ref _mediaLastUpdateMs);
if (last >= 0 && now - last < 3000)
{
return;
}
if (Interlocked.Exchange(ref _mediaUpdateInProgress, 1) == 1)
{
return;
}
_ = Task.Run(async () =>
{
bool playing = false;
try
{
var sessionManager = GlobalSystemMediaTransportControlsSessionManager.RequestAsync().GetAwaiter().GetResult();
var sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
var sessions = sessionManager.GetSessions();
foreach (var session in sessions)
@@ -66,17 +88,22 @@ namespace TimerApp
var playbackInfo = session.GetPlaybackInfo();
if (playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing)
{
return true;
playing = true;
break;
}
}
}
catch
{
// 如果 API 调用失败,返回 false保守处理
return false;
playing = false;
}
return false;
finally
{
Volatile.Write(ref _mediaPlaying, playing);
Interlocked.Exchange(ref _mediaLastUpdateMs, Environment.TickCount64);
Interlocked.Exchange(ref _mediaUpdateInProgress, 0);
}
});
}
}
}

View File

@@ -6,11 +6,11 @@ namespace TimerApp
{
public class RestForm : Form
{
private Label lblMessage;
private Label lblTimer;
private Button btnSkip;
private Label lblMessage = null!;
private Label lblTimer = null!;
private Button btnSkip = null!;
public event EventHandler SkipRequested;
public event EventHandler? SkipRequested;
public RestForm()
{
@@ -99,12 +99,12 @@ namespace TimerApp
this.PerformLayout();
}
private void RestForm_Load(object sender, EventArgs e)
private void RestForm_Load(object? sender, EventArgs e)
{
CenterControls();
}
private void RestForm_Resize(object sender, EventArgs e)
private void RestForm_Resize(object? sender, EventArgs e)
{
CenterControls();
}
@@ -153,7 +153,7 @@ namespace TimerApp
}
}
private void btnSkip_Click(object sender, EventArgs e)
private void btnSkip_Click(object? sender, EventArgs e)
{
SkipRequested?.Invoke(this, EventArgs.Empty);
this.Close();

View File

@@ -11,12 +11,16 @@ namespace TimerApp
private const string MutexName = "Local\\TimerApp.SingleInstance";
private const string PipeName = "TimerApp.SingleInstancePipe";
private readonly Mutex _mutex;
private readonly Mutex? _mutex;
private readonly bool _ownsMutex;
private readonly bool _enableIpc;
private readonly CancellationTokenSource _cts = new();
private SingleInstanceManager(Mutex mutex)
private SingleInstanceManager(Mutex? mutex, bool ownsMutex, bool enableIpc)
{
_mutex = mutex;
_ownsMutex = ownsMutex;
_enableIpc = enableIpc;
}
public static bool TryAcquire(out SingleInstanceManager? manager)
@@ -32,11 +36,12 @@ namespace TimerApp
return false;
}
manager = new SingleInstanceManager(mutex);
manager = new SingleInstanceManager(mutex, ownsMutex: true, enableIpc: true);
return true;
}
catch
{
manager = new SingleInstanceManager(mutex: null, ownsMutex: false, enableIpc: false);
return true;
}
}
@@ -57,6 +62,7 @@ namespace TimerApp
public void StartServer(Action onShowRequested)
{
if (!_enableIpc) return;
Task.Run(() => ServerLoop(onShowRequested, _cts.Token));
}
@@ -104,15 +110,17 @@ namespace TimerApp
try
{
_mutex.ReleaseMutex();
if (_ownsMutex)
{
_mutex?.ReleaseMutex();
}
}
catch
{
}
_mutex.Dispose();
_mutex?.Dispose();
_cts.Dispose();
}
}
}