feat(app): 初始化自动关机工具首个版本

实现基于 WPF 的 AutoShutdown 主界面,支持关机、重启、睡眠、休眠、唤醒、锁屏和注销等电源任务。

支持指定时间和倒计时计划、执行前提醒、系统关机撤销、Windows 唤醒任务、托盘运行、自定义图标以及 OmniNotify 通知适配。

修复关闭到托盘时的运行提醒,并支持单击托盘图标打开或收起主界面。

补充 README、发布配置和 win-x64 Release 输出要求。

Release: win-x64
This commit is contained in:
2026-05-18 23:54:58 +08:00
commit d2d81a482b
17 changed files with 1611 additions and 0 deletions

471
MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,471 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
using AutoShutdown.Models;
using AutoShutdown.Services;
using Forms = System.Windows.Forms;
namespace AutoShutdown;
public partial class MainWindow : Window
{
private readonly PowerService _powerService = new();
private readonly WakeService _wakeService = new();
private readonly NotificationSettingsStore _settingsStore = new();
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromSeconds(1) };
private readonly Dictionary<PowerAction, System.Windows.Controls.Button> _actionButtons;
private readonly Forms.NotifyIcon? _trayIcon;
private readonly bool _enableTray;
private NotificationSettings _notificationSettings;
private NotificationService _notificationService;
private PowerAction _selectedAction = PowerAction.Shutdown;
private PowerAction? _scheduledAction;
private DateTime? _scheduledAt;
private bool _hasExecuted;
private bool _reminderShown;
private bool _allowExit;
public MainWindow(bool enableTray = true)
{
_enableTray = enableTray;
_notificationSettings = _settingsStore.Load();
_notificationService = new NotificationService(_notificationSettings);
InitializeComponent();
_actionButtons = new Dictionary<PowerAction, System.Windows.Controls.Button>
{
[PowerAction.Shutdown] = ShutdownButton,
[PowerAction.Restart] = RestartButton,
[PowerAction.Sleep] = SleepButton,
[PowerAction.Hibernate] = HibernateButton,
[PowerAction.Wake] = WakeButton,
[PowerAction.Lock] = LockButton,
[PowerAction.LogOff] = LogOffButton
};
foreach (var (action, button) in _actionButtons)
{
button.Tag = action;
button.ToolTip = action.Description();
}
DateInput.Text = DateTime.Today.ToString("yyyy/M/d");
TimeInput.Text = DateTime.Now.AddMinutes(30).ToString("HH:mm");
ForceCheck.ToolTip = "强制关闭未响应程序,仅用于关机、重启、注销等动作。";
_trayIcon = _enableTray ? CreateTrayIcon() : null;
SelectAction(PowerAction.Shutdown);
UpdateScheduleInputs();
UpdateStatus();
_timer.Tick += (_, _) => OnTick();
_timer.Start();
}
protected override void OnClosing(CancelEventArgs e)
{
if (_enableTray && !_allowExit)
{
e.Cancel = true;
HideToTray();
return;
}
_trayIcon?.Dispose();
base.OnClosing(e);
}
private Forms.NotifyIcon CreateTrayIcon()
{
var menu = new Forms.ContextMenuStrip();
menu.Items.Add("显示/收起主窗口", null, (_, _) => Dispatcher.Invoke(ToggleMainWindowFromTray));
menu.Items.Add("取消当前计划", null, async (_, _) => await Dispatcher.InvokeAsync(CancelCurrentPlanAsync));
menu.Items.Add("OmniNotify 适配设置", null, (_, _) => Dispatcher.Invoke(OpenNotificationSettings));
menu.Items.Add(new Forms.ToolStripSeparator());
menu.Items.Add("退出 AutoShutdown", null, (_, _) => ExitFromTray());
var icon = new Forms.NotifyIcon
{
Icon = LoadTrayIcon(),
Text = "AutoShutdown",
ContextMenuStrip = menu,
Visible = true
};
icon.MouseClick += (_, e) =>
{
if (e.Button == Forms.MouseButtons.Left)
{
Dispatcher.Invoke(ToggleMainWindowFromTray);
}
};
return icon;
}
private static System.Drawing.Icon LoadTrayIcon()
{
var processPath = Environment.ProcessPath;
return !string.IsNullOrWhiteSpace(processPath) && System.Drawing.Icon.ExtractAssociatedIcon(processPath) is { } icon
? icon
: System.Drawing.SystemIcons.Application;
}
private void HideToTray(bool showHint = true)
{
Hide();
if (!showHint)
{
return;
}
if (_notificationSettings.Mode == NotificationMode.OmniNotify)
{
_ = _notificationService.NotifyAsync("AutoShutdown 已在托盘运行", "关闭窗口不会退出应用,可在托盘菜单中退出。");
return;
}
_trayIcon?.ShowBalloonTip(2500, "AutoShutdown 已在托盘运行", "关闭窗口不会退出应用,可在托盘菜单中退出。", Forms.ToolTipIcon.Info);
}
private void ToggleMainWindowFromTray()
{
if (IsVisible && WindowState != WindowState.Minimized)
{
HideToTray(showHint: false);
return;
}
RestoreFromTray();
}
private void RestoreFromTray()
{
Show();
WindowState = WindowState.Normal;
Activate();
}
private void ExitFromTray()
{
_allowExit = true;
Dispatcher.Invoke(Close);
}
private void UpdateTrayText(string text)
{
if (_trayIcon is null)
{
return;
}
_trayIcon.Text = text.Length > 63 ? text[..63] : text;
}
private void ActionButton_Click(object sender, RoutedEventArgs e)
{
if (sender is System.Windows.Controls.Button { Tag: PowerAction action })
{
SelectAction(action);
}
}
private void ScheduleModeChanged(object sender, RoutedEventArgs e) => UpdateScheduleInputs();
private async void StartButton_Click(object sender, RoutedEventArgs e) => await StartPlanAsync();
private async void CancelButton_Click(object sender, RoutedEventArgs e) => await CancelPlanAsync();
private async void AbortButton_Click(object sender, RoutedEventArgs e) => await AbortSystemShutdownAsync();
private void SettingsButton_Click(object sender, RoutedEventArgs e) => OpenNotificationSettings();
private void OpenNotificationSettings()
{
var window = new NotificationSettingsWindow(_notificationSettings, _notificationService)
{
Owner = this
};
if (window.ShowDialog() != true)
{
return;
}
_notificationSettings = window.Settings;
_settingsStore.Save(_notificationSettings);
_notificationService.UpdateSettings(_notificationSettings);
UpdateStatus();
}
private void SelectAction(PowerAction action)
{
_selectedAction = action;
ActionTitleText.Text = action.Label();
ActionDescriptionText.Text = action.Description();
foreach (var (item, button) in _actionButtons)
{
var selected = item == action;
button.Background = selected ? BrushFrom("#E2ECFF") : BrushFrom("#FFFFFF");
button.Foreground = selected ? BrushFrom("#2563EB") : BrushFrom("#111827");
button.BorderBrush = selected ? BrushFrom("#2563EB") : BrushFrom("#DEE6F2");
button.BorderThickness = selected ? new Thickness(2) : new Thickness(1);
}
}
private async Task StartPlanAsync()
{
if (!TryResolveScheduleTime(out var scheduledAt))
{
await _notificationService.NotifyAsync(this, "计划无效", "请检查时间或倒计时输入。", MessageBoxImage.Warning);
return;
}
if (scheduledAt <= DateTime.Now.AddSeconds(1))
{
await _notificationService.NotifyAsync(this, "计划无效", "请选择一个晚于当前时间的计划。", MessageBoxImage.Warning);
return;
}
var scheduledAction = _selectedAction;
var forceApps = ForceCheck.IsChecked == true;
_scheduledAt = scheduledAt;
_scheduledAction = scheduledAction;
_hasExecuted = false;
_reminderShown = false;
UpdateStatus();
await Dispatcher.Yield(DispatcherPriority.Background);
var seconds = (int)Math.Ceiling((scheduledAt - DateTime.Now).TotalSeconds);
if (scheduledAction == PowerAction.Wake)
{
await TryRunAsync(
() => _wakeService.ScheduleWake(scheduledAt),
"计划已启动",
"已创建 Windows 唤醒任务。请确保电源选项允许唤醒计时器。");
}
else if (scheduledAction is PowerAction.Shutdown or PowerAction.Restart)
{
await TryRunAsync(
() => _powerService.Execute(scheduledAction, seconds, forceApps),
"计划已启动",
"已交给 Windows 系统计划执行。");
}
else
{
await _notificationService.NotifyAsync(this, "计划已启动", $"{scheduledAction.Label()} 将在 {scheduledAt:yyyy-MM-dd HH:mm} 执行。");
}
}
private async Task CancelPlanAsync()
{
var hadPlan = _scheduledAt is not null;
var scheduledAction = _scheduledAction;
_scheduledAt = null;
_scheduledAction = null;
_hasExecuted = false;
_reminderShown = false;
UpdateStatus();
if (scheduledAction == PowerAction.Wake)
{
await TryCancelWakeAsync();
}
if (hadPlan && scheduledAction is not null)
{
await _notificationService.NotifyAsync(this, "计划已取消", $"已取消 {scheduledAction.Value.Label()} 计划。");
}
}
private Task CancelCurrentPlanAsync()
{
return _scheduledAction is PowerAction.Shutdown or PowerAction.Restart
? AbortSystemShutdownAsync()
: CancelPlanAsync();
}
private async Task AbortSystemShutdownAsync()
{
if (_powerService.CancelShutdown())
{
_scheduledAt = null;
_scheduledAction = null;
_hasExecuted = false;
_reminderShown = false;
UpdateStatus();
await _notificationService.NotifyAsync(this, "已撤销", "已请求 Windows 撤销待执行的关机/重启任务。");
return;
}
await _notificationService.NotifyAsync(this, "无需撤销", "当前没有可撤销的系统关机/重启任务,或系统拒绝了该请求。");
}
private async void OnTick()
{
if (_scheduledAt is null)
{
UpdateStatus();
return;
}
var remaining = _scheduledAt.Value - DateTime.Now;
if (remaining.TotalSeconds <= 0 && !_hasExecuted)
{
_hasExecuted = true;
var scheduledAction = _scheduledAction ?? _selectedAction;
var forceApps = ForceCheck.IsChecked == true;
if (scheduledAction is not (PowerAction.Shutdown or PowerAction.Restart or PowerAction.Wake))
{
await TryRunAsync(
() => _powerService.Execute(scheduledAction, 0, forceApps),
"执行完成",
$"{scheduledAction.Label()} 已执行。",
showSuccess: false);
}
_scheduledAt = null;
_scheduledAction = null;
}
else if (!_reminderShown && TryReadInt(ReminderInput.Text, 0, 120, out var reminder) && reminder > 0 && remaining.TotalMinutes <= reminder)
{
_reminderShown = true;
var scheduledAction = _scheduledAction ?? _selectedAction;
await _notificationService.NotifyAsync(this, "任务提醒", $"{scheduledAction.Label()} 将在 {FormatRemaining(remaining)} 后执行。");
}
UpdateStatus();
}
private bool TryResolveScheduleTime(out DateTime scheduledAt)
{
if (ClockRadio.IsChecked == true)
{
if (!DateTime.TryParse(DateInput.Text, out var date) || !TimeSpan.TryParse(TimeInput.Text, out var time))
{
scheduledAt = default;
return false;
}
scheduledAt = date.Date.Add(time);
return true;
}
if (!TryReadInt(HoursInput.Text, 0, 168, out var hours) || !TryReadInt(MinutesInput.Text, 0, 59, out var minutes))
{
scheduledAt = default;
return false;
}
scheduledAt = DateTime.Now.AddHours(hours).AddMinutes(minutes);
return true;
}
private void UpdateScheduleInputs()
{
if (DateInput is null || TimeInput is null || HoursInput is null || MinutesInput is null)
{
return;
}
var byClock = ClockRadio.IsChecked == true;
DateInput.IsEnabled = byClock;
TimeInput.IsEnabled = byClock;
HoursInput.IsEnabled = !byClock;
MinutesInput.IsEnabled = !byClock;
}
private void UpdateStatus()
{
UpdatePlanActionButtons();
if (_scheduledAt is null)
{
StatusText.Text = _notificationSettings.Mode == NotificationMode.OmniNotify
? $"当前没有运行中的计划。提示OmniNotify / {_notificationSettings.Channel}"
: "当前没有运行中的计划。";
CountdownText.Text = "待命";
UpdateTrayText("AutoShutdown - 待命");
return;
}
var remaining = _scheduledAt.Value - DateTime.Now;
if (remaining < TimeSpan.Zero)
{
remaining = TimeSpan.Zero;
}
var scheduledAction = _scheduledAction ?? _selectedAction;
StatusText.Text = $"下一次任务:{_scheduledAt:yyyy-MM-dd HH:mm} 执行 {scheduledAction.Label()}";
CountdownText.Text = FormatRemaining(remaining);
UpdateTrayText($"AutoShutdown - {scheduledAction.Label()} {FormatRemaining(remaining)}");
}
private void UpdatePlanActionButtons()
{
if (StartPlanButton is null || CancelPlanButton is null || AbortShutdownButton is null)
{
return;
}
var hasPlan = _scheduledAt is not null;
var needsSystemAbort = hasPlan && _scheduledAction is PowerAction.Shutdown or PowerAction.Restart;
StartPlanButton.Visibility = hasPlan ? Visibility.Collapsed : Visibility.Visible;
CancelPlanButton.Visibility = hasPlan && !needsSystemAbort ? Visibility.Visible : Visibility.Collapsed;
AbortShutdownButton.Visibility = needsSystemAbort ? Visibility.Visible : Visibility.Collapsed;
}
private async Task TryRunAsync(Action action, string title, string successMessage, bool showSuccess = true)
{
try
{
await Task.Run(action);
if (showSuccess)
{
await _notificationService.NotifyAsync(this, title, successMessage);
}
}
catch (Exception ex)
{
_scheduledAt = null;
_scheduledAction = null;
UpdateStatus();
await _notificationService.NotifyAsync(this, "启动失败", ex.Message, MessageBoxImage.Error);
}
}
private async Task TryCancelWakeAsync()
{
try
{
await Task.Run(_wakeService.CancelWake);
}
catch
{
// 取消普通计划时不因为系统任务不存在或权限差异打断用户。
}
}
private static bool TryReadInt(string text, int min, int max, out int value)
{
return int.TryParse(text, out value) && value >= min && value <= max;
}
private static string FormatRemaining(TimeSpan value)
{
if (value.TotalDays >= 1)
{
return $"{(int)value.TotalDays}天 {value.Hours:D2}:{value.Minutes:D2}:{value.Seconds:D2}";
}
return $"{Math.Max(0, (int)value.TotalHours):D2}:{value.Minutes:D2}:{value.Seconds:D2}";
}
private static System.Windows.Media.Brush BrushFrom(string hex)
{
return (System.Windows.Media.Brush)new System.Windows.Media.BrushConverter().ConvertFromString(hex)!;
}
}