add: 久坐提醒项目文件

This commit is contained in:
2026-01-17 12:48:01 +08:00
commit 5fba4ff110
10 changed files with 1535 additions and 0 deletions

477
MainForm.cs Normal file
View File

@@ -0,0 +1,477 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TimerApp
{
public partial class MainForm : Form
{
private ActivityMonitor _monitor;
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();
}
private void MainForm_Load(object sender, EventArgs e)
{
// Load settings
_settings = AppSettings.Load();
txtWork.Text = _settings.WorkMinutes.ToString();
txtRest.Text = _settings.RestMinutes.ToString();
// Apply Theme
ApplyTheme();
// 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);
_monitor.Start();
// Set tray icon
try
{
// Generate and set custom icon
Icon icon = IconGenerator.GenerateClockIcon();
this.Icon = icon;
notifyIcon1.Icon = icon;
}
catch
{
notifyIcon1.Icon = SystemIcons.Application;
}
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.BackColor = bg; // Revert to bg color (user preference)
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(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;
// 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 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)
{
Invoke(new Action<object, EventArgs>(Monitor_StateChanged), sender, e);
return;
}
UpdateStatusUI();
}
private void UpdateStatusUI()
{
if (_monitor == null) return;
bool dark = _settings.IsDarkMode;
switch (_monitor.CurrentState)
{
case MonitorState.Idle:
lblStatus.Text = "状态: 空闲";
lblStatus.ForeColor = Color.Gray;
lblTimeLeft.Text = "--:--";
lblTimeLeft.ForeColor = Color.Gray;
break;
case MonitorState.Working:
lblStatus.Text = "状态: 工作中";
lblStatus.ForeColor = dark ? Color.LightGreen : Color.Green;
lblTimeLeft.ForeColor = dark ? Color.White : Color.Black;
break;
case MonitorState.Resting:
lblStatus.Text = "状态: 休息中";
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)
{
Invoke(new Action<object, TimeSpan>(Monitor_WorkProgressChanged), sender, remaining);
return;
}
lblTimeLeft.Text = $"{remaining.Minutes:D2}:{remaining.Seconds:D2}";
// Update tray tooltip
if (remaining.TotalMinutes < 1)
{
notifyIcon1.Text = $"即将休息: {remaining.Seconds}秒";
}
else
{
notifyIcon1.Text = $"工作中: 剩余 {remaining.Minutes} 分钟";
}
}
private void Monitor_RestStarted(object sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(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 (_restForm != null && !_restForm.IsDisposed && _restForm.Visible)
{
_restForm.UpdateTime(remaining);
}
}
private void Monitor_RestEnded(object sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new Action<object, EventArgs>(Monitor_RestEnded), sender, e);
return;
}
if (_restForm != null && !_restForm.IsDisposed)
{
_restForm.Close();
}
notifyIcon1.ShowBalloonTip(3000, "休息结束", "继续加油工作吧!", ToolTipIcon.Info);
}
private bool _hasShownMinimizeTip = false;
private void btnHide_Click(object sender, EventArgs e)
{
this.Hide();
if (!_hasShownMinimizeTip)
{
notifyIcon1.ShowBalloonTip(2000, "已隐藏", "程序仍在后台运行,双击托盘图标恢复。", ToolTipIcon.Info);
_hasShownMinimizeTip = true;
}
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
this.Hide();
if (!_hasShownMinimizeTip)
{
notifyIcon1.ShowBalloonTip(2000, "已隐藏", "程序仍在后台运行,双击托盘图标恢复。", ToolTipIcon.Info);
_hasShownMinimizeTip = true;
}
}
}
private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
{
ShowForm();
}
private void toolStripMenuItemShow_Click(object sender, EventArgs e)
{
ShowForm();
}
private void ShowForm()
{
this.Show();
this.WindowState = FormWindowState.Normal;
this.Activate();
}
private void toolStripMenuItemExit_Click(object sender, EventArgs e)
{
_monitor.Stop();
notifyIcon1.Visible = false;
Application.Exit();
}
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;
}
}
}