From 5fba4ff110531e308f1f0cdfed5df7e13b115868 Mon Sep 17 00:00:00 2001 From: Solin Date: Sat, 17 Jan 2026 12:48:01 +0800 Subject: [PATCH] =?UTF-8?q?add:=20=E4=B9=85=E5=9D=90=E6=8F=90=E9=86=92?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 91 +++++++++ ActivityMonitor.cs | 182 +++++++++++++++++ AppSettings.cs | 46 +++++ IconGenerator.cs | 67 ++++++ MainForm.Designer.cs | 431 ++++++++++++++++++++++++++++++++++++++ MainForm.cs | 477 +++++++++++++++++++++++++++++++++++++++++++ NativeMethods.cs | 52 +++++ Program.cs | 16 ++ RestForm.cs | 162 +++++++++++++++ TimerApp.csproj | 11 + 10 files changed, 1535 insertions(+) create mode 100644 .gitignore create mode 100644 ActivityMonitor.cs create mode 100644 AppSettings.cs create mode 100644 IconGenerator.cs create mode 100644 MainForm.Designer.cs create mode 100644 MainForm.cs create mode 100644 NativeMethods.cs create mode 100644 Program.cs create mode 100644 RestForm.cs create mode 100644 TimerApp.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f11773 --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +## .NET 项目忽略文件 + +# 编译输出 +[Bb]in/ +[Oo]bj/ +[Oo]ut/ + +# Visual Studio 用户特定文件 +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio 缓存/选项目录 +.vs/ +.vscode/ + +# Visual Studio 代码分析结果 +*.sln.iml + +# 构建结果 +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# NuGet 包 +*.nupkg +*.snupkg +**/packages/* +!**/packages/build/ +*.nuget.props +*.nuget.targets + +# MSTest 测试结果 +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# .NET Core 项目文件 +project.lock.json +project.fragment.lock.json +artifacts/ + +# Rider +.idea/ +*.sln.iml + +# VS Code +.vscode/ + +# 用户特定文件 +*.user +*.suo +*.userosscache +*.sln.docstates + +# 临时文件 +*.tmp +*.temp +*.log +*.cache + +# 操作系统文件 +.DS_Store +Thumbs.db +ehthumbs.db +Desktop.ini + +# 备份文件 +*.bak +*.swp +*~ + +# 设置文件(如果包含敏感信息) +# settings.json diff --git a/ActivityMonitor.cs b/ActivityMonitor.cs new file mode 100644 index 0000000..65b4791 --- /dev/null +++ b/ActivityMonitor.cs @@ -0,0 +1,182 @@ +using System; +using System.Windows.Forms; + +namespace TimerApp +{ + public enum MonitorState + { + Idle, + Working, + Resting + } + + public class ActivityMonitor + { + private System.Windows.Forms.Timer _timer; + private DateTime _lastWorkStartTime; + private TimeSpan _accumulatedWorkTime; + private DateTime _restStartTime; + + // 配置 (默认值) + public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20); + public TimeSpan RestDuration { get; set; } = TimeSpan.FromMinutes(1); + public TimeSpan IdleThreshold { get; set; } = TimeSpan.FromSeconds(30); + + public MonitorState CurrentState { get; private set; } = MonitorState.Idle; + + // 事件 + public event EventHandler WorkProgressChanged; // 剩余工作时间 + public event EventHandler RestProgressChanged; // 剩余休息时间 + public event EventHandler RestStarted; + public event EventHandler RestEnded; + public event EventHandler StateChanged; + + public ActivityMonitor() + { + _timer = new System.Windows.Forms.Timer(); + _timer.Interval = 1000; // 1秒检查一次 + _timer.Tick += Timer_Tick; + } + + public void Start() + { + _timer.Start(); + ResetWork(); + } + + public void Stop() + { + _timer.Stop(); + } + + private void ResetWork() + { + _accumulatedWorkTime = TimeSpan.Zero; + _lastWorkStartTime = DateTime.Now; + ChangeState(MonitorState.Idle); + } + + private void ChangeState(MonitorState newState) + { + if (CurrentState != newState) + { + CurrentState = newState; + StateChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void Timer_Tick(object sender, EventArgs e) + { + long idleMs = NativeMethods.GetIdleTime(); + TimeSpan idleTime = TimeSpan.FromMilliseconds(idleMs); + + if (CurrentState == MonitorState.Resting) + { + // 休息模式逻辑 + TimeSpan timeInRest = DateTime.Now - _restStartTime; + TimeSpan remainingRest = RestDuration - timeInRest; + + if (remainingRest <= TimeSpan.Zero) + { + // 休息结束 + RestEnded?.Invoke(this, EventArgs.Empty); + ResetWork(); // 重新开始工作周期 + } + else + { + RestProgressChanged?.Invoke(this, remainingRest); + } + } + else + { + // 工作/空闲模式逻辑 + if (idleTime > IdleThreshold) + { + // 用户离开了 + if (CurrentState == MonitorState.Working) + { + // 如果正在工作,但离开了,暂停工作计时? + // 简单起见,如果离开时间过长,可以视为一种“休息”,或者只是暂停累积 + // 这里我们简单处理:如果空闲时间超过阈值,状态变为空闲 + ChangeState(MonitorState.Idle); + } + + // 如果在 Idle 状态,且空闲时间非常长(比如超过了休息时间), + // 是否应该重置工作计时器? + // 假设用户去开会了1小时,回来应该重新计算20分钟。 + if (idleTime > RestDuration) + { + _accumulatedWorkTime = TimeSpan.Zero; + } + } + else + { + // 用户在活动 + if (CurrentState == MonitorState.Idle) + { + // 从空闲变为工作 + ChangeState(MonitorState.Working); + _lastWorkStartTime = DateTime.Now; + } + + // 累加工作时间 + // 简单的累加逻辑:这一秒是工作的 + _accumulatedWorkTime += TimeSpan.FromSeconds(1); + + // 检查是否达到工作时长 + TimeSpan remainingWork = WorkDuration - _accumulatedWorkTime; + + if (remainingWork <= TimeSpan.Zero) + { + // 触发休息 + _restStartTime = DateTime.Now; + ChangeState(MonitorState.Resting); + RestStarted?.Invoke(this, EventArgs.Empty); + } + else + { + WorkProgressChanged?.Invoke(this, remainingWork); + } + } + } + } + + // 用于强制重置或测试 + public void ForceRest() + { + _restStartTime = DateTime.Now; + ChangeState(MonitorState.Resting); + RestStarted?.Invoke(this, EventArgs.Empty); + } + + public void RefreshStatus() + { + if (CurrentState == MonitorState.Working) + { + TimeSpan remaining = WorkDuration - _accumulatedWorkTime; + WorkProgressChanged?.Invoke(this, remaining); + } + else if (CurrentState == MonitorState.Resting) + { + TimeSpan timeInRest = DateTime.Now - _restStartTime; + TimeSpan remaining = RestDuration - timeInRest; + RestProgressChanged?.Invoke(this, remaining); + } + } + + public void Restart() + { + _accumulatedWorkTime = TimeSpan.Zero; + _lastWorkStartTime = DateTime.Now; + + // Ensure timer is running + if (!_timer.Enabled) _timer.Start(); + + // Force state to Working since user manually restarted + ChangeState(MonitorState.Working); + + // Immediately refresh UI to show full duration + RefreshStatus(); + } + } +} diff --git a/AppSettings.cs b/AppSettings.cs new file mode 100644 index 0000000..cd3d5c4 --- /dev/null +++ b/AppSettings.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace TimerApp +{ + public class AppSettings + { + public int WorkMinutes { get; set; } = 20; + public int RestMinutes { get; set; } = 1; + public int IdleThresholdSeconds { get; set; } = 30; + public bool IsDarkMode { get; set; } = true; + + private static string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); + + public static AppSettings Load() + { + try + { + if (File.Exists(ConfigPath)) + { + string json = File.ReadAllText(ConfigPath); + return JsonSerializer.Deserialize(json); + } + } + catch + { + // ignore errors, return default + } + return new AppSettings(); + } + + public void Save() + { + try + { + string json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(ConfigPath, json); + } + catch + { + // ignore + } + } + } +} diff --git a/IconGenerator.cs b/IconGenerator.cs new file mode 100644 index 0000000..bf13708 --- /dev/null +++ b/IconGenerator.cs @@ -0,0 +1,67 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.IO; + +namespace TimerApp +{ + public static class IconGenerator + { + public static Icon GenerateClockIcon() + { + int size = 64; + using (Bitmap bmp = new Bitmap(size, size)) + using (Graphics g = Graphics.FromImage(bmp)) + { + g.SmoothingMode = SmoothingMode.AntiAlias; + g.Clear(Color.Transparent); + + // 绘制闹钟主体 + int margin = 4; + int clockSize = size - margin * 2; + Rectangle rect = new Rectangle(margin, margin, clockSize, clockSize); + + // 外圈 + using (Pen pen = new Pen(Color.White, 4)) + { + g.DrawEllipse(pen, rect); + } + + // 两个耳朵 (闹钟铃) + using (Brush earBrush = new SolidBrush(Color.White)) + { + // 左耳 + g.FillPie(earBrush, 0, 0, size/2, size/2, 200, 50); + // 右耳 + g.FillPie(earBrush, size/2, 0, size/2, size/2, 290, 50); + } + + // 重绘外圈盖住耳朵连接处 + using (Brush bgBrush = new SolidBrush(Color.FromArgb(30, 30, 30))) // 深色背景 + { + g.FillEllipse(bgBrush, rect); + } + using (Pen pen = new Pen(Color.White, 3)) + { + g.DrawEllipse(pen, rect); + } + + // 指针 + Point center = new Point(size / 2, size / 2); + using (Pen handPen = new Pen(Color.LightGreen, 3)) + { + handPen.EndCap = LineCap.Round; + // 时针 + g.DrawLine(handPen, center, new Point(center.X + 10, center.Y - 10)); + // 分针 + g.DrawLine(handPen, center, new Point(center.X, center.Y - 18)); + } + + // 转换为图标 + // 注意:GetHicon 需要释放 + IntPtr hIcon = bmp.GetHicon(); + return Icon.FromHandle(hIcon); + } + } + } +} diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs new file mode 100644 index 0000000..c2abd08 --- /dev/null +++ b/MainForm.Designer.cs @@ -0,0 +1,431 @@ +namespace TimerApp +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.label1 = new System.Windows.Forms.Label(); + this.btnWorkMinus = new System.Windows.Forms.Button(); + this.txtWork = new System.Windows.Forms.TextBox(); + this.btnWorkPlus = new System.Windows.Forms.Button(); + + this.label2 = new System.Windows.Forms.Label(); + this.btnRestMinus = new System.Windows.Forms.Button(); + this.txtRest = new System.Windows.Forms.TextBox(); + this.btnRestPlus = new System.Windows.Forms.Button(); + + this.btnStartStop = new System.Windows.Forms.Button(); + this.btnReset = new System.Windows.Forms.Button(); + this.lblStatus = new System.Windows.Forms.Label(); + this.lblTimeLeft = new System.Windows.Forms.Label(); + this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components); + this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); + this.toolStripMenuItemShow = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripMenuItemExit = new System.Windows.Forms.ToolStripMenuItem(); + this.btnHide = new System.Windows.Forms.Button(); + this.pnlTitle = new System.Windows.Forms.Panel(); + this.lblTitle = new System.Windows.Forms.Label(); + this.btnClose = new System.Windows.Forms.Button(); + this.btnMin = new System.Windows.Forms.Button(); + this.btnTheme = new System.Windows.Forms.Button(); + this.pnlSettings = new System.Windows.Forms.Panel(); + + this.contextMenuStrip1.SuspendLayout(); + this.pnlTitle.SuspendLayout(); + this.pnlSettings.SuspendLayout(); + this.SuspendLayout(); + + // + // pnlTitle + // + this.pnlTitle.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.pnlTitle.Controls.Add(this.btnTheme); + this.pnlTitle.Controls.Add(this.btnMin); + this.pnlTitle.Controls.Add(this.btnClose); + this.pnlTitle.Controls.Add(this.lblTitle); + this.pnlTitle.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlTitle.Location = new System.Drawing.Point(0, 0); + this.pnlTitle.Name = "pnlTitle"; + this.pnlTitle.Size = new System.Drawing.Size(320, 40); + this.pnlTitle.TabIndex = 10; + this.pnlTitle.MouseDown += new System.Windows.Forms.MouseEventHandler(this.pnlTitle_MouseDown); + + // + // lblTitle + // + this.lblTitle.AutoSize = true; + this.lblTitle.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.lblTitle.ForeColor = System.Drawing.Color.LightGray; + this.lblTitle.Location = new System.Drawing.Point(12, 9); + this.lblTitle.Name = "lblTitle"; + this.lblTitle.Size = new System.Drawing.Size(82, 23); + this.lblTitle.TabIndex = 0; + this.lblTitle.Text = "久坐提醒"; + this.lblTitle.MouseDown += new System.Windows.Forms.MouseEventHandler(this.pnlTitle_MouseDown); + + // + // btnClose + // + this.btnClose.Dock = System.Windows.Forms.DockStyle.Right; + this.btnClose.FlatAppearance.BorderSize = 0; + this.btnClose.FlatAppearance.MouseOverBackColor = System.Drawing.Color.Red; + this.btnClose.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnClose.ForeColor = System.Drawing.Color.White; + this.btnClose.Location = new System.Drawing.Point(280, 0); + this.btnClose.Name = "btnClose"; + this.btnClose.Size = new System.Drawing.Size(40, 40); + this.btnClose.TabIndex = 1; + this.btnClose.Text = "✕"; + this.btnClose.UseVisualStyleBackColor = true; + this.btnClose.Click += new System.EventHandler(this.btnClose_Click); + + // + // btnTheme + // + this.btnTheme.Dock = System.Windows.Forms.DockStyle.Right; + this.btnTheme.FlatAppearance.BorderSize = 0; + this.btnTheme.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnTheme.ForeColor = System.Drawing.Color.White; + this.btnTheme.Location = new System.Drawing.Point(200, 0); + this.btnTheme.Name = "btnTheme"; + this.btnTheme.Size = new System.Drawing.Size(40, 40); + this.btnTheme.TabIndex = 3; + this.btnTheme.Text = "☀"; + this.btnTheme.UseVisualStyleBackColor = true; + this.btnTheme.Click += new System.EventHandler(this.btnTheme_Click); + + // + // btnMin + // + this.btnMin.Dock = System.Windows.Forms.DockStyle.Right; + this.btnMin.FlatAppearance.BorderSize = 0; + this.btnMin.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnMin.ForeColor = System.Drawing.Color.White; + this.btnMin.Location = new System.Drawing.Point(240, 0); + this.btnMin.Name = "btnMin"; + this.btnMin.Size = new System.Drawing.Size(40, 40); + this.btnMin.TabIndex = 2; + this.btnMin.Text = "―"; + this.btnMin.UseVisualStyleBackColor = true; + this.btnMin.Click += new System.EventHandler(this.btnMin_Click); + + // + // lblTimeLeft + // + this.lblTimeLeft.Dock = System.Windows.Forms.DockStyle.Top; + this.lblTimeLeft.Font = new System.Drawing.Font("Segoe UI Light", 48F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.lblTimeLeft.ForeColor = System.Drawing.Color.White; + this.lblTimeLeft.Location = new System.Drawing.Point(0, 40); + this.lblTimeLeft.Name = "lblTimeLeft"; + this.lblTimeLeft.Size = new System.Drawing.Size(320, 120); + this.lblTimeLeft.TabIndex = 6; + this.lblTimeLeft.Text = "00:00"; + this.lblTimeLeft.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + + // + // lblStatus + // + this.lblStatus.Dock = System.Windows.Forms.DockStyle.Top; + this.lblStatus.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.lblStatus.ForeColor = System.Drawing.Color.Gray; + this.lblStatus.Location = new System.Drawing.Point(0, 160); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(320, 40); + this.lblStatus.TabIndex = 5; + this.lblStatus.Text = "Idle"; + this.lblStatus.TextAlign = System.Drawing.ContentAlignment.TopCenter; + + // + // pnlSettings + // + this.pnlSettings.Controls.Add(this.btnReset); + this.pnlSettings.Controls.Add(this.btnStartStop); + this.pnlSettings.Controls.Add(this.btnRestPlus); + this.pnlSettings.Controls.Add(this.txtRest); + this.pnlSettings.Controls.Add(this.btnRestMinus); + this.pnlSettings.Controls.Add(this.label2); + this.pnlSettings.Controls.Add(this.btnWorkPlus); + this.pnlSettings.Controls.Add(this.txtWork); + this.pnlSettings.Controls.Add(this.btnWorkMinus); + this.pnlSettings.Controls.Add(this.label1); + this.pnlSettings.Dock = System.Windows.Forms.DockStyle.Bottom; + this.pnlSettings.Location = new System.Drawing.Point(0, 180); + this.pnlSettings.Name = "pnlSettings"; + this.pnlSettings.Size = new System.Drawing.Size(320, 170); + this.pnlSettings.TabIndex = 11; + + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.label1.ForeColor = System.Drawing.Color.LightGray; + this.label1.Location = new System.Drawing.Point(40, 20); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(100, 23); + this.label1.TabIndex = 0; + this.label1.Text = "工作 (分):"; + + // + // btnWorkMinus + // + this.btnWorkMinus.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.btnWorkMinus.FlatAppearance.BorderSize = 0; + this.btnWorkMinus.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnWorkMinus.ForeColor = System.Drawing.Color.White; + this.btnWorkMinus.Location = new System.Drawing.Point(160, 18); + this.btnWorkMinus.Name = "btnWorkMinus"; + this.btnWorkMinus.Size = new System.Drawing.Size(30, 30); + this.btnWorkMinus.TabIndex = 1; + this.btnWorkMinus.Text = "-"; + this.btnWorkMinus.UseVisualStyleBackColor = false; + + // + // txtWork + // + this.txtWork.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.txtWork.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.txtWork.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.txtWork.ForeColor = System.Drawing.Color.White; + this.txtWork.Location = new System.Drawing.Point(190, 18); + this.txtWork.Name = "txtWork"; + this.txtWork.ReadOnly = false; + this.txtWork.Size = new System.Drawing.Size(60, 30); + this.txtWork.TabIndex = 2; + this.txtWork.Text = "20"; + this.txtWork.TextAlign = System.Windows.Forms.HorizontalAlignment.Center; + + // + // btnWorkPlus + // + this.btnWorkPlus.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.btnWorkPlus.FlatAppearance.BorderSize = 0; + this.btnWorkPlus.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnWorkPlus.ForeColor = System.Drawing.Color.White; + this.btnWorkPlus.Location = new System.Drawing.Point(250, 18); + this.btnWorkPlus.Name = "btnWorkPlus"; + this.btnWorkPlus.Size = new System.Drawing.Size(30, 30); + this.btnWorkPlus.TabIndex = 3; + this.btnWorkPlus.Text = "+"; + this.btnWorkPlus.UseVisualStyleBackColor = false; + + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.label2.ForeColor = System.Drawing.Color.LightGray; + this.label2.Location = new System.Drawing.Point(40, 60); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(100, 23); + this.label2.TabIndex = 4; + this.label2.Text = "休息 (分):"; + + // + // btnRestMinus + // + this.btnRestMinus.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.btnRestMinus.FlatAppearance.BorderSize = 0; + this.btnRestMinus.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnRestMinus.ForeColor = System.Drawing.Color.White; + this.btnRestMinus.Location = new System.Drawing.Point(160, 58); + this.btnRestMinus.Name = "btnRestMinus"; + this.btnRestMinus.Size = new System.Drawing.Size(30, 30); + this.btnRestMinus.TabIndex = 5; + this.btnRestMinus.Text = "-"; + this.btnRestMinus.UseVisualStyleBackColor = false; + + // + // txtRest + // + this.txtRest.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.txtRest.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.txtRest.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.txtRest.ForeColor = System.Drawing.Color.White; + this.txtRest.Location = new System.Drawing.Point(190, 58); + this.txtRest.Name = "txtRest"; + this.txtRest.ReadOnly = false; + this.txtRest.Size = new System.Drawing.Size(60, 30); + this.txtRest.TabIndex = 6; + this.txtRest.Text = "1"; + this.txtRest.TextAlign = System.Windows.Forms.HorizontalAlignment.Center; + + // + // btnRestPlus + // + this.btnRestPlus.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.btnRestPlus.FlatAppearance.BorderSize = 0; + this.btnRestPlus.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnRestPlus.ForeColor = System.Drawing.Color.White; + this.btnRestPlus.Location = new System.Drawing.Point(250, 58); + this.btnRestPlus.Name = "btnRestPlus"; + this.btnRestPlus.Size = new System.Drawing.Size(30, 30); + this.btnRestPlus.TabIndex = 7; + this.btnRestPlus.Text = "+"; + this.btnRestPlus.UseVisualStyleBackColor = false; + + // + // btnStartStop + // + this.btnStartStop.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); + this.btnStartStop.FlatAppearance.BorderSize = 0; + this.btnStartStop.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnStartStop.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.btnStartStop.ForeColor = System.Drawing.Color.White; + this.btnStartStop.Location = new System.Drawing.Point(170, 110); + this.btnStartStop.Name = "btnStartStop"; + this.btnStartStop.Size = new System.Drawing.Size(110, 35); + this.btnStartStop.TabIndex = 4; + this.btnStartStop.Text = "应用设置"; + this.btnStartStop.UseVisualStyleBackColor = false; + this.btnStartStop.Click += new System.EventHandler(this.btnStartStop_Click); + + // + // btnReset + // + this.btnReset.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); + this.btnReset.FlatAppearance.BorderSize = 0; + this.btnReset.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnReset.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.btnReset.ForeColor = System.Drawing.Color.White; + this.btnReset.Location = new System.Drawing.Point(40, 110); + this.btnReset.Name = "btnReset"; + this.btnReset.Size = new System.Drawing.Size(110, 35); + this.btnReset.TabIndex = 8; + this.btnReset.Text = "重置计时"; + this.btnReset.UseVisualStyleBackColor = false; + this.btnReset.Click += new System.EventHandler(this.btnReset_Click); + + // + // btnHide + // + this.btnHide.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); + this.btnHide.FlatAppearance.BorderSize = 0; + this.btnHide.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnHide.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.btnHide.ForeColor = System.Drawing.Color.White; + this.btnHide.Location = new System.Drawing.Point(40, 160); + this.btnHide.Name = "btnHide"; + this.btnHide.Size = new System.Drawing.Size(220, 35); + this.btnHide.TabIndex = 7; + this.btnHide.Text = "最小化到托盘"; + this.btnHide.UseVisualStyleBackColor = false; + this.btnHide.Visible = false; + this.btnHide.Click += new System.EventHandler(this.btnHide_Click); + + // + // notifyIcon1 + // + this.notifyIcon1.ContextMenuStrip = this.contextMenuStrip1; + this.notifyIcon1.Text = "TimerApp"; + this.notifyIcon1.Visible = true; + this.notifyIcon1.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.notifyIcon1_MouseDoubleClick); + + // + // contextMenuStrip1 + // + this.contextMenuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); + this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.toolStripMenuItemShow, + this.toolStripMenuItemExit}); + this.contextMenuStrip1.Name = "contextMenuStrip1"; + this.contextMenuStrip1.Size = new System.Drawing.Size(109, 52); + + // + // toolStripMenuItemShow + // + this.toolStripMenuItemShow.Name = "toolStripMenuItemShow"; + this.toolStripMenuItemShow.Size = new System.Drawing.Size(108, 24); + this.toolStripMenuItemShow.Text = "显示"; + this.toolStripMenuItemShow.Click += new System.EventHandler(this.toolStripMenuItemShow_Click); + + // + // toolStripMenuItemExit + // + this.toolStripMenuItemExit.Name = "toolStripMenuItemExit"; + this.toolStripMenuItemExit.Size = new System.Drawing.Size(108, 24); + this.toolStripMenuItemExit.Text = "退出"; + this.toolStripMenuItemExit.Click += new System.EventHandler(this.toolStripMenuItemExit_Click); + + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(30)))), ((int)(((byte)(30))))); + this.ClientSize = new System.Drawing.Size(320, 380); + this.Controls.Add(this.pnlSettings); // Add panel first to be at bottom of z-order + this.Controls.Add(this.lblStatus); + this.Controls.Add(this.lblTimeLeft); + this.Controls.Add(this.pnlTitle); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.Name = "MainForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Focus Timer"; + this.Load += new System.EventHandler(this.MainForm_Load); + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); + + // Ensure pnlSettings matches form background initially + this.pnlSettings.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(30)))), ((int)(((byte)(30))))); + + this.contextMenuStrip1.ResumeLayout(false); + this.pnlTitle.ResumeLayout(false); + this.pnlTitle.PerformLayout(); + this.pnlSettings.ResumeLayout(false); + this.pnlSettings.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button btnWorkMinus; + private System.Windows.Forms.TextBox txtWork; + private System.Windows.Forms.Button btnWorkPlus; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Button btnRestMinus; + private System.Windows.Forms.TextBox txtRest; + private System.Windows.Forms.Button btnRestPlus; + private System.Windows.Forms.Button btnStartStop; + private System.Windows.Forms.Button btnReset; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.Label lblTimeLeft; + private System.Windows.Forms.NotifyIcon notifyIcon1; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; + private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemShow; + private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemExit; + private System.Windows.Forms.Button btnHide; + private System.Windows.Forms.Panel pnlTitle; + private System.Windows.Forms.Button btnClose; + private System.Windows.Forms.Button btnMin; + private System.Windows.Forms.Button btnTheme; + private System.Windows.Forms.Label lblTitle; + private System.Windows.Forms.Panel pnlSettings; + } +} diff --git a/MainForm.cs b/MainForm.cs new file mode 100644 index 0000000..fe5aab6 --- /dev/null +++ b/MainForm.cs @@ -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(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(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(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(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; + } + } +} diff --git a/NativeMethods.cs b/NativeMethods.cs new file mode 100644 index 0000000..a295cbc --- /dev/null +++ b/NativeMethods.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.InteropServices; + +namespace TimerApp +{ + public static class NativeMethods + { + [StructLayout(LayoutKind.Sequential)] + struct LASTINPUTINFO + { + public static readonly int SizeOf = Marshal.SizeOf(typeof(LASTINPUTINFO)); + + [MarshalAs(UnmanagedType.U4)] + public UInt32 cbSize; + [MarshalAs(UnmanagedType.U4)] + public UInt32 dwTime; + } + + [DllImport("user32.dll")] + static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + + /// + /// 获取系统空闲时间(毫秒) + /// + /// + public static long GetIdleTime() + { + LASTINPUTINFO lastInputInfo = new LASTINPUTINFO(); + lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo); + 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 是 int,GetLastInputInfo 是 uint) + // 简单的做法: + return (long)GetTickCount() - (long)lastInputInfo.dwTime; + } + + return 0; + } + + [DllImport("kernel32.dll")] + static extern uint GetTickCount(); + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f7fe7c5 --- /dev/null +++ b/Program.cs @@ -0,0 +1,16 @@ +namespace TimerApp; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } +} \ No newline at end of file diff --git a/RestForm.cs b/RestForm.cs new file mode 100644 index 0000000..4f5a2ef --- /dev/null +++ b/RestForm.cs @@ -0,0 +1,162 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace TimerApp +{ + public class RestForm : Form + { + private Label lblMessage; + private Label lblTimer; + private Button btnSkip; + + public event EventHandler SkipRequested; + + public RestForm() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + this.lblMessage = new System.Windows.Forms.Label(); + this.lblTimer = new System.Windows.Forms.Label(); + // this.btnSkip = new RoundedButton(); // Removed custom button + this.SuspendLayout(); + + // ... (keep form settings) + // + // Form 设置 + // + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.WindowState = FormWindowState.Maximized; + this.TopMost = true; + this.BackColor = System.Drawing.Color.Black; + this.Opacity = 0.90; // 90% 不透明度 + this.ShowInTaskbar = false; + this.Name = "RestForm"; + this.Text = "Rest Now"; + this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); + + // + // lblMessage + // + this.lblMessage.AutoSize = false; + this.lblMessage.Font = new System.Drawing.Font("Microsoft YaHei UI", 36F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); + this.lblMessage.ForeColor = System.Drawing.Color.White; + this.lblMessage.Location = new System.Drawing.Point(100, 100); + this.lblMessage.Name = "lblMessage"; + // 初始大小,会在CenterControls中动态调整 + this.lblMessage.Size = new System.Drawing.Size(800, 100); + this.lblMessage.TabIndex = 0; + this.lblMessage.Text = "休息一下,看看远方"; + this.lblMessage.TextAlign = ContentAlignment.MiddleCenter; + + // + // lblTimer + // + this.lblTimer.AutoSize = false; + this.lblTimer.Font = new System.Drawing.Font("Segoe UI Light", 72F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.lblTimer.ForeColor = System.Drawing.Color.LightGreen; + this.lblTimer.Location = new System.Drawing.Point(100, 200); + this.lblTimer.Name = "lblTimer"; + // 初始大小,会在CenterControls中动态调整 + this.lblTimer.Size = new System.Drawing.Size(400, 180); + this.lblTimer.TabIndex = 1; + this.lblTimer.Text = "01:00"; + this.lblTimer.TextAlign = ContentAlignment.MiddleCenter; + + // + // btnSkip + // + this.btnSkip = new System.Windows.Forms.Button(); + this.btnSkip.BackColor = System.Drawing.Color.FromArgb(63, 63, 70); + this.btnSkip.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnSkip.FlatAppearance.BorderSize = 0; + this.btnSkip.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(80, 80, 90); + this.btnSkip.Font = new System.Drawing.Font("Microsoft YaHei UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.btnSkip.ForeColor = System.Drawing.Color.White; + this.btnSkip.Location = new System.Drawing.Point(100, 400); + this.btnSkip.Name = "btnSkip"; + this.btnSkip.Size = new System.Drawing.Size(120, 46); + this.btnSkip.TabIndex = 2; + this.btnSkip.Text = "跳过"; + this.btnSkip.UseVisualStyleBackColor = false; + this.btnSkip.Cursor = System.Windows.Forms.Cursors.Hand; + this.btnSkip.Click += new System.EventHandler(this.btnSkip_Click); + + // Add controls + this.Controls.Add(this.btnSkip); + this.Controls.Add(this.lblMessage); + this.Controls.Add(this.lblTimer); + + this.btnSkip.BringToFront(); + + this.Load += new System.EventHandler(this.RestForm_Load); + this.Resize += new System.EventHandler(this.RestForm_Resize); + + this.ResumeLayout(false); + this.PerformLayout(); + } + + private void RestForm_Load(object sender, EventArgs e) + { + CenterControls(); + } + + private void RestForm_Resize(object sender, EventArgs e) + { + CenterControls(); + } + + private void CenterControls() + { + // 简单的居中计算 + int centerX = this.Width / 2; + int centerY = this.Height / 2; + + // Equal spacing logic + int spacing = 80; + + // 使用足够大的固定大小,确保文本完整显示 + // 使用屏幕宽度的85%作为最大宽度,确保在不同分辨率下都能完整显示 + int maxWidth = (int)(this.Width * 0.85); + + // 消息文本:使用足够大的尺寸 + lblMessage.Size = new Size(maxWidth, 120); + + // 时间文本:使用足够大的尺寸(72pt字体需要较大空间) + lblTimer.Size = new Size(maxWidth, 200); + + // Timer Center = centerY + lblTimer.Location = new Point(centerX - lblTimer.Width / 2, centerY - lblTimer.Height / 2); + + // Message above Timer + lblMessage.Location = new Point(centerX - lblMessage.Width / 2, lblTimer.Top - spacing - lblMessage.Height); + + // Button below Timer + btnSkip.Location = new Point(centerX - btnSkip.Width / 2, lblTimer.Bottom + spacing); + } + + + public void UpdateTime(TimeSpan remaining) + { + if (lblTimer.InvokeRequired) + { + lblTimer.Invoke(new Action(UpdateTime), remaining); + } + else + { + // 只更新文本,不重新居中,避免闪烁 + // 由于lblTimer已设置固定大小,文本居中显示,不会因为文本变化而改变位置 + lblTimer.Text = $"{remaining.Minutes:D2}:{remaining.Seconds:D2}"; + } + } + + private void btnSkip_Click(object sender, EventArgs e) + { + SkipRequested?.Invoke(this, EventArgs.Empty); + this.Close(); + } + } +} diff --git a/TimerApp.csproj b/TimerApp.csproj new file mode 100644 index 0000000..c27cd77 --- /dev/null +++ b/TimerApp.csproj @@ -0,0 +1,11 @@ + + + + WinExe + net9.0-windows + enable + true + enable + + + \ No newline at end of file