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 Resting
} }
public class ActivityMonitor public sealed class ActivityMonitor : IDisposable
{ {
private System.Windows.Forms.Timer _timer; private readonly System.Windows.Forms.Timer _timer;
private DateTime _lastWorkStartTime;
private TimeSpan _accumulatedWorkTime; private TimeSpan _accumulatedWorkTime;
private DateTime _restStartTime; private int _restElapsedSeconds;
private int _restElapsedSeconds; // 休息已过秒数(用于避免时间计算导致的跳变) private bool _isPaused;
private bool _isPaused = false; // 暂停状态 private bool _disposed;
private DateTime _pauseStartTime; // 暂停开始时间
private TimeSpan _pauseDuration = TimeSpan.Zero; // 累计暂停时间(用于工作计时)
private int _restPauseStartSeconds = 0; // 休息暂停时的已过秒数
// 配置 (默认值) // 配置 (默认值)
public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20); public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20);
@@ -31,11 +27,11 @@ namespace TimerApp
public bool IsPaused { get; private set; } = false; public bool IsPaused { get; private set; } = false;
// 事件 // 事件
public event EventHandler<TimeSpan> WorkProgressChanged; // 剩余工作时间 public event EventHandler<TimeSpan>? WorkProgressChanged; // 剩余工作时间
public event EventHandler<TimeSpan> RestProgressChanged; // 剩余休息时间 public event EventHandler<TimeSpan>? RestProgressChanged; // 剩余休息时间
public event EventHandler RestStarted; public event EventHandler? RestStarted;
public event EventHandler RestEnded; public event EventHandler? RestEnded;
public event EventHandler StateChanged; public event EventHandler? StateChanged;
public ActivityMonitor() public ActivityMonitor()
{ {
@@ -58,7 +54,6 @@ namespace TimerApp
private void ResetWork() private void ResetWork()
{ {
_accumulatedWorkTime = TimeSpan.Zero; _accumulatedWorkTime = TimeSpan.Zero;
_lastWorkStartTime = DateTime.Now;
ChangeState(MonitorState.Idle); 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) if (_isPaused)
@@ -142,7 +137,6 @@ namespace TimerApp
{ {
// 从空闲变为工作 // 从空闲变为工作
ChangeState(MonitorState.Working); ChangeState(MonitorState.Working);
_lastWorkStartTime = DateTime.Now;
} }
// 累加工作时间 // 累加工作时间
@@ -155,7 +149,6 @@ namespace TimerApp
if (remainingWork <= TimeSpan.Zero) if (remainingWork <= TimeSpan.Zero)
{ {
// 触发休息 // 触发休息
_restStartTime = DateTime.Now;
_restElapsedSeconds = 0; // 重置休息计数器 _restElapsedSeconds = 0; // 重置休息计数器
ChangeState(MonitorState.Resting); ChangeState(MonitorState.Resting);
RestStarted?.Invoke(this, EventArgs.Empty); RestStarted?.Invoke(this, EventArgs.Empty);
@@ -171,7 +164,6 @@ namespace TimerApp
// 用于强制重置或测试 // 用于强制重置或测试
public void ForceRest() public void ForceRest()
{ {
_restStartTime = DateTime.Now;
_restElapsedSeconds = 0; // 重置休息计数器 _restElapsedSeconds = 0; // 重置休息计数器
ChangeState(MonitorState.Resting); ChangeState(MonitorState.Resting);
RestStarted?.Invoke(this, EventArgs.Empty); RestStarted?.Invoke(this, EventArgs.Empty);
@@ -198,10 +190,8 @@ namespace TimerApp
public void Restart() public void Restart()
{ {
_accumulatedWorkTime = TimeSpan.Zero; _accumulatedWorkTime = TimeSpan.Zero;
_lastWorkStartTime = DateTime.Now;
_isPaused = false; _isPaused = false;
IsPaused = false; IsPaused = false;
_pauseDuration = TimeSpan.Zero;
// Ensure timer is running // Ensure timer is running
if (!_timer.Enabled) _timer.Start(); if (!_timer.Enabled) _timer.Start();
@@ -219,13 +209,6 @@ namespace TimerApp
{ {
_isPaused = true; _isPaused = true;
IsPaused = true; IsPaused = true;
_pauseStartTime = DateTime.Now;
// 如果正在休息,记录当前已过秒数
if (CurrentState == MonitorState.Resting)
{
_restPauseStartSeconds = _restElapsedSeconds;
}
StateChanged?.Invoke(this, EventArgs.Empty); StateChanged?.Invoke(this, EventArgs.Empty);
} }
@@ -238,25 +221,19 @@ namespace TimerApp
_isPaused = false; _isPaused = false;
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); StateChanged?.Invoke(this, EventArgs.Empty);
RefreshStatus(); 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 int IdleThresholdSeconds { get; set; } = 30;
public bool IsDarkMode { get; set; } = true; 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() public static AppSettings Load()
{ {
try try
{ {
if (File.Exists(ConfigPath)) string path = File.Exists(ConfigPath) ? ConfigPath : LegacyConfigPath;
if (File.Exists(path))
{ {
string json = File.ReadAllText(ConfigPath); string json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppSettings>(json); return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
} }
} }
catch catch
@@ -34,6 +44,7 @@ namespace TimerApp
{ {
try try
{ {
Directory.CreateDirectory(Path.GetDirectoryName(ConfigPath)!);
string json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); string json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(ConfigPath, json); 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> /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing && (components != null)) if (disposing)
{ {
components.Dispose(); _monitor?.Dispose();
_restForm?.Dispose();
components?.Dispose();
} }
base.Dispose(disposing); base.Dispose(disposing);
} }

View File

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

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.Media.Control; using Windows.Media.Control;
namespace TimerApp namespace TimerApp
@@ -26,22 +28,17 @@ namespace TimerApp
/// <returns></returns> /// <returns></returns>
public static long GetIdleTime() public static long GetIdleTime()
{ {
LASTINPUTINFO lastInputInfo = new LASTINPUTINFO(); LASTINPUTINFO lastInputInfo = new LASTINPUTINFO
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo); {
lastInputInfo.dwTime = 0; cbSize = (uint)Marshal.SizeOf<LASTINPUTINFO>(),
dwTime = 0
};
if (GetLastInputInfo(ref lastInputInfo)) if (GetLastInputInfo(ref lastInputInfo))
{ {
// Environment.TickCount 可能会在大约 24.9 天后翻转为负数, uint tickCount = GetTickCount();
// 但 GetLastInputInfo 返回的也是 uint (DWORD),所以我们统一转为 long 处理差值 uint idleMs = unchecked(tickCount - lastInputInfo.dwTime);
// 或者直接使用 unchecked 减法处理溢出 return idleMs;
// 更稳健的做法是使用 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;
} }
return 0; return 0;
@@ -56,27 +53,57 @@ namespace TimerApp
/// <returns>如果有媒体正在播放返回 true否则返回 false</returns> /// <returns>如果有媒体正在播放返回 true否则返回 false</returns>
public static bool IsMediaPlaying() public static bool IsMediaPlaying()
{ {
try EnsureMediaPlaybackStatusFresh();
{ return Volatile.Read(ref _mediaPlaying);
var sessionManager = GlobalSystemMediaTransportControlsSessionManager.RequestAsync().GetAwaiter().GetResult(); }
var sessions = sessionManager.GetSessions();
foreach (var session in sessions) 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 playbackInfo = session.GetPlaybackInfo(); var sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
if (playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) var sessions = sessionManager.GetSessions();
foreach (var session in sessions)
{ {
return true; var playbackInfo = session.GetPlaybackInfo();
if (playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing)
{
playing = true;
break;
}
} }
} }
} catch
catch {
{ playing = false;
// 如果 API 调用失败,返回 false保守处理 }
return false; finally
} {
Volatile.Write(ref _mediaPlaying, playing);
return false; Interlocked.Exchange(ref _mediaLastUpdateMs, Environment.TickCount64);
Interlocked.Exchange(ref _mediaUpdateInProgress, 0);
}
});
} }
} }
} }

View File

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

View File

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