Files
auto-shutdown/MainWindow.xaml.cs
home-PC d2d81a482b feat(app): 初始化自动关机工具首个版本
实现基于 WPF 的 AutoShutdown 主界面,支持关机、重启、睡眠、休眠、唤醒、锁屏和注销等电源任务。

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

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

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

Release: win-x64
2026-05-18 23:54:58 +08:00

472 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)!;
}
}