Files
TimerApp/MainForm.cs

640 lines
23 KiB
C#

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Microsoft.Win32;
namespace TimerApp
{
public partial class MainForm : Form
{
private ActivityMonitor _monitor = null!;
private AppSettings _settings;
private RestForm? _restForm;
// Colors
private Color _darkBg = Color.FromArgb(30, 30, 30);
private Color _lightBg = Color.FromArgb(240, 240, 240);
private Color _darkPanel = Color.FromArgb(45, 45, 48);
private Color _lightPanel = Color.FromArgb(200, 200, 200);
private Color _darkText = Color.White;
private Color _lightText = Color.Black;
// Drag window support
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[DllImport("user32.dll")]
static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight);
[DllImport("user32.dll")]
static extern bool ShowCaret(IntPtr hWnd);
private const int WM_NCLBUTTONDOWN = 0xA1;
private const int HT_CAPTION = 0x2;
public MainForm()
{
InitializeComponent();
// 在窗口显示前设置背景色,避免白色闪烁
// 确保所有控件在显示前都有正确的背景色
this.SuspendLayout();
// 先加载设置,以便知道应该使用什么背景色
_settings = AppSettings.Load();
bool dark = _settings.IsDarkMode;
Color bg = dark ? _darkBg : _lightBg;
this.BackColor = bg;
pnlSettings.BackColor = bg;
lblTimeLeft.BackColor = bg;
lblStatus.BackColor = bg;
label1.BackColor = bg;
label2.BackColor = bg;
// 设置文本框和按钮的初始背景色
Color panelColor = dark ? _darkPanel : _lightPanel;
txtWork.BackColor = panelColor;
txtRest.BackColor = panelColor;
btnWorkMinus.BackColor = panelColor;
btnWorkPlus.BackColor = panelColor;
btnRestMinus.BackColor = panelColor;
btnRestPlus.BackColor = panelColor;
btnStartStop.BackColor = dark ? Color.FromArgb(63, 63, 70) : Color.White;
btnReset.BackColor = dark ? Color.FromArgb(63, 63, 70) : Color.White;
btnPause.BackColor = dark ? Color.FromArgb(63, 63, 70) : Color.White;
// 优化绘制,减少闪烁
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.ResizeRedraw, true);
this.ResumeLayout(false);
}
private void MainForm_Load(object sender, EventArgs e)
{
// Settings already loaded in constructor, just update text
txtWork.Text = _settings.WorkMinutes.ToString();
txtRest.Text = _settings.RestMinutes.ToString();
// Apply Theme - 确保在窗口显示前应用主题
ApplyTheme();
// 强制刷新背景色,确保所有控件都正确显示
this.Invalidate(true);
pnlSettings.Invalidate(true);
this.Update();
// Init monitor
_monitor = new ActivityMonitor();
ApplySettings();
// Bind events
_monitor.StateChanged += Monitor_StateChanged;
_monitor.WorkProgressChanged += Monitor_WorkProgressChanged;
_monitor.RestStarted += Monitor_RestStarted;
_monitor.RestEnded += Monitor_RestEnded;
_monitor.RestProgressChanged += Monitor_RestProgressChanged;
// Numeric Buttons
btnWorkMinus.Click += (s, ev) => AdjustTime(txtWork, -1, 1, 120);
btnWorkPlus.Click += (s, ev) => AdjustTime(txtWork, 1, 1, 120);
btnRestMinus.Click += (s, ev) => AdjustTime(txtRest, -1, 1, 30);
btnRestPlus.Click += (s, ev) => AdjustTime(txtRest, 1, 1, 30);
// 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);
// Focus handling (remove custom caret)
txtWork.KeyDown += TextBox_KeyDown;
txtRest.KeyDown += TextBox_KeyDown;
// Setup TextBoxes with Panels for vertical centering
SetupTextBoxPanel(txtWork, pnlSettings);
SetupTextBoxPanel(txtRest, pnlSettings);
// 确保容器背景色正确(在 SetupTextBoxPanel 之后再次应用主题)
UpdateTextBoxStyle(txtWork, _settings.IsDarkMode);
UpdateTextBoxStyle(txtRest, _settings.IsDarkMode);
// 再次确保 pnlSettings 的背景色正确
bool dark = _settings.IsDarkMode;
Color bg = dark ? _darkBg : _lightBg;
pnlSettings.BackColor = bg;
pnlSettings.Refresh();
_monitor.Start();
// Set tray icon
try
{
// Generate and set custom icon
Icon icon = IconGenerator.GenerateClockIcon(64);
this.Icon = icon;
notifyIcon1.Icon = icon;
contextMenuStrip1.ShowImageMargin = true;
toolStripMenuItemShow.Image = null;
toolStripMenuItemExit.Image = null;
}
catch
{
notifyIcon1.Icon = SystemIcons.Application;
contextMenuStrip1.ShowImageMargin = true;
toolStripMenuItemShow.Image = null;
toolStripMenuItemExit.Image = null;
}
UpdateStatusUI();
}
private void AdjustTime(TextBox txt, int delta, int min, int max)
{
if (int.TryParse(txt.Text, out int val))
{
val += delta;
if (val < min) val = min;
if (val > max) val = max;
txt.Text = val.ToString();
}
}
private void ValidateDigitInput(object? sender, KeyPressEventArgs e)
{
// Allow control keys (backspace, etc.) and digits
if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar))
{
e.Handled = true;
}
}
private void SetupTextBoxPanel(TextBox txt, Panel parent)
{
// Create a container panel for the textbox
Panel container = new Panel();
container.Size = txt.Size; // 60x30
container.Location = txt.Location;
// 确保容器背景色与文本框背景色一致
container.BackColor = txt.BackColor;
// Adjust textbox to be centered inside
txt.Parent = container;
txt.Location = new Point(0, (container.Height - txt.Height) / 2); // Center vertically
txt.Dock = DockStyle.None;
txt.Width = container.Width;
txt.BorderStyle = BorderStyle.None;
// Add container to parent
parent.Controls.Add(container);
// Ensure correct tab order and tagging
container.Tag = txt.Tag; // Copy tag if any
// 确保容器背景色与父容器一致(如果文本框背景色与父容器不同)
// 这里容器应该使用文本框的背景色,因为它是文本框的容器
}
private void ValidateRange(TextBox txt, int min, int max)
{
if (int.TryParse(txt.Text, out int val))
{
if (val < min) txt.Text = min.ToString();
if (val > max) txt.Text = max.ToString();
}
else
{
txt.Text = min.ToString(); // Default to min if invalid/empty
}
// Clear selection on leave
txt.SelectionLength = 0;
}
// CustomCaret removed to use system caret with centered text
private void TextBox_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
// Lose focus by focusing the label
lblStatus.Focus();
e.Handled = true;
e.SuppressKeyPress = true; // Prevent 'ding' sound
}
}
private void ApplyTheme()
{
bool dark = _settings.IsDarkMode;
Color bg = dark ? _darkBg : _lightBg;
Color panel = dark ? _darkPanel : _lightPanel;
Color text = dark ? _darkText : _lightText;
Color btnBg = dark ? Color.FromArgb(45, 45, 48) : Color.White;
// 确保窗口和所有面板的背景色一致
this.BackColor = bg;
pnlTitle.BackColor = panel;
// 强制设置 pnlSettings 的背景色,确保与窗口背景色一致
pnlSettings.BackColor = bg;
pnlSettings.Invalidate(); // 强制重绘
lblTimeLeft.BackColor = bg;
lblStatus.BackColor = bg;
label1.BackColor = bg;
label2.BackColor = bg;
// 确保所有标签都使用正确的背景色
foreach (Control ctrl in pnlSettings.Controls)
{
if (ctrl is Label label && label.BackColor != bg)
{
label.BackColor = bg;
}
}
lblTitle.ForeColor = dark ? Color.LightGray : Color.FromArgb(64, 64, 64);
lblTimeLeft.ForeColor = text;
label1.ForeColor = dark ? Color.LightGray : Color.DimGray;
label2.ForeColor = dark ? Color.LightGray : Color.DimGray;
// Update buttons
UpdateButtonStyle(btnStartStop, dark);
UpdateButtonStyle(btnReset, dark);
UpdateButtonStyle(btnPause, dark);
UpdateButtonStyle(btnHide, dark);
// Numeric buttons and text
UpdateNumericButtonStyle(btnWorkMinus, dark);
UpdateNumericButtonStyle(btnWorkPlus, dark);
UpdateNumericButtonStyle(btnRestMinus, dark);
UpdateNumericButtonStyle(btnRestPlus, dark);
UpdateTextBoxStyle(txtWork, dark);
UpdateTextBoxStyle(txtRest, dark);
// Title buttons
btnClose.ForeColor = text;
btnMin.ForeColor = text;
// Set hover background colors for title buttons
if (dark)
{
// 深色模式下使用深色悬停背景
btnClose.FlatAppearance.MouseOverBackColor = Color.FromArgb(232, 17, 35); // 深色模式下的红色(稍微暗一点)
btnMin.FlatAppearance.MouseOverBackColor = Color.FromArgb(63, 63, 70);
btnTheme.FlatAppearance.MouseOverBackColor = Color.FromArgb(63, 63, 70);
}
else
{
// 浅色模式下使用浅色悬停背景
btnClose.FlatAppearance.MouseOverBackColor = Color.Red;
btnMin.FlatAppearance.MouseOverBackColor = Color.FromArgb(220, 220, 220); // 浅灰色,接近标题栏背景
btnTheme.FlatAppearance.MouseOverBackColor = Color.FromArgb(220, 220, 220); // 浅灰色,接近标题栏背景
}
// Theme button with Segoe MDL2 Assets
btnTheme.ForeColor = text;
btnTheme.Font = new Font("Segoe MDL2 Assets", 10F, FontStyle.Regular, GraphicsUnit.Point);
btnTheme.Text = dark ? "\uE706" : "\uE708"; // Sun : Moon
// Context Menu Theme
// 浅色模式下使用白色背景,深色模式使用深色面板色
Color menuBg = dark ? panel : Color.White;
var menuRenderer = new ToolStripProfessionalRenderer(new CustomColorTable(dark, menuBg));
contextMenuStrip1.Renderer = menuRenderer;
contextMenuStrip1.BackColor = menuBg;
contextMenuStrip1.ForeColor = dark ? text : Color.Black;
UpdateStatusUI(); // Re-apply status colors
}
private class CustomColorTable : ProfessionalColorTable
{
private bool _dark;
private Color _backColor;
public CustomColorTable(bool dark, Color backColor)
{
_dark = dark;
_backColor = backColor;
}
public override Color MenuItemSelected => _dark ? Color.FromArgb(63, 63, 70) : base.MenuItemSelected;
public override Color MenuItemBorder => _dark ? Color.FromArgb(63, 63, 70) : base.MenuItemBorder;
public override Color MenuBorder => _dark ? Color.FromArgb(45, 45, 48) : base.MenuBorder;
// Fix white margin in dark mode, and set margin color to background color in light mode
public override Color ImageMarginGradientBegin => _backColor;
public override Color ImageMarginGradientMiddle => _backColor;
public override Color ImageMarginGradientEnd => _backColor;
}
private void UpdateButtonStyle(Button btn, bool dark)
{
btn.BackColor = dark ? Color.FromArgb(63, 63, 70) : Color.White;
btn.ForeColor = dark ? Color.White : Color.Black;
}
private void UpdateNumericButtonStyle(Button btn, bool dark)
{
btn.BackColor = dark ? Color.FromArgb(45, 45, 48) : Color.FromArgb(230, 230, 230);
btn.ForeColor = dark ? Color.White : Color.Black;
}
private void UpdateTextBoxStyle(TextBox txt, bool dark)
{
Color bgColor = dark ? Color.FromArgb(45, 45, 48) : Color.White;
txt.BackColor = bgColor;
txt.ForeColor = dark ? Color.White : Color.Black;
// Also update parent container if it exists
if (txt.Parent is Panel p)
{
p.BackColor = bgColor;
}
}
private void btnTheme_Click(object sender, EventArgs e)
{
_settings.IsDarkMode = !_settings.IsDarkMode;
_settings.Save();
ApplyTheme();
}
private void btnStartStop_Click(object sender, EventArgs e)
{
// Save settings
if (int.TryParse(txtWork.Text, out int w)) _settings.WorkMinutes = w;
if (int.TryParse(txtRest.Text, out int r)) _settings.RestMinutes = r;
_settings.Save();
ApplySettings();
_monitor.RefreshStatus(); // Force update UI
// MessageBox removed to prevent blocking timer
}
private void btnReset_Click(object sender, EventArgs e)
{
_monitor.Restart();
}
private void btnPause_Click(object sender, EventArgs e)
{
if (_monitor.IsPaused)
{
_monitor.Resume();
btnPause.Text = "暂停";
}
else
{
_monitor.Pause();
btnPause.Text = "恢复";
}
}
private void ApplySettings()
{
int workMin = 20;
int restMin = 1;
int.TryParse(txtWork.Text, out workMin);
int.TryParse(txtRest.Text, out restMin);
_monitor.WorkDuration = TimeSpan.FromMinutes(workMin);
_monitor.RestDuration = TimeSpan.FromMinutes(restMin);
_monitor.IdleThreshold = TimeSpan.FromSeconds(_settings.IdleThresholdSeconds);
}
private void Monitor_StateChanged(object? sender, EventArgs e)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object?, EventArgs>(Monitor_StateChanged), sender, e);
return;
}
UpdateStatusUI();
}
private void UpdateStatusUI()
{
if (_monitor == null) return;
bool dark = _settings.IsDarkMode;
// 更新暂停按钮状态
if (_monitor.CurrentState == MonitorState.Idle)
{
btnPause.Enabled = false;
btnPause.Text = "暂停";
}
else
{
btnPause.Enabled = true;
btnPause.Text = _monitor.IsPaused ? "恢复" : "暂停";
}
switch (_monitor.CurrentState)
{
case MonitorState.Idle:
lblStatus.Text = "状态: 空闲";
lblStatus.ForeColor = Color.Gray;
lblTimeLeft.Text = "--:--";
lblTimeLeft.ForeColor = Color.Gray;
break;
case MonitorState.Working:
lblStatus.Text = _monitor.IsPaused ? "状态: 工作中 (已暂停)" : "状态: 工作中";
lblStatus.ForeColor = dark ? Color.LightGreen : Color.Green;
lblTimeLeft.ForeColor = dark ? Color.White : Color.Black;
break;
case MonitorState.Resting:
lblStatus.Text = _monitor.IsPaused ? "状态: 休息中 (已暂停)" : "状态: 休息中";
lblStatus.ForeColor = dark ? Color.LightSkyBlue : Color.Blue;
lblTimeLeft.ForeColor = dark ? Color.LightSkyBlue : Color.Blue;
break;
}
}
private void Monitor_WorkProgressChanged(object? sender, TimeSpan remaining)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object?, TimeSpan>(Monitor_WorkProgressChanged), sender, remaining);
return;
}
lblTimeLeft.Text = $"{remaining.Minutes:D2}:{remaining.Seconds:D2}";
// Update tray tooltip
string newText;
if (remaining.TotalMinutes < 1)
{
newText = $"即将休息: {remaining.Seconds}秒";
}
else
{
newText = $"工作中: 剩余 {remaining.Minutes} 分钟";
}
if (notifyIcon1.Text != newText)
{
notifyIcon1.Text = newText;
}
}
private void Monitor_RestStarted(object? sender, EventArgs e)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object?, EventArgs>(Monitor_RestStarted), sender, e);
return;
}
// Show rest form
if (_restForm == null || _restForm.IsDisposed)
{
_restForm = new RestForm();
_restForm.SkipRequested += RestForm_SkipRequested;
}
_restForm.Show();
}
private void RestForm_SkipRequested(object? sender, EventArgs e)
{
_monitor.Stop();
_monitor.Start();
}
private void Monitor_RestProgressChanged(object? sender, TimeSpan remaining)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object?, TimeSpan>(Monitor_RestProgressChanged), sender, remaining);
return;
}
if (_restForm != null && !_restForm.IsDisposed && _restForm.Visible)
{
_restForm.UpdateTime(remaining);
}
}
private void Monitor_RestEnded(object? sender, EventArgs e)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object?, EventArgs>(Monitor_RestEnded), sender, e);
return;
}
if (_restForm != null && !_restForm.IsDisposed)
{
_restForm.Close();
}
notifyIcon1.ShowBalloonTip(3000, "休息结束", "继续加油工作吧!", ToolTipIcon.Info);
}
private void btnHide_Click(object sender, EventArgs e)
{
this.Hide();
if (!_settings.HasShownMinimizeTip)
{
notifyIcon1.ShowBalloonTip(2000, "已隐藏", "程序仍在后台运行,双击托盘图标恢复。", ToolTipIcon.Info);
_settings.HasShownMinimizeTip = true;
_settings.Save();
}
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
this.Hide();
if (!_settings.HasShownMinimizeTip)
{
notifyIcon1.ShowBalloonTip(2000, "已隐藏", "程序仍在后台运行,双击托盘图标恢复。", ToolTipIcon.Info);
_settings.HasShownMinimizeTip = true;
_settings.Save();
}
}
else
{
notifyIcon1.Visible = false;
notifyIcon1.Dispose();
}
}
private void notifyIcon1_MouseDoubleClick(object? sender, MouseEventArgs e)
{
ShowForm();
}
private void toolStripMenuItemShow_Click(object? sender, EventArgs e)
{
ShowForm();
}
private void ShowForm()
{
if (this.IsDisposed) return;
this.Show();
if (this.WindowState == FormWindowState.Minimized)
{
this.WindowState = FormWindowState.Normal;
}
this.Activate();
this.BringToFront();
}
public void ActivateFromExternal()
{
if (InvokeRequired)
{
BeginInvoke(new Action(ActivateFromExternal));
return;
}
this.Show();
this.WindowState = FormWindowState.Normal;
this.ShowInTaskbar = true;
this.BringToFront();
this.Activate();
this.TopMost = true;
this.TopMost = false;
}
private void toolStripMenuItemExit_Click(object sender, EventArgs e)
{
_monitor.Stop();
notifyIcon1.Visible = false;
notifyIcon1.Dispose();
Application.Exit();
Environment.Exit(0);
}
private void pnlTitle_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
ReleaseCapture();
SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
}
}
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
private void btnMin_Click(object sender, EventArgs e)
{
this.WindowState = FormWindowState.Minimized;
}
}
}