commit 5fba4ff110531e308f1f0cdfed5df7e13b115868 Author: Solin Date: Sat Jan 17 12:48:01 2026 +0800 add: 久坐提醒项目文件 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