diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8aa670 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.lnk +extracted_icon.png +ACE_LowPriority.ico diff --git a/ACE_LowPriority.exe b/ACE_LowPriority.exe deleted file mode 100644 index 3781381..0000000 Binary files a/ACE_LowPriority.exe and /dev/null differ diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..be70849 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,134 @@ +# 项目代理说明 + +## 项目概述 + +本项目是一个面向 Windows 的单文件桌面工具,目标是把 ACE(AntiCheatExpert)相关进程固定调整为低优先级,并将 CPU 亲和性限制到最后一个逻辑处理器上,以尽量降低其对前台游戏或其他程序的性能干扰。 + +当前源码集中在 `开发文件夹/Program.cs`,构建后输出到 `exe文件夹/ACE_LowPriority.exe`。项目不是标准的 Visual Studio 解决方案,而是通过 PowerShell 脚本直接调用 C# CodeDOM 编译成 `winexe`。 + +## 当前实际功能 + +基于 `开发文件夹` 的现有实现,项目目前已经具备以下能力: + +- 启动时检查管理员权限,不足时使用 `runas` 重新拉起自身。 +- 使用全局互斥锁保证单实例运行;重复启动时会向已运行实例发送激活消息。 +- 图形界面支持 4 种状态:等待、处理中、成功、失败。 +- 点击按钮后会把以下固定目标进程设置为 `Idle` 优先级,并把 CPU 亲和性设置为最后一个逻辑处理器: + - `C:\Program Files\AntiCheatExpert\SGuard\x64\SGuard64.exe` + - `C:\Program Files\AntiCheatExpert\SGuard\x64\SGuardSvc64.exe` +- 支持系统托盘常驻、托盘右键菜单、双击托盘图标恢复窗口。 +- 支持“开机自启”和“自动执行”两个设置项。 +- 自动执行开启后,每 5 秒轮询一次目标进程;当检测到新的目标进程实例时自动再次执行设置。 +- 关闭主窗口时弹出对话框,让用户选择“隐藏到托盘”或“直接退出”。 + +## 具体实现方式 + +### 1. 启动与权限处理 + +- 程序入口在 `Program.Main`。 +- 启动后先执行 `SingleInstanceManager.TryAcquire()`,通过命名互斥锁 `Global\ACE_LowPriority_SingleInstance` 限制单实例。 +- 如果已有实例存在,则通过 `RegisterWindowMessage + PostMessage(HWND_BROADCAST)` 发送激活消息,让已有窗口恢复显示。 +- 权限部分由 `PrivilegeHelper` 负责: + - `IsAdministrator()` 判断当前是否为管理员。 + - `RelaunchElevated()` 使用 `ProcessStartInfo.Verb = "runas"` 以管理员权限重启当前 exe。 + +### 2. 进程识别与状态修改 + +- 目标进程列表写死在 `MainForm.Targets` 常量数组中。 +- 处理逻辑在 `BeginOperation()` -> `PerformOperation()`: + - 读取 `Environment.ProcessorCount`。 + - 计算最后一个逻辑处理器的编号 `logicalProcessorCount - 1`。 + - 生成亲和性掩码 `1L << lastProcessorIndex`。 + - 遍历两个目标进程路径,调用 `GetTargetProcesses()` 找到匹配进程。 +- 进程匹配策略: + - 先按进程名 `Process.GetProcessesByName()` 查找。 + - 再读取 `process.MainModule.FileName` 尝试确认完整路径。 + - 优先使用“路径完全匹配”的结果。 + - 如果路径无法读取,则保留为兜底候选。 +- 真正的修改发生在 `SetTargetProcessState()`: + - `process.PriorityClass = ProcessPriorityClass.Idle` + - `process.ProcessorAffinity = (IntPtr)affinityMask` + +### 3. 自动执行机制 + +- 设置由 `AppSettings` 管理,保存位置为: + - `%AppData%\ACE_LowPriority\settings.ini` +- 当前只保存两个布尔值: + - `StartWithWindows` + - `AutoExecute` +- 自动执行由 `_autoExecuteTimer` 驱动,轮询周期固定为 5000 ms。 +- `CheckAutoExecuteTargets()` 会: + - 收集当前所有目标进程 PID。 + - 与 `_processedProcessIds` 比较。 + - 只有当检测到“新的目标实例”时,才再次触发 `BeginOperation()`。 +- 如果目标进程全部退出,已处理 PID 集合会被清空,等待下一轮重新触发。 + +### 4. 开机自启实现 + +- 开机自启由 `StartupRegistrationManager` 实现。 +- 写入注册表位置: + - `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` +- 注册值名固定为: + - `ACE_LowPriority` +- 写入命令行为: + - `"当前程序路径" --start-in-tray` + +### 5. 界面与交互实现 + +- 整个界面基于 WinForms 自绘,没有使用设计器文件。 +- 主窗口 `MainForm` 采用无边框窗体,拖动依赖 `ReleaseCapture + SendMessage` 模拟标题栏拖拽。 +- UI 大部分控件是代码绘制: + - `RoundedPanel`:圆角卡片/容器 + - `RoundedButton`:圆角按钮 + - `WindowCaptionButton`:标题栏最小化、设置、关闭按钮 + - `StatusIconControl`:等待/成功/失败状态图标 +- 状态切换通过 `SetState()` 集中管理,统一更新标题、说明文案、按钮样式和错误详情区。 +- 处理过程放在线程池 `ThreadPool.QueueUserWorkItem` 中执行,完成后通过 `BeginInvoke()` 回到 UI 线程刷新界面。 +- 托盘部分使用 `NotifyIcon + ContextMenuStrip` 实现,支持: + - 显示主界面 + - 立即执行一次 + - 退出程序 + +### 6. 构建与发布方式 + +- 构建脚本是 `开发文件夹/build-exe.ps1`。 +- 它会先把 `红温猫.jpg` 转成临时 `.ico`,再把图标嵌入最终 exe。 +- 编译器使用 `Microsoft.CSharp.CSharpCodeProvider`,不是 `dotnet build`。 +- 编译参数包含: + - `/target:winexe` + - `/platform:x64` + - `/optimize+` + - `/win32icon` +- 入口源码只有 `Program.cs`,引用的程序集主要是: + - `System.dll` + - `System.Core.dll` + - `System.Drawing.dll` + - `System.Windows.Forms.dll` +- `开发文件夹/run-set-ace-process.cmd` 用于在仓库内递归查找 `ACE_LowPriority.exe` 并启动它。 + +## 目录职责 + +- `开发文件夹/Program.cs`:主程序全部逻辑与 UI 实现。 +- `开发文件夹/build-exe.ps1`:打包编译脚本。 +- `开发文件夹/set-ace-process.ps1`:更早期的 PowerShell 版实现,逻辑与现版 exe 基本一致。 +- `开发文件夹/UI`:HTML + 截图形式的界面设计参考稿,不参与程序运行。 +- `开发文件夹/TODO.md`:迭代计划,部分内容已经被源码实现,部分仍是后续规划。 +- `exe文件夹`:构建产物目录。 + +## 当前代码状态说明 + +以下内容是按现有源码观察到的实际情况,后续维护时应注意: + +- `--start-in-tray` 参数会被解析,但当前 `MainForm` 中没有真正根据该参数在启动时自动隐藏到托盘。 +- `--auto-start` 参数也会被解析,并会在窗口显示后立即执行一次,但当前开机自启写入的命令行并没有带这个参数。 +- `ShowTrayBalloon()` 当前是空实现,代码中虽然调用了“托盘气泡提示”,实际不会显示。 +- 目标进程路径、优先级策略、CPU 选择策略目前全部写死,尚未做成可配置规则。 + +## 适合后续继续维护的方向 + +如果继续开发,最直接的演进点是: + +- 把目标进程、优先级、CPU 亲和性做成配置项,而不是硬编码。 +- 补齐 `--start-in-tray` 的真实行为,使开机自启真正静默常驻。 +- 实现日志记录,便于排查“路径不匹配”“权限不足”“进程瞬时退出”等失败场景。 +- 为自动执行增加更稳妥的重试和退避机制,避免目标进程刚启动时设置失败。 diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 75bfba8..0000000 --- a/Program.cs +++ /dev/null @@ -1,961 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.IO; -using System.Security.Principal; -using System.Threading; -using System.Windows.Forms; - -internal enum AppState -{ - Waiting, - Running, - Success, - Failure -} - -internal sealed class OperationResult -{ - public OperationResult(bool success, int lastProcessorIndex, int affectedCount, string errorMessage) - { - Success = success; - LastProcessorIndex = lastProcessorIndex; - AffectedCount = affectedCount; - ErrorMessage = errorMessage; - } - - public bool Success { get; private set; } - - public int LastProcessorIndex { get; private set; } - - public int AffectedCount { get; private set; } - - public string ErrorMessage { get; private set; } -} - -internal static class Program -{ - [STAThread] - private static void Main(string[] args) - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - if (!PrivilegeHelper.IsAdministrator()) - { - try - { - PrivilegeHelper.RelaunchElevated(args); - } - catch (Win32Exception ex) - { - if (ex.NativeErrorCode == 1223) - { - MessageBox.Show("你已取消管理员授权,程序无法继续运行。", "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - else - { - MessageBox.Show(string.Format("无法获取管理员权限:{0}", ex.Message), "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - catch (Exception ex) - { - MessageBox.Show(string.Format("程序启动失败:{0}", ex.Message), "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - - return; - } - - Application.Run(new MainForm(args)); - } -} - -internal static class PrivilegeHelper -{ - internal static bool IsAdministrator() - { - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - try - { - WindowsPrincipal principal = new WindowsPrincipal(identity); - return principal.IsInRole(WindowsBuiltInRole.Administrator); - } - finally - { - identity.Dispose(); - } - } - - internal static void RelaunchElevated(string[] args) - { - string executablePath = Process.GetCurrentProcess().MainModule.FileName; - ProcessStartInfo startInfo = new ProcessStartInfo(); - startInfo.FileName = executablePath; - startInfo.Arguments = BuildArgumentString(args); - startInfo.UseShellExecute = true; - startInfo.Verb = "runas"; - startInfo.WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory; - Process.Start(startInfo); - } - - private static string BuildArgumentString(string[] args) - { - List parts = new List(); - for (int index = 0; index < args.Length; index++) - { - parts.Add(QuoteArgument(args[index])); - } - - return string.Join(" ", parts.ToArray()); - } - - private static string QuoteArgument(string argument) - { - if (string.IsNullOrEmpty(argument)) - { - return "\"\""; - } - - bool requiresQuotes = false; - for (int index = 0; index < argument.Length; index++) - { - char character = argument[index]; - if (char.IsWhiteSpace(character) || character == '"') - { - requiresQuotes = true; - break; - } - } - - if (!requiresQuotes) - { - return argument; - } - - return "\"" + argument.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; - } -} - -internal sealed class MainForm : Form -{ - private static readonly string[] Targets = - { - @"C:\Program Files\AntiCheatExpert\SGuard\x64\SGuard64.exe", - @"C:\Program Files\AntiCheatExpert\SGuard\x64\SGuardSvc64.exe" - }; - - private readonly RoundedPanel _cardPanel; - private readonly RoundedPanel _detailPanel; - private readonly StatusIconControl _statusIcon; - private readonly Label _titleLabel; - private readonly Label _descriptionLabel; - private readonly Label _detailTitleLabel; - private readonly TextBox _detailBodyTextBox; - private readonly RoundedButton _actionButton; - private readonly Label _footerHintLabel; - private readonly System.Windows.Forms.Timer _loadingTimer; - - private readonly bool _autoStart; - private AppState _currentState; - private int _loadingFrame; - private bool _busy; - - public MainForm(string[] args) - { - _autoStart = HasArgument(args, "--auto-start"); - - Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point); - Text = "ACE 低优先级工具"; - StartPosition = FormStartPosition.CenterScreen; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; - ShowIcon = false; - ClientSize = new Size(430, 540); - DoubleBuffered = true; - BackColor = Color.FromArgb(248, 250, 252); - KeyPreview = true; - - _cardPanel = new RoundedPanel(); - _cardPanel.BackColor = Color.White; - _cardPanel.BorderColor = Color.FromArgb(232, 236, 240); - _cardPanel.CornerRadius = 28; - - _statusIcon = new StatusIconControl(); - - _titleLabel = new Label(); - _titleLabel.AutoSize = false; - _titleLabel.TextAlign = ContentAlignment.MiddleCenter; - _titleLabel.Font = new Font("Microsoft YaHei UI", 20F, FontStyle.Bold, GraphicsUnit.Point); - _titleLabel.ForeColor = Color.FromArgb(30, 41, 59); - _titleLabel.BackColor = Color.Transparent; - - _descriptionLabel = new Label(); - _descriptionLabel.AutoSize = false; - _descriptionLabel.TextAlign = ContentAlignment.TopCenter; - _descriptionLabel.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Regular, GraphicsUnit.Point); - _descriptionLabel.ForeColor = Color.FromArgb(100, 116, 139); - _descriptionLabel.BackColor = Color.Transparent; - - _detailPanel = new RoundedPanel(); - _detailPanel.BackColor = Color.FromArgb(254, 242, 242); - _detailPanel.BorderColor = Color.FromArgb(254, 226, 226); - _detailPanel.CornerRadius = 18; - _detailPanel.Visible = false; - - _detailTitleLabel = new Label(); - _detailTitleLabel.AutoSize = false; - _detailTitleLabel.Text = "错误详情:"; - _detailTitleLabel.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold, GraphicsUnit.Point); - _detailTitleLabel.ForeColor = Color.FromArgb(220, 38, 38); - _detailTitleLabel.BackColor = Color.Transparent; - - _detailBodyTextBox = new TextBox(); - _detailBodyTextBox.Multiline = true; - _detailBodyTextBox.ReadOnly = true; - _detailBodyTextBox.BorderStyle = BorderStyle.None; - _detailBodyTextBox.ScrollBars = ScrollBars.Vertical; - _detailBodyTextBox.WordWrap = true; - _detailBodyTextBox.TabStop = false; - _detailBodyTextBox.Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Regular, GraphicsUnit.Point); - _detailBodyTextBox.ForeColor = Color.FromArgb(71, 85, 105); - _detailBodyTextBox.BackColor = Color.FromArgb(254, 242, 242); - - _actionButton = new RoundedButton(); - _actionButton.Font = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold, GraphicsUnit.Point); - _actionButton.Click += ActionButton_Click; - _actionButton.TabStop = true; - - _footerHintLabel = new Label(); - _footerHintLabel.AutoSize = false; - _footerHintLabel.TextAlign = ContentAlignment.TopCenter; - _footerHintLabel.Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Regular, GraphicsUnit.Point); - _footerHintLabel.ForeColor = Color.FromArgb(148, 163, 184); - _footerHintLabel.BackColor = Color.Transparent; - - _cardPanel.Controls.Add(_statusIcon); - _cardPanel.Controls.Add(_titleLabel); - _cardPanel.Controls.Add(_descriptionLabel); - _detailPanel.Controls.Add(_detailTitleLabel); - _detailPanel.Controls.Add(_detailBodyTextBox); - _cardPanel.Controls.Add(_detailPanel); - - Controls.Add(_cardPanel); - Controls.Add(_actionButton); - Controls.Add(_footerHintLabel); - - _loadingTimer = new System.Windows.Forms.Timer(); - _loadingTimer.Interval = 350; - _loadingTimer.Tick += LoadingTimer_Tick; - - Resize += MainForm_Resize; - Shown += MainForm_Shown; - - SetState(AppState.Waiting, null, null); - LayoutControls(); - } - - protected override void OnPaint(PaintEventArgs e) - { - e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - using (LinearGradientBrush backgroundBrush = new LinearGradientBrush(ClientRectangle, Color.FromArgb(248, 250, 252), Color.FromArgb(226, 232, 240), 135F)) - { - e.Graphics.FillRectangle(backgroundBrush, ClientRectangle); - } - - using (SolidBrush accentBrushA = new SolidBrush(Color.FromArgb(28, 59, 130, 246))) - { - e.Graphics.FillEllipse(accentBrushA, -40, 40, 220, 220); - } - - using (SolidBrush accentBrushB = new SolidBrush(Color.FromArgb(20, 236, 91, 19))) - { - e.Graphics.FillEllipse(accentBrushB, ClientSize.Width - 220, ClientSize.Height - 250, 260, 260); - } - - DrawCardShadow(e.Graphics, _cardPanel.Bounds, 28); - base.OnPaint(e); - } - - private void MainForm_Shown(object sender, EventArgs e) - { - if (_autoStart) - { - BeginOperation(); - } - } - - private void MainForm_Resize(object sender, EventArgs e) - { - LayoutControls(); - Invalidate(); - } - - private void LoadingTimer_Tick(object sender, EventArgs e) - { - if (_currentState != AppState.Running) - { - return; - } - - _loadingFrame = (_loadingFrame + 1) % 4; - if (_loadingFrame == 0) - { - _actionButton.Text = "处理中"; - } - else if (_loadingFrame == 1) - { - _actionButton.Text = "处理中."; - } - else if (_loadingFrame == 2) - { - _actionButton.Text = "处理中.."; - } - else - { - _actionButton.Text = "处理中..."; - } - } - - private void ActionButton_Click(object sender, EventArgs e) - { - if (_currentState == AppState.Success) - { - Close(); - return; - } - - if (_currentState == AppState.Waiting || _currentState == AppState.Failure) - { - BeginOperation(); - } - } - - private void BeginOperation() - { - if (_busy) - { - return; - } - - if (!PrivilegeHelper.IsAdministrator()) - { - ShowFailure("当前程序未以管理员权限运行,请重新启动程序。", "权限不足,无法修改目标进程。程序启动时会自动请求管理员权限。"); - return; - } - - _busy = true; - SetState(AppState.Running, "正在设置目标进程的优先级和 CPU 亲和性,请稍候...", null); - ThreadPool.QueueUserWorkItem(PerformOperation); - } - - private void PerformOperation(object state) - { - OperationResult result; - - try - { - int logicalProcessorCount = Environment.ProcessorCount; - if (logicalProcessorCount < 1) - { - throw new InvalidOperationException("未检测到可用的逻辑处理器。"); - } - - if (logicalProcessorCount > 64) - { - throw new InvalidOperationException(string.Format("当前检测到 {0} 个逻辑处理器,程序目前仅支持最多 64 个。", logicalProcessorCount)); - } - - int lastProcessorIndex = logicalProcessorCount - 1; - long affinityMask = 1L << lastProcessorIndex; - int affectedCount = 0; - - foreach (string targetPath in Targets) - { - IList processes = GetTargetProcesses(targetPath); - for (int index = 0; index < processes.Count; index++) - { - SetTargetProcessState(processes[index], affinityMask); - affectedCount++; - } - } - - result = new OperationResult(true, lastProcessorIndex, affectedCount, null); - } - catch (Exception ex) - { - result = new OperationResult(false, -1, 0, ex.Message); - } - - try - { - if (!IsDisposed && IsHandleCreated) - { - BeginInvoke((MethodInvoker)delegate - { - _busy = false; - if (result.Success) - { - ShowSuccess(result); - } - else - { - ShowFailure("处理过程中出现问题,请查看下方错误详情后重试。", result.ErrorMessage); - } - }); - } - } - catch (ObjectDisposedException) - { - } - catch (InvalidOperationException) - { - } - } - - private void ShowSuccess(OperationResult result) - { - string description = string.Format("已成功设置 {0} 个目标进程,仅使用 CPU {1}。", result.AffectedCount, result.LastProcessorIndex); - SetState(AppState.Success, description, null); - } - - private void ShowFailure(string description, string errorMessage) - { - SetState(AppState.Failure, description, errorMessage); - } - - - - - private void SetState(AppState state, string description, string detailMessage) - { - _currentState = state; - _detailPanel.Visible = false; - _descriptionLabel.Visible = true; - _detailBodyTextBox.Text = string.Empty; - _loadingFrame = 0; - _loadingTimer.Stop(); - - if (state == AppState.Waiting) - { - _statusIcon.VisualState = AppState.Waiting; - _titleLabel.Text = "等待启动"; - _descriptionLabel.Text = "点击下方按钮开始任务"; - _actionButton.Enabled = true; - _actionButton.Text = "启动"; - _actionButton.NormalColor = Color.FromArgb(59, 130, 246); - _actionButton.HoverColor = Color.FromArgb(37, 99, 235); - _actionButton.DisabledColor = Color.FromArgb(147, 197, 253); - _footerHintLabel.Text = "点击按钮开始运行"; - AcceptButton = _actionButton; - } - else if (state == AppState.Running) - { - _statusIcon.VisualState = AppState.Running; - _titleLabel.Text = "正在处理中"; - _descriptionLabel.Text = description; - _actionButton.Enabled = false; - _actionButton.Text = "处理中"; - _actionButton.NormalColor = Color.FromArgb(59, 130, 246); - _actionButton.HoverColor = Color.FromArgb(59, 130, 246); - _actionButton.DisabledColor = Color.FromArgb(147, 197, 253); - _footerHintLabel.Text = "请稍候,程序正在处理目标进程"; - AcceptButton = null; - _loadingTimer.Start(); - } - else if (state == AppState.Success) - { - _statusIcon.VisualState = AppState.Success; - _titleLabel.Text = "操作成功"; - _descriptionLabel.Text = description; - _actionButton.Enabled = true; - _actionButton.Text = "退出"; - _actionButton.NormalColor = Color.FromArgb(100, 116, 139); - _actionButton.HoverColor = Color.FromArgb(71, 85, 105); - _actionButton.DisabledColor = Color.FromArgb(148, 163, 184); - _footerHintLabel.Text = "按 Enter 键或点击退出即可关闭程序"; - AcceptButton = _actionButton; - } - else - { - _statusIcon.VisualState = AppState.Failure; - _titleLabel.Text = "操作失败"; - _descriptionLabel.Text = description; - _detailPanel.Visible = true; - _detailBodyTextBox.Text = detailMessage ?? string.Empty; - _actionButton.Enabled = true; - _actionButton.Text = "重试"; - _actionButton.NormalColor = Color.FromArgb(236, 91, 19); - _actionButton.HoverColor = Color.FromArgb(214, 77, 11); - _actionButton.DisabledColor = Color.FromArgb(253, 186, 116); - _footerHintLabel.Text = "点击按钮重新尝试"; - AcceptButton = _actionButton; - } - - LayoutControls(); - Invalidate(); - } - - private void LayoutControls() - { - int cardWidth = Math.Min(ClientSize.Width - 44, 372); - int cardHeight = _currentState == AppState.Failure ? 354 : 252; - int cardLeft = (ClientSize.Width - cardWidth) / 2; - int cardTop = 56; - - _cardPanel.Bounds = new Rectangle(cardLeft, cardTop, cardWidth, cardHeight); - - int iconSize = 80; - _statusIcon.Bounds = new Rectangle((_cardPanel.Width - iconSize) / 2, 24, iconSize, iconSize); - - _titleLabel.Bounds = new Rectangle(20, _statusIcon.Bottom + 14, _cardPanel.Width - 40, 38); - - if (_currentState == AppState.Failure) - { - _descriptionLabel.Bounds = new Rectangle(28, _titleLabel.Bottom + 6, _cardPanel.Width - 56, 40); - _detailPanel.Bounds = new Rectangle(20, _descriptionLabel.Bottom + 10, _cardPanel.Width - 40, 116); - _detailTitleLabel.Bounds = new Rectangle(14, 12, _detailPanel.Width - 28, 22); - _detailBodyTextBox.Bounds = new Rectangle(14, 38, _detailPanel.Width - 28, 64); - } - else - { - _descriptionLabel.Bounds = new Rectangle(28, _titleLabel.Bottom + 8, _cardPanel.Width - 56, 48); - } - - int buttonWidth = Math.Max(150, cardWidth / 2); - int buttonLeft = (ClientSize.Width - buttonWidth) / 2; - int buttonBottomMargin = 24; - int buttonTop = ClientSize.Height - buttonBottomMargin - 52; - int footerTop = buttonTop - 34; - - _actionButton.Bounds = new Rectangle(buttonLeft, buttonTop, buttonWidth, 52); - _footerHintLabel.Bounds = new Rectangle(cardLeft + 8, footerTop, cardWidth - 16, 24); - } - - private static bool HasArgument(string[] args, string expected) - { - for (int index = 0; index < args.Length; index++) - { - if (string.Equals(args[index], expected, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - - - - private static IList GetTargetProcesses(string executablePath) - { - string processName = Path.GetFileNameWithoutExtension(executablePath); - Process[] processes = Process.GetProcessesByName(processName); - if (processes.Length == 0) - { - throw new InvalidOperationException(string.Format("目标进程未运行:{0}", executablePath)); - } - - List exactPathMatches = new List(); - List unknownPathMatches = new List(); - - for (int index = 0; index < processes.Length; index++) - { - Process process = processes[index]; - string resolvedPath = GetReadableProcessPath(process); - if (string.IsNullOrEmpty(resolvedPath)) - { - unknownPathMatches.Add(process); - continue; - } - - if (string.Equals(resolvedPath, executablePath, StringComparison.OrdinalIgnoreCase)) - { - exactPathMatches.Add(process); - } - } - - if (exactPathMatches.Count > 0) - { - return exactPathMatches; - } - - if (unknownPathMatches.Count > 0) - { - return unknownPathMatches; - } - - throw new InvalidOperationException(string.Format("已找到同名进程 {0},但路径与目标不匹配。", processName)); - } - - private static string GetReadableProcessPath(Process process) - { - try - { - return process.MainModule.FileName; - } - catch (Win32Exception) - { - return null; - } - catch (InvalidOperationException) - { - return null; - } - } - - private static void SetTargetProcessState(Process process, long affinityMask) - { - process.PriorityClass = ProcessPriorityClass.Idle; - process.ProcessorAffinity = (IntPtr)affinityMask; - process.Refresh(); - } - - private static void DrawCardShadow(Graphics graphics, Rectangle bounds, int cornerRadius) - { - if (bounds.Width <= 0 || bounds.Height <= 0) - { - return; - } - - for (int layer = 0; layer < 5; layer++) - { - Rectangle shadowBounds = new Rectangle(bounds.X - layer, bounds.Y + 4 + layer, bounds.Width + layer * 2, bounds.Height + layer * 2); - int alpha = 24 - layer * 4; - if (alpha < 0) - { - alpha = 0; - } - - using (GraphicsPath path = RoundedPanel.CreateRoundedPath(shadowBounds, cornerRadius + layer)) - using (SolidBrush brush = new SolidBrush(Color.FromArgb(alpha, 15, 23, 42))) - { - graphics.FillPath(brush, path); - } - } - } -} - -internal sealed class RoundedPanel : Panel -{ - public RoundedPanel() - { - DoubleBuffered = true; - BorderColor = Color.Transparent; - CornerRadius = 24; - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - } - - public Color BorderColor { get; set; } - - public int CornerRadius { get; set; } - - protected override void OnPaint(PaintEventArgs e) - { - e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - Rectangle rectangle = new Rectangle(0, 0, Width - 1, Height - 1); - - using (GraphicsPath path = CreateRoundedPath(rectangle, CornerRadius)) - using (SolidBrush brush = new SolidBrush(BackColor)) - using (Pen pen = new Pen(BorderColor)) - { - e.Graphics.FillPath(brush, path); - if (BorderColor.A > 0) - { - e.Graphics.DrawPath(pen, path); - } - } - - base.OnPaint(e); - } - - protected override void OnResize(EventArgs eventargs) - { - base.OnResize(eventargs); - if (Width > 0 && Height > 0) - { - using (GraphicsPath path = CreateRoundedPath(new Rectangle(0, 0, Width, Height), CornerRadius)) - { - Region = new Region(path); - } - } - } - - public static GraphicsPath CreateRoundedPath(Rectangle bounds, int radius) - { - int diameter = radius * 2; - GraphicsPath path = new GraphicsPath(); - - if (diameter > bounds.Width) - { - diameter = bounds.Width; - } - - if (diameter > bounds.Height) - { - diameter = bounds.Height; - } - - Rectangle arc = new Rectangle(bounds.Location, new Size(diameter, diameter)); - path.AddArc(arc, 180, 90); - arc.X = bounds.Right - diameter; - path.AddArc(arc, 270, 90); - arc.Y = bounds.Bottom - diameter; - path.AddArc(arc, 0, 90); - arc.X = bounds.Left; - path.AddArc(arc, 90, 90); - path.CloseFigure(); - return path; - } -} - -internal sealed class RoundedButton : Button -{ - private bool _hovered; - private bool _pressed; - - public RoundedButton() - { - FlatStyle = FlatStyle.Flat; - FlatAppearance.BorderSize = 0; - ForeColor = Color.White; - Cursor = Cursors.Hand; - NormalColor = Color.FromArgb(59, 130, 246); - HoverColor = Color.FromArgb(37, 99, 235); - DisabledColor = Color.FromArgb(147, 197, 253); - TabStop = true; - UseVisualStyleBackColor = false; - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - } - - public Color NormalColor { get; set; } - - public Color HoverColor { get; set; } - - public Color DisabledColor { get; set; } - - protected override void OnPaint(PaintEventArgs pevent) - { - pevent.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - Rectangle rectangle = new Rectangle(0, 0, Width - 1, Height - 1); - Color fillColor = ResolveFillColor(); - - using (GraphicsPath path = RoundedPanel.CreateRoundedPath(rectangle, 20)) - using (SolidBrush brush = new SolidBrush(fillColor)) - { - pevent.Graphics.FillPath(brush, path); - } - - TextRenderer.DrawText(pevent.Graphics, Text, Font, rectangle, ForeColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine); - } - - protected override void OnMouseEnter(EventArgs eventargs) - { - _hovered = true; - Invalidate(); - base.OnMouseEnter(eventargs); - } - - protected override void OnMouseLeave(EventArgs eventargs) - { - _hovered = false; - _pressed = false; - Invalidate(); - base.OnMouseLeave(eventargs); - } - - protected override void OnMouseDown(MouseEventArgs mevent) - { - _pressed = true; - Invalidate(); - base.OnMouseDown(mevent); - } - - protected override void OnMouseUp(MouseEventArgs mevent) - { - _pressed = false; - Invalidate(); - base.OnMouseUp(mevent); - } - - protected override void OnEnabledChanged(EventArgs e) - { - Invalidate(); - base.OnEnabledChanged(e); - } - - protected override void OnResize(EventArgs e) - { - base.OnResize(e); - if (Width > 0 && Height > 0) - { - using (GraphicsPath path = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, Width, Height), 20)) - { - Region = new Region(path); - } - } - } - - private Color ResolveFillColor() - { - if (!Enabled) - { - return DisabledColor; - } - - if (_pressed) - { - return ControlPaint.Dark(HoverColor, 0.08F); - } - - if (_hovered) - { - return HoverColor; - } - - return NormalColor; - } -} - -internal sealed class StatusIconControl : Control -{ - private AppState _visualState; - - public StatusIconControl() - { - Size = new Size(92, 92); - VisualState = AppState.Waiting; - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - } - - public AppState VisualState - { - get { return _visualState; } - set - { - if (_visualState == value) - { - return; - } - - _visualState = value; - Invalidate(); - Update(); - } - } - - protected override void OnPaint(PaintEventArgs e) - { - e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - Rectangle outer = new Rectangle(0, 0, Width - 1, Height - 1); - - Color backgroundColor; - Color strokeColor; - - if (VisualState == AppState.Success) - { - backgroundColor = Color.FromArgb(236, 253, 245); - strokeColor = Color.FromArgb(16, 185, 129); - } - else if (VisualState == AppState.Failure) - { - backgroundColor = Color.FromArgb(254, 242, 242); - strokeColor = Color.FromArgb(239, 68, 68); - } - else if (VisualState == AppState.Running) - { - backgroundColor = Color.FromArgb(239, 246, 255); - strokeColor = Color.FromArgb(59, 130, 246); - } - else - { - backgroundColor = Color.FromArgb(241, 245, 249); - strokeColor = Color.FromArgb(148, 163, 184); - } - - using (SolidBrush brush = new SolidBrush(backgroundColor)) - { - e.Graphics.FillEllipse(brush, outer); - } - - if (VisualState == AppState.Success) - { - DrawSuccessIcon(e.Graphics, strokeColor); - } - else if (VisualState == AppState.Failure) - { - DrawFailureIcon(e.Graphics, strokeColor); - } - else - { - DrawClockIcon(e.Graphics, strokeColor); - } - } - - private void DrawClockIcon(Graphics graphics, Color strokeColor) - { - float centerX = Width / 2F; - float centerY = Height / 2F; - float circleSize = Math.Min(Width, Height) * 0.5F; - float circleLeft = centerX - circleSize / 2F; - float circleTop = centerY - circleSize / 2F; - float lineWidth = Math.Max(4F, Math.Min(Width, Height) * 0.055F); - - using (Pen pen = new Pen(strokeColor, lineWidth)) - { - pen.StartCap = LineCap.Round; - pen.EndCap = LineCap.Round; - graphics.DrawEllipse(pen, circleLeft, circleTop, circleSize, circleSize); - graphics.DrawLine(pen, centerX, centerY, centerX, centerY - circleSize * 0.3F); - graphics.DrawLine(pen, centerX, centerY, centerX + circleSize * 0.22F, centerY + circleSize * 0.12F); - } - } - - private void DrawSuccessIcon(Graphics graphics, Color strokeColor) - { - float lineWidth = Math.Max(6F, Math.Min(Width, Height) * 0.075F); - PointF pointA = new PointF(Width * 0.30F, Height * 0.54F); - PointF pointB = new PointF(Width * 0.44F, Height * 0.68F); - PointF pointC = new PointF(Width * 0.71F, Height * 0.36F); - - using (Pen pen = new Pen(strokeColor, lineWidth)) - { - pen.StartCap = LineCap.Round; - pen.EndCap = LineCap.Round; - pen.LineJoin = LineJoin.Round; - graphics.DrawLines(pen, new PointF[] { pointA, pointB, pointC }); - } - } - - private void DrawFailureIcon(Graphics graphics, Color strokeColor) - { - float centerX = Width / 2F; - float topY = Height * 0.3F; - float bottomY = Height * 0.62F; - float dotCenterY = Height * 0.78F; - float dotSize = Math.Max(6F, Math.Min(Width, Height) * 0.08F); - float lineWidth = Math.Max(5F, Math.Min(Width, Height) * 0.065F); - - using (Pen pen = new Pen(strokeColor, lineWidth)) - using (SolidBrush brush = new SolidBrush(strokeColor)) - { - pen.StartCap = LineCap.Round; - pen.EndCap = LineCap.Round; - graphics.DrawLine(pen, centerX, topY, centerX, bottomY); - graphics.FillEllipse(brush, centerX - dotSize / 2F, dotCenterY - dotSize / 2F, dotSize, dotSize); - } - } -} - - - - - - - - - diff --git a/README.md b/README.md deleted file mode 100644 index 6342b97..0000000 --- a/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# ACE 低优先级工具 - -这是一个 Windows 图形界面程序,用来调整 AntiCheatExpert 相关进程的运行属性。 - -目标进程: - -- `C:\Program Files\AntiCheatExpert\SGuard\x64\SGuard64.exe` -- `C:\Program Files\AntiCheatExpert\SGuard\x64\SGuardSvc64.exe` - -程序功能: - -- 将目标进程优先级设置为 **低**(Windows 对应 `Idle`) -- 将 CPU 亲和性限制为 **最后一个逻辑处理器** -- 使用图形界面展示 **等待启动 / 正在处理 / 操作成功 / 操作失败** 四种状态 -- 启动时自动请求管理员权限 -- 操作成功后可直接按 `Enter` 键退出程序 - -## 主要文件 - -- `ACE_LowPriority.exe`:主程序 -- `Program.cs`:图形界面和处理逻辑源码 -- `build-exe.ps1`:重新编译 `exe` 的脚本 -- `run-set-ace-process.cmd`:兼容启动入口,会调用 `ACE_LowPriority.exe` -- `UI`:界面设计参考稿 -- `任务描述.txt`:原始任务说明 - -## 使用方法 - -### 直接运行 - -双击运行: - -- `ACE_LowPriority.exe` - -程序启动时会先请求管理员权限,通过后再显示等待界面。 - -### 操作流程 - -1. 点击 `启动` -2. 如果当前不是管理员权限,程序会弹出 UAC 提权 -3. 提权成功后自动开始处理目标进程 -4. 处理完成后显示成功或失败界面 -5. 成功时可以点击 `退出`,也可以直接按 `Enter` 键关闭程序 - -## 当前界面设计 - -程序界面已经按 `UI` 目录中的设计稿调整: - -- 等待启动:居中卡片 + 启动按钮 -- 正在处理:处理中状态和按钮反馈 -- 操作成功:绿色成功图标 + 退出按钮 -- 操作失败:红色失败图标 + 错误详情 + 重试按钮 - -## 重新编译 - -如果修改了 `Program.cs`,可以用下面的命令重新生成程序: - -```powershell -powershell.exe -ExecutionPolicy Bypass -File .\build-exe.ps1 -``` - -默认输出文件: - -- `ACE_LowPriority.exe` - -## 重要说明 - -- 程序**不会启动目标程序**,只会处理**已经运行中的目标进程** -- 如果目标进程没有运行,会显示失败界面并提示错误原因 -- 如果系统逻辑处理器数量大于 64,会直接显示失败信息 -- 如果存在多个同名进程,会优先尝试按完整路径匹配;如果路径无法读取,则退回按进程名处理 - diff --git a/build-exe.ps1 b/build-exe.ps1 deleted file mode 100644 index fbabb86..0000000 --- a/build-exe.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -param( - [string]$OutputPath = (Join-Path $PSScriptRoot 'ACE_LowPriority.exe') -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$sourcePath = Join-Path $PSScriptRoot 'Program.cs' -if (-not (Test-Path -LiteralPath $sourcePath)) { - throw "Source file not found: $sourcePath" -} - -$provider = New-Object Microsoft.CSharp.CSharpCodeProvider -$compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters -$compilerParameters.GenerateExecutable = $true -$compilerParameters.GenerateInMemory = $false -$compilerParameters.IncludeDebugInformation = $false -$compilerParameters.TreatWarningsAsErrors = $false -$compilerParameters.WarningLevel = 4 -$compilerParameters.OutputAssembly = $OutputPath -$compilerParameters.CompilerOptions = '/target:winexe /platform:x64 /optimize+' - -[void]$compilerParameters.ReferencedAssemblies.Add('System.dll') -[void]$compilerParameters.ReferencedAssemblies.Add('System.Core.dll') -[void]$compilerParameters.ReferencedAssemblies.Add('System.Drawing.dll') -[void]$compilerParameters.ReferencedAssemblies.Add('System.Windows.Forms.dll') - -$results = $provider.CompileAssemblyFromFile($compilerParameters, $sourcePath) -$errors = @($results.Errors | Where-Object { -not $_.IsWarning }) -if ($errors.Count -gt 0) { - $messages = foreach ($compilerIssue in $errors) { - '{0}({1},{2}): {3}' -f $sourcePath, $compilerIssue.Line, $compilerIssue.Column, $compilerIssue.ErrorText - } - - throw ($messages -join [Environment]::NewLine) -} - -Write-Host "Build succeeded: $OutputPath" -ForegroundColor Green diff --git "a/exe\346\226\207\344\273\266\345\244\271/ACE_LowPriority.exe" "b/exe\346\226\207\344\273\266\345\244\271/ACE_LowPriority.exe" new file mode 100644 index 0000000..dc549fb Binary files /dev/null and "b/exe\346\226\207\344\273\266\345\244\271/ACE_LowPriority.exe" differ diff --git "a/exe\346\226\207\344\273\266\345\244\271/\345\272\224\347\224\250\350\257\264\346\230\216.txt" "b/exe\346\226\207\344\273\266\345\244\271/\345\272\224\347\224\250\350\257\264\346\230\216.txt" new file mode 100644 index 0000000..1c8bf4d --- /dev/null +++ "b/exe\346\226\207\344\273\266\345\244\271/\345\272\224\347\224\250\350\257\264\346\230\216.txt" @@ -0,0 +1,69 @@ +ACE 低优先级工具 - 应用说明 + +一、软件功能 + +本工具用于在 Windows 下自动调整 ACE(AntiCheatExpert)相关进程的运行状态,降低其对系统性能的影响。 + +当前会处理以下两个进程: + +1. C:\Program Files\AntiCheatExpert\SGuard\x64\SGuard64.exe +2. C:\Program Files\AntiCheatExpert\SGuard\x64\SGuardSvc64.exe + +处理内容: + +1. 将进程优先级设置为低优先级(Idle) +2. 将进程 CPU 亲和性限制到最后一个逻辑处理器 + + +二、使用方法 + +1. 双击运行 ACE_LowPriority.exe +2. 程序启动时会自动请求管理员权限,请点击允许 +3. 进入主界面后,点击“启动”即可执行一次处理 +4. 处理成功后,程序会显示成功界面,并发送一条 Windows 提示消息 +5. 如果处理失败,程序会显示失败原因,并发送一条 Windows 提示消息 + + +三、程序特点 + +1. 支持单实例运行,重复打开时会唤醒已运行窗口 +2. 支持最小化到系统托盘后台运行 +3. 支持托盘菜单“显示主界面 / 立即执行一次 / 退出程序” +4. 支持开机自启 +5. 支持自动执行 + + +四、设置说明 + +点击主界面右上角“设置”按钮后,可配置以下内容: + +1. 开机自启 +开启后,程序会在 Windows 登录后自动启动。 + +2. 自动执行 +开启后,程序会每 5 秒检测一次目标进程。 +当检测到目标进程启动后,会自动执行优先级和 CPU 亲和性设置。 + + +五、通知说明 + +程序当前只会发送两种 Windows 提示消息: + +1. 执行成功 +2. 执行失败 + +除以上两种情况外,不会弹出其他系统提示消息。 + + +六、注意事项 + +1. 本工具需要管理员权限,否则无法修改目标进程状态 +2. 如果目标进程尚未启动,执行时会提示失败 +3. 本工具当前只处理固定的 ACE 目标进程,不支持自定义进程 +4. 如果关闭主窗口,可选择隐藏到托盘继续后台运行 + + +七、文件说明 + +1. ACE_LowPriority.exe:主程序 +2. 应用说明.txt:当前说明文档 diff --git a/run-set-ace-process.cmd b/run-set-ace-process.cmd deleted file mode 100644 index 1614183..0000000 --- a/run-set-ace-process.cmd +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -setlocal - -set "SCRIPT_DIR=%~dp0" -set "APP_EXE=%SCRIPT_DIR%ACE_LowPriority.exe" - -if not exist "%APP_EXE%" ( - echo ERROR: "%APP_EXE%" not found. - echo Please run build-exe.ps1 first. - pause - exit /b 1 -) - -"%APP_EXE%" -exit /b %ERRORLEVEL% diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/Program.cs" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/Program.cs" new file mode 100644 index 0000000..efbc828 --- /dev/null +++ "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/Program.cs" @@ -0,0 +1,2034 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Threading; +using System.Windows.Forms; +using System.Reflection; +using Microsoft.Win32; + +[assembly: AssemblyTitle("ACE_LowPriority")] +[assembly: AssemblyDescription("ACE 低优先级工具")] +[assembly: AssemblyCompany("GBall5599")] +[assembly: AssemblyProduct("ACE_LowPriority")] +[assembly: AssemblyVersion("1.2.0.0")] +[assembly: AssemblyFileVersion("1.2.0.0")] +[assembly: AssemblyInformationalVersion("1.2")] + +internal enum AppState +{ + Waiting, + Running, + Success, + Failure +} + +internal sealed class OperationResult +{ + public OperationResult(bool success, int lastProcessorIndex, int affectedCount, string errorMessage) + { + Success = success; + LastProcessorIndex = lastProcessorIndex; + AffectedCount = affectedCount; + ErrorMessage = errorMessage; + } + + public bool Success { get; private set; } + + public int LastProcessorIndex { get; private set; } + + public int AffectedCount { get; private set; } + + public string ErrorMessage { get; private set; } +} + +internal enum CloseChoice +{ + Cancel, + HideToTray, + Exit +} + +internal sealed class AppSettings +{ + private const string SettingsDirectoryName = "ACE_LowPriority"; + private const string SettingsFileName = "settings.ini"; + + public bool StartWithWindows { get; set; } + + public bool AutoExecute { get; set; } + + public static int AutoExecutePollIntervalMilliseconds + { + get { return 5000; } + } + + public static AppSettings Load() + { + AppSettings settings = new AppSettings(); + string settingsPath = GetSettingsFilePath(); + if (!File.Exists(settingsPath)) + { + return settings; + } + + string[] lines = File.ReadAllLines(settingsPath); + for (int index = 0; index < lines.Length; index++) + { + string line = lines[index]; + int separatorIndex = line.IndexOf('='); + if (separatorIndex <= 0) + { + continue; + } + + string key = line.Substring(0, separatorIndex).Trim(); + string value = line.Substring(separatorIndex + 1).Trim(); + bool parsedValue; + if (!bool.TryParse(value, out parsedValue)) + { + continue; + } + + if (string.Equals(key, "StartWithWindows", StringComparison.OrdinalIgnoreCase)) + { + settings.StartWithWindows = parsedValue; + } + else if (string.Equals(key, "AutoExecute", StringComparison.OrdinalIgnoreCase)) + { + settings.AutoExecute = parsedValue; + } + } + + return settings; + } + + public void Save() + { + string settingsPath = GetSettingsFilePath(); + string directoryPath = Path.GetDirectoryName(settingsPath); + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + File.WriteAllLines(settingsPath, new string[] + { + string.Format("StartWithWindows={0}", StartWithWindows), + string.Format("AutoExecute={0}", AutoExecute) + }); + } + + public AppSettings Clone() + { + return new AppSettings + { + StartWithWindows = StartWithWindows, + AutoExecute = AutoExecute + }; + } + + private static string GetSettingsFilePath() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + return Path.Combine(appDataPath, SettingsDirectoryName, SettingsFileName); + } +} + +internal static class StartupRegistrationManager +{ + private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string ValueName = "ACE_LowPriority"; + + internal static void Apply(bool enabled) + { + using (RegistryKey runKey = Registry.CurrentUser.CreateSubKey(RunKeyPath)) + { + if (runKey == null) + { + throw new InvalidOperationException("无法打开开机启动注册表项。"); + } + + if (enabled) + { + runKey.SetValue(ValueName, BuildCommandLine(), RegistryValueKind.String); + } + else if (runKey.GetValue(ValueName) != null) + { + runKey.DeleteValue(ValueName, false); + } + } + } + + private static string BuildCommandLine() + { + return string.Format("\"{0}\" --start-in-tray", Application.ExecutablePath); + } +} +internal static class Program +{ + [STAThread] + private static void Main(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + if (!SingleInstanceManager.TryAcquire()) + { + SingleInstanceManager.SignalExistingInstance(); + return; + } + + try + { + if (!PrivilegeHelper.IsAdministrator()) + { + try + { + PrivilegeHelper.RelaunchElevated(args); + } + catch (Win32Exception ex) + { + if (ex.NativeErrorCode == 1223) + { + MessageBox.Show("你已取消管理员授权,程序无法继续运行。", "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + else + { + MessageBox.Show(string.Format("无法获取管理员权限:{0}", ex.Message), "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + catch (Exception ex) + { + MessageBox.Show(string.Format("程序启动失败:{0}", ex.Message), "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + return; + } + + try + { + Application.Run(new MainForm(args)); + } + catch (Exception ex) + { + MessageBox.Show(string.Format("程序启动失败:{0}", ex.Message), "ACE 低优先级工具", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + finally + { + SingleInstanceManager.Release(); + } + } +} + +internal static class SingleInstanceManager +{ + private const string MutexName = @"Global\ACE_LowPriority_SingleInstance"; + private const string ActivationMessageName = "ACE_LowPriority_ActivateExistingInstance"; + + private static Mutex _instanceMutex; + private static readonly int _activationMessageId = RegisterWindowMessage(ActivationMessageName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int RegisterWindowMessage(string lpString); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + internal static int ActivationMessageId + { + get { return _activationMessageId; } + } + + internal static bool TryAcquire() + { + bool createdNew; + _instanceMutex = new Mutex(true, MutexName, out createdNew); + return createdNew; + } + + internal static void SignalExistingInstance() + { + if (_activationMessageId != 0) + { + PostMessage((IntPtr)0xFFFF, _activationMessageId, IntPtr.Zero, IntPtr.Zero); + } + } + + internal static void Release() + { + if (_instanceMutex == null) + { + return; + } + + try + { + _instanceMutex.ReleaseMutex(); + } + catch (ApplicationException) + { + } + + _instanceMutex.Dispose(); + _instanceMutex = null; + } +} + +internal static class PrivilegeHelper +{ + internal static bool IsAdministrator() + { + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + try + { + WindowsPrincipal principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + finally + { + identity.Dispose(); + } + } + + internal static void RelaunchElevated(string[] args) + { + string executablePath = Process.GetCurrentProcess().MainModule.FileName; + ProcessStartInfo startInfo = new ProcessStartInfo(); + startInfo.FileName = executablePath; + startInfo.Arguments = BuildArgumentString(args); + startInfo.UseShellExecute = true; + startInfo.Verb = "runas"; + startInfo.WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory; + Process.Start(startInfo); + } + + private static string BuildArgumentString(string[] args) + { + List parts = new List(); + for (int index = 0; index < args.Length; index++) + { + parts.Add(QuoteArgument(args[index])); + } + + return string.Join(" ", parts.ToArray()); + } + + private static string QuoteArgument(string argument) + { + if (string.IsNullOrEmpty(argument)) + { + return "\"\""; + } + + bool requiresQuotes = false; + for (int index = 0; index < argument.Length; index++) + { + char character = argument[index]; + if (char.IsWhiteSpace(character) || character == '"') + { + requiresQuotes = true; + break; + } + } + + if (!requiresQuotes) + { + return argument; + } + + return "\"" + argument.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } +} + +internal sealed class MainForm : Form +{ + private static readonly string[] Targets = + { + @"C:\Program Files\AntiCheatExpert\SGuard\x64\SGuard64.exe", + @"C:\Program Files\AntiCheatExpert\SGuard\x64\SGuardSvc64.exe" + }; + + private readonly RoundedPanel _cardPanel; + private readonly RoundedPanel _detailPanel; + private readonly StatusIconControl _statusIcon; + private readonly Label _titleLabel; + private readonly Label _descriptionLabel; + private readonly Label _detailTitleLabel; + private readonly TextBox _detailBodyTextBox; + private readonly RoundedButton _actionButton; + private readonly Label _footerHintLabel; + private readonly System.Windows.Forms.Timer _loadingTimer; + private readonly System.Windows.Forms.Timer _autoExecuteTimer; + private readonly Label _windowTitleLabel; + private readonly WindowCaptionButton _minimizeButton; + private readonly WindowCaptionButton _settingsButton; + private readonly WindowCaptionButton _closeButton; + private readonly NotifyIcon _notifyIcon; + private readonly ContextMenuStrip _trayMenu; + private readonly ToolStripMenuItem _showWindowMenuItem; + private readonly ToolStripMenuItem _runNowMenuItem; + private readonly ToolStripMenuItem _exitMenuItem; + + private readonly bool _autoStart; + private readonly bool _startInTray; + private readonly HashSet _processedProcessIds; + private AppSettings _settings; + private AppState _currentState; + private int _loadingFrame; + private bool _busy; + private bool _allowExit; + + [DllImport("user32.dll")] + private static extern bool ReleaseCapture(); + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr handle, int message, IntPtr wParam, IntPtr lParam); + + public MainForm(string[] args) + { + _autoStart = HasArgument(args, "--auto-start"); + _startInTray = HasArgument(args, "--start-in-tray"); + _processedProcessIds = new HashSet(); + _settings = AppSettings.Load(); + + Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point); + Text = "ACE 低优先级工具"; + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.None; + MaximizeBox = false; + MinimizeBox = false; + ControlBox = false; + ShowIcon = false; + try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { } + ClientSize = new Size(430, 540); + DoubleBuffered = true; + BackColor = Color.FromArgb(248, 250, 252); + KeyPreview = true; + + _cardPanel = new RoundedPanel(); + _cardPanel.BackColor = Color.White; + _cardPanel.BorderColor = Color.FromArgb(203, 213, 225); + _cardPanel.CornerRadius = 28; + + _statusIcon = new StatusIconControl(); + + _titleLabel = new Label(); + _titleLabel.AutoSize = false; + _titleLabel.TextAlign = ContentAlignment.MiddleCenter; + _titleLabel.Font = new Font("Microsoft YaHei UI", 20F, FontStyle.Bold, GraphicsUnit.Point); + _titleLabel.ForeColor = Color.FromArgb(30, 41, 59); + _titleLabel.BackColor = Color.Transparent; + + _descriptionLabel = new Label(); + _descriptionLabel.AutoSize = false; + _descriptionLabel.TextAlign = ContentAlignment.TopCenter; + _descriptionLabel.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Regular, GraphicsUnit.Point); + _descriptionLabel.ForeColor = Color.FromArgb(100, 116, 139); + _descriptionLabel.BackColor = Color.Transparent; + + _detailPanel = new RoundedPanel(); + _detailPanel.BackColor = Color.FromArgb(254, 242, 242); + _detailPanel.BorderColor = Color.FromArgb(254, 226, 226); + _detailPanel.CornerRadius = 18; + _detailPanel.Visible = false; + + _detailTitleLabel = new Label(); + _detailTitleLabel.AutoSize = false; + _detailTitleLabel.Text = "错误详情:"; + _detailTitleLabel.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold, GraphicsUnit.Point); + _detailTitleLabel.ForeColor = Color.FromArgb(220, 38, 38); + _detailTitleLabel.BackColor = Color.Transparent; + + _detailBodyTextBox = new TextBox(); + _detailBodyTextBox.Multiline = true; + _detailBodyTextBox.ReadOnly = true; + _detailBodyTextBox.BorderStyle = BorderStyle.None; + _detailBodyTextBox.ScrollBars = ScrollBars.Vertical; + _detailBodyTextBox.WordWrap = true; + _detailBodyTextBox.TabStop = false; + _detailBodyTextBox.Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Regular, GraphicsUnit.Point); + _detailBodyTextBox.ForeColor = Color.FromArgb(71, 85, 105); + _detailBodyTextBox.BackColor = Color.FromArgb(254, 242, 242); + + _actionButton = new RoundedButton(); + _actionButton.Font = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold, GraphicsUnit.Point); + _actionButton.Click += ActionButton_Click; + _actionButton.TabStop = true; + + _windowTitleLabel = new Label(); + _windowTitleLabel.AutoSize = false; + _windowTitleLabel.Text = "ACE 低优先级工具"; + _windowTitleLabel.TextAlign = ContentAlignment.MiddleLeft; + _windowTitleLabel.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Bold, GraphicsUnit.Point); + _windowTitleLabel.ForeColor = Color.FromArgb(51, 65, 85); + _windowTitleLabel.BackColor = Color.Transparent; + _windowTitleLabel.MouseDown += DragSurface_MouseDown; + + _minimizeButton = new WindowCaptionButton(); + _minimizeButton.ButtonKind = CaptionButtonKind.Minimize; + _minimizeButton.Click += MinimizeButton_Click; + + _settingsButton = new WindowCaptionButton(); + _settingsButton.ButtonKind = CaptionButtonKind.Settings; + _settingsButton.Click += SettingsButton_Click; + + _closeButton = new WindowCaptionButton(); + _closeButton.ButtonKind = CaptionButtonKind.Close; + _closeButton.Click += CloseButton_Click; + + _footerHintLabel = new Label(); + _footerHintLabel.AutoSize = false; + _footerHintLabel.TextAlign = ContentAlignment.TopCenter; + _footerHintLabel.Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Regular, GraphicsUnit.Point); + _footerHintLabel.ForeColor = Color.FromArgb(148, 163, 184); + _footerHintLabel.BackColor = Color.Transparent; + + _cardPanel.Controls.Add(_statusIcon); + _cardPanel.Controls.Add(_titleLabel); + _cardPanel.Controls.Add(_descriptionLabel); + _detailPanel.Controls.Add(_detailTitleLabel); + _detailPanel.Controls.Add(_detailBodyTextBox); + _cardPanel.Controls.Add(_detailPanel); + + Controls.Add(_cardPanel); + Controls.Add(_actionButton); + Controls.Add(_footerHintLabel); + Controls.Add(_windowTitleLabel); + Controls.Add(_minimizeButton); + Controls.Add(_settingsButton); + Controls.Add(_closeButton); + + _loadingTimer = new System.Windows.Forms.Timer(); + _loadingTimer.Interval = 350; + _loadingTimer.Tick += LoadingTimer_Tick; + + _autoExecuteTimer = new System.Windows.Forms.Timer(); + _autoExecuteTimer.Interval = AppSettings.AutoExecutePollIntervalMilliseconds; + _autoExecuteTimer.Tick += AutoExecuteTimer_Tick; + + _trayMenu = new ContextMenuStrip(); + _showWindowMenuItem = new ToolStripMenuItem("显示主界面"); + _runNowMenuItem = new ToolStripMenuItem("立即执行一次"); + _exitMenuItem = new ToolStripMenuItem("退出程序"); + + _showWindowMenuItem.Click += ShowWindowMenuItem_Click; + _runNowMenuItem.Click += RunNowMenuItem_Click; + _exitMenuItem.Click += ExitMenuItem_Click; + + _trayMenu.Items.AddRange(new ToolStripItem[] + { + _showWindowMenuItem, + _runNowMenuItem, + new ToolStripSeparator(), + _exitMenuItem + }); + + _notifyIcon = new NotifyIcon(); + _notifyIcon.Text = "ACE 低优先级工具"; + _notifyIcon.Visible = true; + _notifyIcon.ContextMenuStrip = _trayMenu; + _notifyIcon.MouseDoubleClick += NotifyIcon_MouseDoubleClick; + try + { + _notifyIcon.Icon = Icon ?? SystemIcons.Application; + } + catch + { + _notifyIcon.Icon = SystemIcons.Application; + } + + Resize += MainForm_Resize; + Shown += MainForm_Shown; + FormClosing += MainForm_FormClosing; + MouseDown += DragSurface_MouseDown; + + SetState(AppState.Waiting, null, null); + ApplySettings(_settings, false); + LayoutControls(); + UpdateFormRegion(); + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (LinearGradientBrush backgroundBrush = new LinearGradientBrush(ClientRectangle, Color.FromArgb(248, 250, 252), Color.FromArgb(226, 232, 240), 135F)) + { + e.Graphics.FillRectangle(backgroundBrush, ClientRectangle); + } + + using (SolidBrush accentBrushA = new SolidBrush(Color.FromArgb(28, 59, 130, 246))) + { + e.Graphics.FillEllipse(accentBrushA, -40, 40, 220, 220); + } + + using (SolidBrush accentBrushB = new SolidBrush(Color.FromArgb(20, 236, 91, 19))) + { + e.Graphics.FillEllipse(accentBrushB, ClientSize.Width - 220, ClientSize.Height - 250, 260, 260); + } + + DrawCardShadow(e.Graphics, _cardPanel.Bounds, 28); + using (GraphicsPath borderPath = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, ClientSize.Width - 1, ClientSize.Height - 1), 28)) + using (Pen borderPen = new Pen(Color.FromArgb(148, 163, 184))) + { + borderPen.Width = 2F; + e.Graphics.DrawPath(borderPen, borderPath); + } + base.OnPaint(e); + } + + protected override void WndProc(ref Message message) + { + if (message.Msg == SingleInstanceManager.ActivationMessageId) + { + RestoreFromTray(); + Activate(); + return; + } + + base.WndProc(ref message); + } + + private void MainForm_Shown(object sender, EventArgs e) + { + if (_autoStart) + { + BeginOperation(); + } + } + + private void MainForm_Resize(object sender, EventArgs e) + { + if (WindowState == FormWindowState.Minimized) + { + HideToTray(); + return; + } + + UpdateFormRegion(); + LayoutControls(); + Invalidate(); + } + + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) + { + if (_allowExit || e.CloseReason == CloseReason.WindowsShutDown || e.CloseReason == CloseReason.TaskManagerClosing) + { + return; + } + + CloseChoice choice = ClosePromptDialog.ShowChoice(this); + if (choice == CloseChoice.Exit) + { + _allowExit = true; + return; + } + + e.Cancel = true; + if (choice == CloseChoice.HideToTray) + { + HideToTray(); + } + } + + private void LoadingTimer_Tick(object sender, EventArgs e) + { + if (_currentState != AppState.Running) + { + return; + } + + _loadingFrame = (_loadingFrame + 1) % 4; + if (_loadingFrame == 0) + { + _actionButton.Text = "处理中"; + } + else if (_loadingFrame == 1) + { + _actionButton.Text = "处理中."; + } + else if (_loadingFrame == 2) + { + _actionButton.Text = "处理中.."; + } + else + { + _actionButton.Text = "处理中..."; + } + } + + private void ActionButton_Click(object sender, EventArgs e) + { + if (_currentState == AppState.Success) + { + HideToTray(); + return; + } + + if (_currentState == AppState.Waiting || _currentState == AppState.Failure) + { + BeginOperation(); + } + } + + private void BeginOperation() + { + if (_busy) + { + return; + } + + if (!PrivilegeHelper.IsAdministrator()) + { + ShowFailure("当前程序未以管理员权限运行,请重新启动程序。", "权限不足,无法修改目标进程。程序启动时会自动请求管理员权限。"); + return; + } + + _busy = true; + SetState(AppState.Running, "正在设置目标进程的优先级和 CPU 亲和性,请稍候...", null); + ThreadPool.QueueUserWorkItem(PerformOperation); + } + + private void PerformOperation(object state) + { + OperationResult result; + + try + { + int logicalProcessorCount = Environment.ProcessorCount; + if (logicalProcessorCount < 1) + { + throw new InvalidOperationException("未检测到可用的逻辑处理器。"); + } + + if (logicalProcessorCount > 64) + { + throw new InvalidOperationException(string.Format("当前检测到 {0} 个逻辑处理器,程序目前仅支持最多 64 个。", logicalProcessorCount)); + } + + int lastProcessorIndex = logicalProcessorCount - 1; + long affinityMask = 1L << lastProcessorIndex; + int affectedCount = 0; + + foreach (string targetPath in Targets) + { + IList processes = GetTargetProcesses(targetPath); + for (int index = 0; index < processes.Count; index++) + { + SetTargetProcessState(processes[index], affinityMask); + affectedCount++; + } + } + + result = new OperationResult(true, lastProcessorIndex, affectedCount, null); + } + catch (Exception ex) + { + result = new OperationResult(false, -1, 0, ex.Message); + } + + try + { + if (!IsDisposed && IsHandleCreated) + { + BeginInvoke((MethodInvoker)delegate + { + _busy = false; + if (result.Success) + { + ShowSuccess(result); + ShowTrayBalloon("操作成功", string.Format("已成功设置 {0} 个目标进程,仅使用 CPU {1}。", result.AffectedCount, result.LastProcessorIndex), ToolTipIcon.Info); + } + else + { + ShowFailure("处理过程中出现问题,请查看下方错误详情后重试。", result.ErrorMessage); + RestoreFromTray(); + Activate(); + ShowTrayBalloon("操作失败", "处理失败,已打开主界面显示错误详情。", ToolTipIcon.Error); + } + }); + } + } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException) + { + } + } + + private void ShowSuccess(OperationResult result) + { + string description = string.Format("已成功设置 {0} 个目标进程,仅使用 CPU {1}。", result.AffectedCount, result.LastProcessorIndex); + SetState(AppState.Success, description, null); + } + + private void ShowFailure(string description, string errorMessage) + { + SetState(AppState.Failure, description, errorMessage); + } + + private void ApplySettings(AppSettings settings, bool persist) + { + _settings = settings.Clone(); + + if (persist) + { + _settings.Save(); + StartupRegistrationManager.Apply(_settings.StartWithWindows); + } + + ConfigureAutoExecute(); + } + + private void ConfigureAutoExecute() + { + if (_settings.AutoExecute) + { + _autoExecuteTimer.Start(); + } + else + { + _autoExecuteTimer.Stop(); + _processedProcessIds.Clear(); + } + } + + private void AutoExecuteTimer_Tick(object sender, EventArgs e) + { + CheckAutoExecuteTargets(); + } + + private void CheckAutoExecuteTargets() + { + if (!_settings.AutoExecute || _busy) + { + return; + } + + HashSet runningProcessIds; + if (!TryGetAllTargetProcessIds(out runningProcessIds)) + { + _processedProcessIds.Clear(); + return; + } + + _processedProcessIds.IntersectWith(runningProcessIds); + if (_processedProcessIds.SetEquals(runningProcessIds)) + { + return; + } + + BeginOperation(); + } + + private void SettingsButton_Click(object sender, EventArgs e) + { + try + { + using (SettingsDialog dialog = new SettingsDialog(_settings)) + { + if (dialog.ShowDialog(this) == DialogResult.OK) + { + ApplySettings(dialog.Settings, true); + if (_settings.AutoExecute) + { + CheckAutoExecuteTargets(); + } + } + } + } + catch (Exception ex) + { + ShowFailure("设置保存失败,请检查配置后重试。", ex.Message); + RestoreFromTray(); + Activate(); + } + } + + private void SyncProcessedProcessIds() + { + HashSet runningProcessIds; + if (!TryGetAllTargetProcessIds(out runningProcessIds)) + { + _processedProcessIds.Clear(); + return; + } + + _processedProcessIds.Clear(); + foreach (int processId in runningProcessIds) + { + _processedProcessIds.Add(processId); + } + } + + + private void NotifyIcon_MouseDoubleClick(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + RestoreFromTray(); + } + } + + private void ShowWindowMenuItem_Click(object sender, EventArgs e) + { + RestoreFromTray(); + } + + private void RunNowMenuItem_Click(object sender, EventArgs e) + { + BeginOperation(); + } + + private void ExitMenuItem_Click(object sender, EventArgs e) + { + _allowExit = true; + Close(); + } + + private void MinimizeButton_Click(object sender, EventArgs e) + { + WindowState = FormWindowState.Minimized; + } + + private void CloseButton_Click(object sender, EventArgs e) + { + Close(); + } + + private void DragSurface_MouseDown(object sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) + { + return; + } + + ReleaseCapture(); + SendMessage(Handle, 0xA1, (IntPtr)2, IntPtr.Zero); + } + + private void HideToTray() + { + if (IsDisposed) + { + return; + } + + Hide(); + ShowInTaskbar = false; + } + + private void RestoreFromTray() + { + if (IsDisposed) + { + return; + } + + ShowInTaskbar = true; + Show(); + WindowState = FormWindowState.Normal; + BringToFront(); + Activate(); + } + + private void ShowTrayBalloon(string title, string text, ToolTipIcon icon) + { + if (_notifyIcon == null || IsDisposed) + { + return; + } + + try + { + _notifyIcon.BalloonTipTitle = title; + _notifyIcon.BalloonTipText = text; + _notifyIcon.BalloonTipIcon = icon; + _notifyIcon.ShowBalloonTip(3000); + } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException) + { + } + } + + private void UpdateFormRegion() + { + if (ClientSize.Width <= 0 || ClientSize.Height <= 0) + { + return; + } + + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, ClientSize.Width, ClientSize.Height), 28)) + { + Region = new Region(path); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_notifyIcon != null) + { + _notifyIcon.Visible = false; + _notifyIcon.Dispose(); + } + + if (_trayMenu != null) + { + _trayMenu.Dispose(); + } + } + + base.Dispose(disposing); + } + + + + + private void SetState(AppState state, string description, string detailMessage) + { + _currentState = state; + _detailPanel.Visible = false; + _descriptionLabel.Visible = true; + _detailBodyTextBox.Text = string.Empty; + _loadingFrame = 0; + _loadingTimer.Stop(); + + if (state == AppState.Waiting) + { + _statusIcon.VisualState = AppState.Waiting; + _titleLabel.Text = "等待启动"; + _descriptionLabel.Text = "点击下方按钮开始任务"; + _actionButton.Enabled = true; + _actionButton.Text = "启动"; + _actionButton.NormalColor = Color.FromArgb(59, 130, 246); + _actionButton.HoverColor = Color.FromArgb(37, 99, 235); + _actionButton.DisabledColor = Color.FromArgb(147, 197, 253); + _footerHintLabel.Text = "点击按钮开始运行"; + AcceptButton = _actionButton; + } + else if (state == AppState.Running) + { + _statusIcon.VisualState = AppState.Running; + _titleLabel.Text = "正在处理中"; + _descriptionLabel.Text = description; + _actionButton.Enabled = false; + + _actionButton.Text = "处理中"; + _actionButton.NormalColor = Color.FromArgb(59, 130, 246); + _actionButton.HoverColor = Color.FromArgb(59, 130, 246); + _actionButton.DisabledColor = Color.FromArgb(147, 197, 253); + _footerHintLabel.Text = "请稍候,程序正在处理目标进程"; + AcceptButton = null; + _loadingTimer.Start(); + } + else if (state == AppState.Success) + { + _statusIcon.VisualState = AppState.Success; + _titleLabel.Text = "操作成功"; + _descriptionLabel.Text = description; + _actionButton.Enabled = true; + _actionButton.Text = "隐藏到托盘"; + _actionButton.NormalColor = Color.FromArgb(100, 116, 139); + _actionButton.HoverColor = Color.FromArgb(71, 85, 105); + _actionButton.DisabledColor = Color.FromArgb(148, 163, 184); + _footerHintLabel.Text = "按 Enter 键或点击按钮即可隐藏到托盘"; + AcceptButton = _actionButton; + } + else + { + _statusIcon.VisualState = AppState.Failure; + _titleLabel.Text = "操作失败"; + _descriptionLabel.Text = description; + _detailPanel.Visible = true; + _detailBodyTextBox.Text = detailMessage ?? string.Empty; + _actionButton.Enabled = true; + _actionButton.Text = "重试"; + _actionButton.NormalColor = Color.FromArgb(236, 91, 19); + _actionButton.HoverColor = Color.FromArgb(214, 77, 11); + _actionButton.DisabledColor = Color.FromArgb(253, 186, 116); + _footerHintLabel.Text = "点击按钮重新尝试"; + AcceptButton = _actionButton; + } + + LayoutControls(); + Invalidate(); + } + + private void LayoutControls() + { + int cardWidth = Math.Min(ClientSize.Width - 44, 372); + int cardHeight = _currentState == AppState.Failure ? 354 : 252; + int cardLeft = (ClientSize.Width - cardWidth) / 2; + int cardTop = 68; + + _windowTitleLabel.Bounds = new Rectangle(18, 12, 180, 28); + _closeButton.Bounds = new Rectangle(ClientSize.Width - 18 - 26, 12, 26, 26); + _settingsButton.Bounds = new Rectangle(_closeButton.Left - 6 - 26, 12, 26, 26); + _minimizeButton.Bounds = new Rectangle(_settingsButton.Left - 6 - 26, 12, 26, 26); + + _cardPanel.Bounds = new Rectangle(cardLeft, cardTop, cardWidth, cardHeight); + + int iconSize = 80; + _statusIcon.Bounds = new Rectangle((_cardPanel.Width - iconSize) / 2, 24, iconSize, iconSize); + + _titleLabel.Bounds = new Rectangle(20, _statusIcon.Bottom + 14, _cardPanel.Width - 40, 38); + + if (_currentState == AppState.Failure) + { + _descriptionLabel.Bounds = new Rectangle(28, _titleLabel.Bottom + 6, _cardPanel.Width - 56, 40); + _detailPanel.Bounds = new Rectangle(20, _descriptionLabel.Bottom + 10, _cardPanel.Width - 40, 116); + _detailTitleLabel.Bounds = new Rectangle(14, 12, _detailPanel.Width - 28, 22); + _detailBodyTextBox.Bounds = new Rectangle(14, 38, _detailPanel.Width - 28, 64); + } + else + { + _descriptionLabel.Bounds = new Rectangle(28, _titleLabel.Bottom + 8, _cardPanel.Width - 56, 48); + } + + int buttonWidth = Math.Max(150, cardWidth / 2); + int buttonLeft = (ClientSize.Width - buttonWidth) / 2; + int buttonBottomMargin = 24; + int buttonTop = ClientSize.Height - buttonBottomMargin - 52; + int footerTop = buttonTop - 34; + + _actionButton.Bounds = new Rectangle(buttonLeft, buttonTop, buttonWidth, 52); + _footerHintLabel.Bounds = new Rectangle(cardLeft + 8, footerTop, cardWidth - 16, 24); + } + + private static bool HasArgument(string[] args, string expected) + { + for (int index = 0; index < args.Length; index++) + { + if (string.Equals(args[index], expected, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static bool TryGetAllTargetProcessIds(out HashSet processIds) + { + processIds = new HashSet(); + + foreach (string targetPath in Targets) + { + IList processes; + try + { + processes = GetTargetProcesses(targetPath); + } + catch (Exception) + { + processIds.Clear(); + return false; + } + + if (processes == null || processes.Count == 0) + { + processIds.Clear(); + return false; + } + + for (int index = 0; index < processes.Count; index++) + { + processIds.Add(processes[index].Id); + } + } + + return processIds.Count > 0; + } + + private static IList GetTargetProcesses(string executablePath) + { + string processName = Path.GetFileNameWithoutExtension(executablePath); + Process[] processes = Process.GetProcessesByName(processName); + if (processes.Length == 0) + { + throw new InvalidOperationException(string.Format("目标进程未运行:{0}", executablePath)); + } + + List exactPathMatches = new List(); + List unknownPathMatches = new List(); + + for (int index = 0; index < processes.Length; index++) + { + Process process = processes[index]; + string resolvedPath = GetReadableProcessPath(process); + if (string.IsNullOrEmpty(resolvedPath)) + { + unknownPathMatches.Add(process); + continue; + } + + if (string.Equals(resolvedPath, executablePath, StringComparison.OrdinalIgnoreCase)) + { + exactPathMatches.Add(process); + } + } + + if (exactPathMatches.Count > 0) + { + return exactPathMatches; + } + + if (unknownPathMatches.Count > 0) + { + return unknownPathMatches; + } + + throw new InvalidOperationException(string.Format("已找到同名进程 {0},但路径与目标不匹配。", processName)); + } + + private static string GetReadableProcessPath(Process process) + { + try + { + return process.MainModule.FileName; + } + catch (Win32Exception) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + private static void SetTargetProcessState(Process process, long affinityMask) + { + process.PriorityClass = ProcessPriorityClass.Idle; + process.ProcessorAffinity = (IntPtr)affinityMask; + process.Refresh(); + } + + private static void DrawCardShadow(Graphics graphics, Rectangle bounds, int cornerRadius) + { + if (bounds.Width <= 0 || bounds.Height <= 0) + { + return; + } + + for (int layer = 0; layer < 5; layer++) + { + Rectangle shadowBounds = new Rectangle(bounds.X - layer, bounds.Y + 4 + layer, bounds.Width + layer * 2, bounds.Height + layer * 2); + int alpha = 24 - layer * 4; + if (alpha < 0) + { + alpha = 0; + } + + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(shadowBounds, cornerRadius + layer)) + using (SolidBrush brush = new SolidBrush(Color.FromArgb(alpha, 15, 23, 42))) + { + graphics.FillPath(brush, path); + } + } + } +} + +internal sealed class RoundedPanel : Panel +{ + public RoundedPanel() + { + DoubleBuffered = true; + BorderColor = Color.Transparent; + CornerRadius = 24; + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + } + + public Color BorderColor { get; set; } + + public int CornerRadius { get; set; } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Rectangle rectangle = new Rectangle(0, 0, Width - 1, Height - 1); + + using (GraphicsPath path = CreateRoundedPath(rectangle, CornerRadius)) + using (SolidBrush brush = new SolidBrush(BackColor)) + using (Pen pen = new Pen(BorderColor)) + { + e.Graphics.FillPath(brush, path); + if (BorderColor.A > 0) + { + e.Graphics.DrawPath(pen, path); + } + } + + base.OnPaint(e); + } + + protected override void OnResize(EventArgs eventargs) + { + base.OnResize(eventargs); + if (Width > 0 && Height > 0) + { + using (GraphicsPath path = CreateRoundedPath(new Rectangle(0, 0, Width, Height), CornerRadius)) + { + Region = new Region(path); + } + } + } + + public static GraphicsPath CreateRoundedPath(Rectangle bounds, int radius) + { + int diameter = radius * 2; + GraphicsPath path = new GraphicsPath(); + + if (diameter > bounds.Width) + { + diameter = bounds.Width; + } + + if (diameter > bounds.Height) + { + diameter = bounds.Height; + } + + Rectangle arc = new Rectangle(bounds.Location, new Size(diameter, diameter)); + path.AddArc(arc, 180, 90); + arc.X = bounds.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = bounds.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = bounds.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + return path; + } +} + +internal sealed class RoundedButton : Button +{ + private bool _hovered; + private bool _pressed; + + public RoundedButton() + { + FlatStyle = FlatStyle.Flat; + FlatAppearance.BorderSize = 0; + ForeColor = Color.White; + Cursor = Cursors.Hand; + NormalColor = Color.FromArgb(59, 130, 246); + HoverColor = Color.FromArgb(37, 99, 235); + DisabledColor = Color.FromArgb(147, 197, 253); + TabStop = true; + UseVisualStyleBackColor = false; + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + } + + public Color NormalColor { get; set; } + + public Color HoverColor { get; set; } + + public Color DisabledColor { get; set; } + + protected override void OnPaint(PaintEventArgs pevent) + { + pevent.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Rectangle rectangle = new Rectangle(0, 0, Width - 1, Height - 1); + Color fillColor = ResolveFillColor(); + + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(rectangle, 20)) + using (SolidBrush brush = new SolidBrush(fillColor)) + { + pevent.Graphics.FillPath(brush, path); + } + + TextRenderer.DrawText(pevent.Graphics, Text, Font, rectangle, ForeColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine); + } + + protected override void OnMouseEnter(EventArgs eventargs) + { + _hovered = true; + Invalidate(); + base.OnMouseEnter(eventargs); + } + + protected override void OnMouseLeave(EventArgs eventargs) + { + _hovered = false; + _pressed = false; + Invalidate(); + base.OnMouseLeave(eventargs); + } + + protected override void OnMouseDown(MouseEventArgs mevent) + { + _pressed = true; + Invalidate(); + base.OnMouseDown(mevent); + } + + protected override void OnMouseUp(MouseEventArgs mevent) + { + _pressed = false; + Invalidate(); + base.OnMouseUp(mevent); + } + + protected override void OnEnabledChanged(EventArgs e) + { + Invalidate(); + base.OnEnabledChanged(e); + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + if (Width > 0 && Height > 0) + { + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, Width, Height), 20)) + { + Region = new Region(path); + } + } + } + + private Color ResolveFillColor() + { + if (!Enabled) + { + return DisabledColor; + } + + if (_pressed) + { + return ControlPaint.Dark(HoverColor, 0.08F); + } + + if (_hovered) + { + return HoverColor; + } + + return NormalColor; + } +} + +internal enum CaptionButtonKind +{ + Minimize, + Settings, + Close +} + +internal sealed class WindowCaptionButton : Control +{ + private bool _hovered; + private bool _pressed; + private CaptionButtonKind _buttonKind; + + public WindowCaptionButton() + { + Size = new Size(26, 26); + Cursor = Cursors.Hand; + ForeColor = Color.FromArgb(148, 163, 184); + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor, true); + BackColor = Color.Transparent; + } + + public CaptionButtonKind ButtonKind + { + get { return _buttonKind; } + set + { + _buttonKind = value; + Invalidate(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Rectangle rect = new Rectangle(0, 0, Width - 1, Height - 1); + + Color fillColor = Color.Transparent; + if (_pressed) + { + fillColor = ButtonKind == CaptionButtonKind.Close ? Color.FromArgb(255, 254, 226, 226) : Color.FromArgb(255, 219, 234, 254); + } + else if (_hovered) + { + fillColor = ButtonKind == CaptionButtonKind.Close ? Color.FromArgb(255, 254, 242, 242) : Color.FromArgb(255, 239, 246, 255); + } + + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(rect, 10)) + { + if (fillColor.A > 0) + { + using (SolidBrush brush = new SolidBrush(fillColor)) + { + e.Graphics.FillPath(brush, path); + } + } + + } + + using (Pen pen = new Pen(ButtonKind == CaptionButtonKind.Close ? Color.FromArgb(234, 88, 12) : ForeColor, 1.8F)) + { + pen.StartCap = LineCap.Round; + pen.EndCap = LineCap.Round; + + if (ButtonKind == CaptionButtonKind.Minimize) + { + float y = Height * 0.62F; + e.Graphics.DrawLine(pen, Width * 0.32F, y, Width * 0.68F, y); + } + else if (ButtonKind == CaptionButtonKind.Settings) + { + float centerX = Width / 2F; + float centerY = Height / 2F; + float outerRadius = Math.Min(Width, Height) * 0.26F; + float innerRadius = Math.Min(Width, Height) * 0.18F; + float toothRadius = Math.Min(Width, Height) * 0.34F; + PointF[] points = new PointF[16]; + + for (int index = 0; index < points.Length; index++) + { + double angle = (-Math.PI / 2D) + (Math.PI / 8D) * index; + float radius = (index % 2 == 0) ? toothRadius : outerRadius; + points[index] = new PointF( + centerX + (float)(Math.Cos(angle) * radius), + centerY + (float)(Math.Sin(angle) * radius)); + } + + using (GraphicsPath gearPath = new GraphicsPath()) + using (SolidBrush gearBrush = new SolidBrush(ForeColor)) + using (SolidBrush holeBrush = new SolidBrush(fillColor.A > 0 ? fillColor : Color.FromArgb(239, 246, 255))) + { + gearPath.AddPolygon(points); + e.Graphics.FillPath(gearBrush, gearPath); + e.Graphics.FillEllipse(holeBrush, centerX - innerRadius, centerY - innerRadius, innerRadius * 2F, innerRadius * 2F); + } + } + else + { + e.Graphics.DrawLine(pen, Width * 0.34F, Height * 0.34F, Width * 0.66F, Height * 0.66F); + e.Graphics.DrawLine(pen, Width * 0.66F, Height * 0.34F, Width * 0.34F, Height * 0.66F); + } + } + } + + protected override void OnMouseEnter(EventArgs e) + { + _hovered = true; + Invalidate(); + base.OnMouseEnter(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + _hovered = false; + _pressed = false; + Invalidate(); + base.OnMouseLeave(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + _pressed = true; + Invalidate(); + base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + _pressed = false; + Invalidate(); + base.OnMouseUp(e); + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + if (Width > 0 && Height > 0) + { + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, Width, Height), 10)) + { + Region = new Region(path); + } + } + } +} + +internal sealed class SettingsDialog : Form +{ + private readonly Label _titleLabel; + private readonly Label _descriptionLabel; + private readonly CheckBox _startupCheckBox; + private readonly CheckBox _autoExecuteCheckBox; + private readonly Label _autoExecuteNoteLabel; + private readonly RoundedButton _saveButton; + private readonly RoundedButton _cancelButton; + private readonly WindowCaptionButton _closeButton; + + internal SettingsDialog(AppSettings settings) + { + Settings = settings.Clone(); + + Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point); + Text = "设置"; + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.None; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + ClientSize = new Size(392, 268); + BackColor = Color.FromArgb(239, 246, 255); + DoubleBuffered = true; + + _titleLabel = new Label(); + _titleLabel.Text = "设置"; + _titleLabel.Font = new Font("Microsoft YaHei UI", 14F, FontStyle.Bold, GraphicsUnit.Point); + _titleLabel.ForeColor = Color.FromArgb(30, 41, 59); + _titleLabel.TextAlign = ContentAlignment.MiddleCenter; + _titleLabel.AutoSize = false; + _titleLabel.BackColor = Color.Transparent; + + _descriptionLabel = new Label(); + _descriptionLabel.Text = "你可以在这里设置开机自启和自动执行。"; + _descriptionLabel.Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Regular, GraphicsUnit.Point); + _descriptionLabel.ForeColor = Color.FromArgb(100, 116, 139); + _descriptionLabel.TextAlign = ContentAlignment.MiddleCenter; + _descriptionLabel.AutoSize = false; + _descriptionLabel.BackColor = Color.Transparent; + + _startupCheckBox = new CheckBox(); + _startupCheckBox.Text = "开机自启"; + _startupCheckBox.Checked = Settings.StartWithWindows; + _startupCheckBox.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Bold, GraphicsUnit.Point); + _startupCheckBox.ForeColor = Color.FromArgb(30, 41, 59); + _startupCheckBox.BackColor = Color.Transparent; + _startupCheckBox.AutoSize = false; + _startupCheckBox.FlatStyle = FlatStyle.Flat; + + _autoExecuteCheckBox = new CheckBox(); + _autoExecuteCheckBox.Text = "自动执行"; + _autoExecuteCheckBox.Checked = Settings.AutoExecute; + _autoExecuteCheckBox.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Bold, GraphicsUnit.Point); + _autoExecuteCheckBox.ForeColor = Color.FromArgb(30, 41, 59); + _autoExecuteCheckBox.BackColor = Color.Transparent; + _autoExecuteCheckBox.AutoSize = false; + _autoExecuteCheckBox.FlatStyle = FlatStyle.Flat; + + _autoExecuteNoteLabel = new Label(); + _autoExecuteNoteLabel.Text = string.Format("开启后,程序会每 {0} 秒轮询一次目标进程,检测到后自动执行。", AppSettings.AutoExecutePollIntervalMilliseconds / 1000); + _autoExecuteNoteLabel.Font = new Font("Microsoft YaHei UI", 8.8F, FontStyle.Regular, GraphicsUnit.Point); + _autoExecuteNoteLabel.ForeColor = Color.FromArgb(71, 85, 105); + _autoExecuteNoteLabel.TextAlign = ContentAlignment.TopLeft; + _autoExecuteNoteLabel.AutoSize = false; + _autoExecuteNoteLabel.BackColor = Color.Transparent; + + _saveButton = new RoundedButton(); + _saveButton.Text = "保存"; + _saveButton.NormalColor = Color.FromArgb(59, 130, 246); + _saveButton.HoverColor = Color.FromArgb(37, 99, 235); + _saveButton.DisabledColor = Color.FromArgb(147, 197, 253); + _saveButton.Click += SaveButton_Click; + + _cancelButton = new RoundedButton(); + _cancelButton.Text = "取消"; + _cancelButton.NormalColor = Color.FromArgb(100, 116, 139); + _cancelButton.HoverColor = Color.FromArgb(71, 85, 105); + _cancelButton.DisabledColor = Color.FromArgb(148, 163, 184); + _cancelButton.Click += delegate { DialogResult = DialogResult.Cancel; Close(); }; + + _closeButton = new WindowCaptionButton(); + _closeButton.ButtonKind = CaptionButtonKind.Close; + _closeButton.Click += delegate { DialogResult = DialogResult.Cancel; Close(); }; + + Controls.Add(_titleLabel); + Controls.Add(_descriptionLabel); + Controls.Add(_startupCheckBox); + Controls.Add(_autoExecuteCheckBox); + Controls.Add(_autoExecuteNoteLabel); + Controls.Add(_saveButton); + Controls.Add(_cancelButton); + Controls.Add(_closeButton); + + AcceptButton = _saveButton; + CancelButton = _cancelButton; + Resize += SettingsDialog_Resize; + Shown += SettingsDialog_Shown; + LayoutDialog(); + UpdateDialogRegion(); + } + + internal AppSettings Settings { get; private set; } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (SolidBrush backgroundBrush = new SolidBrush(Color.FromArgb(239, 246, 255))) + { + e.Graphics.FillRectangle(backgroundBrush, ClientRectangle); + } + + base.OnPaint(e); + } + + private void SettingsDialog_Shown(object sender, EventArgs e) + { + _saveButton.Focus(); + } + + private void SettingsDialog_Resize(object sender, EventArgs e) + { + LayoutDialog(); + UpdateDialogRegion(); + Invalidate(); + } + + private void LayoutDialog() + { + _closeButton.Bounds = new Rectangle(ClientSize.Width - 16 - 26, 12, 26, 26); + _titleLabel.Bounds = new Rectangle(24, 22, ClientSize.Width - 76, 32); + _descriptionLabel.Bounds = new Rectangle(26, 56, ClientSize.Width - 52, 28); + _startupCheckBox.Bounds = new Rectangle(36, 102, ClientSize.Width - 72, 28); + _autoExecuteCheckBox.Bounds = new Rectangle(36, 142, ClientSize.Width - 72, 28); + _autoExecuteNoteLabel.Bounds = new Rectangle(60, 174, ClientSize.Width - 96, 38); + + int buttonWidth = 92; + int buttonHeight = 42; + int spacing = 14; + int totalWidth = buttonWidth * 2 + spacing; + int left = (ClientSize.Width - totalWidth) / 2; + int top = ClientSize.Height - 54; + + _saveButton.Bounds = new Rectangle(left, top, buttonWidth, buttonHeight); + _cancelButton.Bounds = new Rectangle(left + buttonWidth + spacing, top, buttonWidth, buttonHeight); + } + + private void UpdateDialogRegion() + { + if (ClientSize.Width <= 0 || ClientSize.Height <= 0) + { + return; + } + + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, ClientSize.Width, ClientSize.Height), 28)) + { + Region = new Region(path); + } + } + + private void SaveButton_Click(object sender, EventArgs e) + { + Settings.StartWithWindows = _startupCheckBox.Checked; + Settings.AutoExecute = _autoExecuteCheckBox.Checked; + DialogResult = DialogResult.OK; + Close(); + } +} +internal sealed class ClosePromptDialog : Form +{ + private readonly Label _titleLabel; + private readonly Label _messageLabel; + private readonly RoundedButton _hideButton; + private readonly RoundedButton _exitButton; + private readonly WindowCaptionButton _closeButton; + private CloseChoice _choice; + + private ClosePromptDialog() + { + Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point); + Text = "退出提示"; + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.None; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + ClientSize = new Size(368, 198); + BackColor = Color.FromArgb(239, 246, 255); + DoubleBuffered = true; + + _titleLabel = new Label(); + _titleLabel.Text = "是否关闭程序?"; + _titleLabel.Font = new Font("Microsoft YaHei UI", 14F, FontStyle.Bold, GraphicsUnit.Point); + _titleLabel.ForeColor = Color.FromArgb(30, 41, 59); + _titleLabel.TextAlign = ContentAlignment.MiddleCenter; + _titleLabel.AutoSize = false; + _titleLabel.BackColor = Color.Transparent; + + _messageLabel = new Label(); + _messageLabel.Text = "你可以隐藏到托盘继续后台运行,或直接关闭程序。"; + _messageLabel.Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Regular, GraphicsUnit.Point); + _messageLabel.ForeColor = Color.FromArgb(100, 116, 139); + _messageLabel.TextAlign = ContentAlignment.MiddleCenter; + _messageLabel.AutoSize = false; + _messageLabel.BackColor = Color.Transparent; + + _hideButton = new RoundedButton(); + _hideButton.Text = "隐藏到托盘"; + _hideButton.NormalColor = Color.FromArgb(59, 130, 246); + _hideButton.HoverColor = Color.FromArgb(37, 99, 235); + _hideButton.DisabledColor = Color.FromArgb(147, 197, 253); + _hideButton.Click += delegate { _choice = CloseChoice.HideToTray; DialogResult = DialogResult.OK; Close(); }; + + _exitButton = new RoundedButton(); + _exitButton.Text = "关闭程序"; + _exitButton.NormalColor = Color.FromArgb(234, 88, 12); + _exitButton.HoverColor = Color.FromArgb(194, 65, 12); + _exitButton.DisabledColor = Color.FromArgb(253, 186, 116); + _exitButton.Click += delegate { _choice = CloseChoice.Exit; DialogResult = DialogResult.OK; Close(); }; + + _closeButton = new WindowCaptionButton(); + _closeButton.ButtonKind = CaptionButtonKind.Close; + _closeButton.Click += delegate { _choice = CloseChoice.Cancel; DialogResult = DialogResult.Cancel; Close(); }; + + Controls.Add(_titleLabel); + Controls.Add(_messageLabel); + Controls.Add(_hideButton); + Controls.Add(_exitButton); + Controls.Add(_closeButton); + + AcceptButton = _hideButton; + Shown += ClosePromptDialog_Shown; + Resize += ClosePromptDialog_Resize; + LayoutDialog(); + UpdateDialogRegion(); + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (SolidBrush backgroundBrush = new SolidBrush(Color.FromArgb(239, 246, 255))) + { + e.Graphics.FillRectangle(backgroundBrush, ClientRectangle); + } + + + base.OnPaint(e); + } + + private void ClosePromptDialog_Shown(object sender, EventArgs e) + { + _hideButton.Focus(); + } + + private void ClosePromptDialog_Resize(object sender, EventArgs e) + { + LayoutDialog(); + UpdateDialogRegion(); + Invalidate(); + } + + private void LayoutDialog() + { + _closeButton.Bounds = new Rectangle(ClientSize.Width - 16 - 26, 12, 26, 26); + _titleLabel.Bounds = new Rectangle(24, 24, ClientSize.Width - 76, 32); + _messageLabel.Bounds = new Rectangle(28, 66, ClientSize.Width - 68, 38); + + int buttonWidth = 92; + int buttonHeight = 42; + int spacing = 14; + int totalWidth = buttonWidth * 2 + spacing; + int left = (ClientSize.Width - totalWidth) / 2; + int top = ClientSize.Height - 58; + + _hideButton.Bounds = new Rectangle(left, top, buttonWidth, buttonHeight); + _exitButton.Bounds = new Rectangle(left + buttonWidth + spacing, top, buttonWidth, buttonHeight); + } + + private void UpdateDialogRegion() + { + if (ClientSize.Width <= 0 || ClientSize.Height <= 0) + { + return; + } + + using (GraphicsPath path = RoundedPanel.CreateRoundedPath(new Rectangle(0, 0, ClientSize.Width, ClientSize.Height), 28)) + { + Region = new Region(path); + } + } + + public static CloseChoice ShowChoice(IWin32Window owner) + { + using (ClosePromptDialog dialog = new ClosePromptDialog()) + { + dialog.ShowDialog(owner); + return dialog._choice; + } + } +} + +internal sealed class StatusIconControl : Control +{ + private AppState _visualState; + + public StatusIconControl() + { + Size = new Size(92, 92); + VisualState = AppState.Waiting; + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + } + + public AppState VisualState + { + get { return _visualState; } + set + { + if (_visualState == value) + { + return; + } + + _visualState = value; + Invalidate(); + Update(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Rectangle outer = new Rectangle(0, 0, Width - 1, Height - 1); + + Color backgroundColor; + Color strokeColor; + + if (VisualState == AppState.Success) + { + backgroundColor = Color.FromArgb(236, 253, 245); + strokeColor = Color.FromArgb(16, 185, 129); + } + else if (VisualState == AppState.Failure) + { + backgroundColor = Color.FromArgb(254, 242, 242); + strokeColor = Color.FromArgb(239, 68, 68); + } + else if (VisualState == AppState.Running) + { + backgroundColor = Color.FromArgb(239, 246, 255); + strokeColor = Color.FromArgb(59, 130, 246); + } + else + { + backgroundColor = Color.FromArgb(241, 245, 249); + strokeColor = Color.FromArgb(148, 163, 184); + } + + using (SolidBrush brush = new SolidBrush(backgroundColor)) + { + e.Graphics.FillEllipse(brush, outer); + } + + if (VisualState == AppState.Success) + { + DrawSuccessIcon(e.Graphics, strokeColor); + } + else if (VisualState == AppState.Failure) + { + DrawFailureIcon(e.Graphics, strokeColor); + } + else + { + DrawClockIcon(e.Graphics, strokeColor); + } + } + + private void DrawClockIcon(Graphics graphics, Color strokeColor) + { + float centerX = Width / 2F; + float centerY = Height / 2F; + float circleSize = Math.Min(Width, Height) * 0.5F; + float circleLeft = centerX - circleSize / 2F; + float circleTop = centerY - circleSize / 2F; + float lineWidth = Math.Max(4F, Math.Min(Width, Height) * 0.055F); + + using (Pen pen = new Pen(strokeColor, lineWidth)) + { + pen.StartCap = LineCap.Round; + pen.EndCap = LineCap.Round; + graphics.DrawEllipse(pen, circleLeft, circleTop, circleSize, circleSize); + graphics.DrawLine(pen, centerX, centerY, centerX, centerY - circleSize * 0.3F); + graphics.DrawLine(pen, centerX, centerY, centerX + circleSize * 0.22F, centerY + circleSize * 0.12F); + } + } + + private void DrawSuccessIcon(Graphics graphics, Color strokeColor) + { + float lineWidth = Math.Max(6F, Math.Min(Width, Height) * 0.075F); + PointF pointA = new PointF(Width * 0.30F, Height * 0.54F); + PointF pointB = new PointF(Width * 0.44F, Height * 0.68F); + PointF pointC = new PointF(Width * 0.71F, Height * 0.36F); + + using (Pen pen = new Pen(strokeColor, lineWidth)) + { + pen.StartCap = LineCap.Round; + pen.EndCap = LineCap.Round; + pen.LineJoin = LineJoin.Round; + graphics.DrawLines(pen, new PointF[] { pointA, pointB, pointC }); + } + } + + private void DrawFailureIcon(Graphics graphics, Color strokeColor) + { + float centerX = Width / 2F; + float topY = Height * 0.3F; + float bottomY = Height * 0.62F; + float dotCenterY = Height * 0.78F; + float dotSize = Math.Max(6F, Math.Min(Width, Height) * 0.08F); + float lineWidth = Math.Max(5F, Math.Min(Width, Height) * 0.065F); + + using (Pen pen = new Pen(strokeColor, lineWidth)) + using (SolidBrush brush = new SolidBrush(strokeColor)) + { + pen.StartCap = LineCap.Round; + pen.EndCap = LineCap.Round; + graphics.DrawLine(pen, centerX, topY, centerX, bottomY); + graphics.FillEllipse(brush, centerX - dotSize / 2F, dotCenterY - dotSize / 2F, dotSize, dotSize); + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/README.md" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/README.md" new file mode 100644 index 0000000..5ab08cd --- /dev/null +++ "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/README.md" @@ -0,0 +1,50 @@ +# ACE 低优先级工具(开发分支) + +当前开发版本:`1.2` + +这个分支用于继续开发项目,本地目录已拆分为两个文件夹: + +- `开发文件夹`:源码、UI 设计稿、构建脚本和开发说明 +- `exe文件夹`:编译后的可执行文件 + +## 当前目录结构 + +- `开发文件夹/Program.cs`:主程序源码 +- `开发文件夹/build-exe.ps1`:构建脚本 +- `开发文件夹/run-set-ace-process.cmd`:本地启动入口 +- `开发文件夹/红温猫.jpg`:当前 exe 图标源图 +- `开发文件夹/UI`:界面设计参考稿 +- `开发文件夹/TODO.md`:后续开发计划 +- `exe文件夹/ACE_LowPriority.exe`:编译产物 + +## 开发方式 + +1. 在 `开发文件夹` 中修改源码或资源 +2. 运行下面的命令重新构建: + +```powershell +powershell.exe -ExecutionPolicy Bypass -File .\开发文件夹\build-exe.ps1 +``` + +3. 新的程序会输出到: + +- `exe文件夹/ACE_LowPriority.exe` + +## 当前功能 + +- 启动时自动请求管理员权限 +- 单实例运行,重复打开时会唤醒已运行窗口 +- 支持最小化到托盘与托盘常驻 +- 支持标题栏内置 `设置 / 最小化 / 关闭` 按钮 +- 支持设置 `开机自启` +- 支持设置 `自动执行`,默认每 5 秒轮询一次目标进程 +- 关闭窗口时弹出“隐藏到托盘 / 关闭程序”选择弹窗 +- 图形界面显示等待、处理中、成功、失败状态 +- 将指定进程优先级改为低 +- 将指定进程 CPU 亲和性限制到最后一个逻辑处理器 +- 使用 `红温猫.jpg` 作为程序图标来源 + +## Git 分支说明 + +- `main`:只保留最终发布用的 `exe` 和用户说明 +- `source`:保留开发文件和构建环境 diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/TODO.md" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/TODO.md" new file mode 100644 index 0000000..3767b4e --- /dev/null +++ "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/TODO.md" @@ -0,0 +1,88 @@ +# ACE_LowPriority 开发 TODO + +## 已完成 + +- [x] 将脚本整理为独立 `exe` +- [x] 使用图形界面替代直接终端界面 +- [x] 启动时自动请求管理员权限 +- [x] 支持将目标进程设置为低优先级 +- [x] 支持将目标进程限制到最后一个逻辑处理器 +- [x] 成功与失败界面分别展示结果 +- [x] 成功后支持按 Enter 直接关闭程序 +- [x] 根据参考设计调整主要 UI 布局 +- [x] 使用自定义图标打包 `exe` +- [x] 拆分本地目录为 `开发文件夹` 与 `exe文件夹` +- [x] 将发布版上传到 GitHub Release + +## 当前优先开发 + +### 1. 托盘常驻 +- [ ] 启动后最小化到系统托盘 +- [ ] 托盘菜单支持“立即执行一次” +- [ ] 托盘菜单支持“显示主界面” +- [ ] 托盘菜单支持“退出程序” +- [ ] 防止程序重复启动,改为单实例运行 + +### 2. 自动检测目标进程 +- [ ] 后台轮询 `SGuard64.exe` 是否启动 +- [ ] 检测到目标进程后自动执行优先级调整 +- [ ] 检测到目标进程后自动设置 CPU 亲和性 +- [ ] 目标进程退出后继续等待下次启动 +- [ ] 增加自动重试机制,避免刚启动时设置失败 + +### 3. 开机自启 +- [ ] 增加 UI 开关控制是否开机自启 +- [ ] 通过注册表 `Run` 或启动文件夹实现自启 +- [ ] 支持开机静默启动,不主动弹主窗口 +- [ ] 在界面中显示当前自启状态 + +## 第二阶段 + +### 4. 日志与错误排查 +- [ ] 写入运行日志到本地文件 +- [ ] 记录成功时间、失败时间、错误原因 +- [ ] 增加“查看日志”入口 +- [ ] 错误提示区分未找到进程、权限不足、设置失败等情况 + +### 5. 设置页 +- [ ] 增加独立设置界面 +- [ ] 支持修改目标进程名 +- [ ] 支持修改目标进程路径 +- [ ] 支持选择 CPU 编号 +- [ ] 支持选择优先级级别 +- [ ] 支持设置成功后直接退出或后台驻留 + +## 第三阶段 + +### 6. 通用化增强 +- [ ] 支持多个目标进程规则 +- [ ] 支持不同进程对应不同 CPU / 优先级配置 +- [ ] 支持导入导出配置文件 +- [ ] 支持重置默认配置 + +### 7. 产品化完善 +- [ ] 在界面显示版本号 +- [ ] 增加检查更新功能(基于 GitHub Release) +- [ ] 优化首次启动引导说明 +- [ ] 优化异常状态下的提示文案与视觉样式 + +## 暂不优先 + +- [ ] 自动更新下载安装 +- [ ] 云同步配置 +- [ ] 复杂动画效果 +- [ ] 跨平台支持 + +## 推荐开发顺序 + +1. 托盘常驻 +2. 自动检测目标进程 +3. 开机自启 +4. 日志系统 +5. 设置页 +6. 多进程规则与更新功能 + +## 备注 + +- 当前项目最核心的方向,是从“手动执行一次”升级为“后台自动处理”。 +- 如果后续继续迭代,建议优先保证稳定性和静默运行体验,再扩展更多配置项。 diff --git "a/UI/\346\223\215\344\275\234\345\244\261\350\264\245code.html" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\345\244\261\350\264\245code.html" similarity index 100% rename from "UI/\346\223\215\344\275\234\345\244\261\350\264\245code.html" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\345\244\261\350\264\245code.html" diff --git "a/UI/\346\223\215\344\275\234\345\244\261\350\264\245screen.png" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\345\244\261\350\264\245screen.png" similarity index 100% rename from "UI/\346\223\215\344\275\234\345\244\261\350\264\245screen.png" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\345\244\261\350\264\245screen.png" diff --git "a/UI/\346\223\215\344\275\234\346\210\220\345\212\237code.html" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\346\210\220\345\212\237code.html" similarity index 100% rename from "UI/\346\223\215\344\275\234\346\210\220\345\212\237code.html" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\346\210\220\345\212\237code.html" diff --git "a/UI/\346\223\215\344\275\234\346\210\220\345\212\237screen.png" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\346\210\220\345\212\237screen.png" similarity index 100% rename from "UI/\346\223\215\344\275\234\346\210\220\345\212\237screen.png" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\346\223\215\344\275\234\346\210\220\345\212\237screen.png" diff --git "a/UI/\347\255\211\345\276\205\345\220\257\345\212\250code.html" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\347\255\211\345\276\205\345\220\257\345\212\250code.html" similarity index 100% rename from "UI/\347\255\211\345\276\205\345\220\257\345\212\250code.html" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\347\255\211\345\276\205\345\220\257\345\212\250code.html" diff --git "a/UI/\347\255\211\345\276\205\345\220\257\345\212\250screen.png" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\347\255\211\345\276\205\345\220\257\345\212\250screen.png" similarity index 100% rename from "UI/\347\255\211\345\276\205\345\220\257\345\212\250screen.png" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\347\255\211\345\276\205\345\220\257\345\212\250screen.png" diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\351\275\277\350\275\256\345\233\276\346\240\207.png" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\351\275\277\350\275\256\345\233\276\346\240\207.png" new file mode 100644 index 0000000..af1a8b7 Binary files /dev/null and "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/UI/\351\275\277\350\275\256\345\233\276\346\240\207.png" differ diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/build-exe.ps1" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/build-exe.ps1" new file mode 100644 index 0000000..9cd1d70 --- /dev/null +++ "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/build-exe.ps1" @@ -0,0 +1,150 @@ +param( + [string]$OutputPath = (Join-Path (Join-Path $PSScriptRoot '..\exe文件夹') 'ACE_LowPriority.exe') +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function New-IconFileFromImage { + param( + [Parameter(Mandatory = $true)] + [string]$ImagePath, + + [Parameter(Mandatory = $true)] + [string]$IcoPath + ) + + Add-Type -AssemblyName System.Drawing + + $sizes = @(16, 32, 48, 64, 128, 256) + $sourceImage = [System.Drawing.Image]::FromFile($ImagePath) + + try { + $iconFrames = New-Object System.Collections.Generic.List[byte[]] + + foreach ($size in $sizes) { + $bitmap = New-Object System.Drawing.Bitmap $size, $size + try { + $bitmap.SetResolution(96, 96) + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + try { + $graphics.Clear([System.Drawing.Color]::Transparent) + $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality + $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + $graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality + $graphics.DrawImage($sourceImage, 0, 0, $size, $size) + } + finally { + $graphics.Dispose() + } + + $memoryStream = New-Object System.IO.MemoryStream + try { + $bitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png) + $iconFrames.Add($memoryStream.ToArray()) + } + finally { + $memoryStream.Dispose() + } + } + finally { + $bitmap.Dispose() + } + } + + $fileStream = [System.IO.File]::Open($IcoPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write) + try { + $writer = New-Object System.IO.BinaryWriter($fileStream) + try { + $writer.Write([UInt16]0) + $writer.Write([UInt16]1) + $writer.Write([UInt16]$iconFrames.Count) + + $dataOffset = 6 + (16 * $iconFrames.Count) + for ($index = 0; $index -lt $iconFrames.Count; $index++) { + $frameData = $iconFrames[$index] + $size = $sizes[$index] + $dimension = if ($size -ge 256) { 0 } else { [byte]$size } + + $writer.Write([byte]$dimension) + $writer.Write([byte]$dimension) + $writer.Write([byte]0) + $writer.Write([byte]0) + $writer.Write([UInt16]1) + $writer.Write([UInt16]32) + $writer.Write([UInt32]$frameData.Length) + $writer.Write([UInt32]$dataOffset) + $dataOffset += $frameData.Length + } + + foreach ($frameData in $iconFrames) { + $writer.Write($frameData) + } + } + finally { + $writer.Dispose() + } + } + finally { + $fileStream.Dispose() + } + } + finally { + $sourceImage.Dispose() + } +} + +$sourcePath = Join-Path $PSScriptRoot 'Program.cs' +if (-not (Test-Path -LiteralPath $sourcePath)) { + throw "Source file not found: $sourcePath" +} + +$outputDirectory = Split-Path -Path $OutputPath -Parent +if (-not (Test-Path -LiteralPath $outputDirectory)) { + [void](New-Item -ItemType Directory -Force -Path $outputDirectory) +} + +$iconSourcePath = Join-Path $PSScriptRoot '红温猫.jpg' +if (-not (Test-Path -LiteralPath $iconSourcePath)) { + throw "Icon source not found: $iconSourcePath" +} + +$temporaryIconPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ACE_LowPriority_{0}.ico' -f [System.Guid]::NewGuid().ToString('N')) + +try { + New-IconFileFromImage -ImagePath $iconSourcePath -IcoPath $temporaryIconPath + + $provider = New-Object Microsoft.CSharp.CSharpCodeProvider + $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters + $compilerParameters.GenerateExecutable = $true + $compilerParameters.GenerateInMemory = $false + $compilerParameters.IncludeDebugInformation = $false + $compilerParameters.TreatWarningsAsErrors = $false + $compilerParameters.WarningLevel = 4 + $compilerParameters.OutputAssembly = $OutputPath + $compilerParameters.CompilerOptions = ('/target:winexe /platform:x64 /optimize+ /win32icon:"{0}"' -f $temporaryIconPath) + + [void]$compilerParameters.ReferencedAssemblies.Add('System.dll') + [void]$compilerParameters.ReferencedAssemblies.Add('System.Core.dll') + [void]$compilerParameters.ReferencedAssemblies.Add('System.Drawing.dll') + [void]$compilerParameters.ReferencedAssemblies.Add('System.Windows.Forms.dll') + + $results = $provider.CompileAssemblyFromFile($compilerParameters, $sourcePath) + $errors = @($results.Errors | Where-Object { -not $_.IsWarning }) + if ($errors.Count -gt 0) { + $messages = foreach ($compilerIssue in $errors) { + '{0}({1},{2}): {3}' -f $sourcePath, $compilerIssue.Line, $compilerIssue.Column, $compilerIssue.ErrorText + } + + throw ($messages -join [Environment]::NewLine) + } +} +finally { + if (Test-Path -LiteralPath $temporaryIconPath) { + Remove-Item -LiteralPath $temporaryIconPath -Force + } +} + +Write-Host "Build succeeded: $OutputPath" -ForegroundColor Green +Write-Host "Icon embedded from: $iconSourcePath" -ForegroundColor Green diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/run-set-ace-process.cmd" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/run-set-ace-process.cmd" new file mode 100644 index 0000000..3015acc --- /dev/null +++ "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/run-set-ace-process.cmd" @@ -0,0 +1,24 @@ +@echo off +setlocal EnableExtensions + +set "SCRIPT_DIR=%~dp0" +set "SEARCH_ROOT=%SCRIPT_DIR%.." +set "APP_EXE=" + +for /r "%SEARCH_ROOT%" %%I in (ACE_LowPriority.exe) do ( + if /i not "%%~fI"=="%~f0" ( + set "APP_EXE=%%~fI" + goto :found + ) +) + +:found +if not defined APP_EXE ( + echo ERROR: ACE_LowPriority.exe not found under "%SEARCH_ROOT%". + echo Please run build-exe.ps1 first. + pause + exit /b 1 +) + +"%APP_EXE%" +exit /b %ERRORLEVEL% diff --git a/set-ace-process.ps1 "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/set-ace-process.ps1" similarity index 100% rename from set-ace-process.ps1 rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/set-ace-process.ps1" diff --git "a/\344\273\273\345\212\241\346\217\217\350\277\260.txt" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/\344\273\273\345\212\241\346\217\217\350\277\260.txt" similarity index 100% rename from "\344\273\273\345\212\241\346\217\217\350\277\260.txt" rename to "\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/\344\273\273\345\212\241\346\217\217\350\277\260.txt" diff --git "a/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/\347\272\242\346\270\251\347\214\253.jpg" "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/\347\272\242\346\270\251\347\214\253.jpg" new file mode 100644 index 0000000..40b1d1a Binary files /dev/null and "b/\345\274\200\345\217\221\346\226\207\344\273\266\345\244\271/\347\272\242\346\270\251\347\214\253.jpg" differ