实现基于 WPF 的 AutoShutdown 主界面,支持关机、重启、睡眠、休眠、唤醒、锁屏和注销等电源任务。 支持指定时间和倒计时计划、执行前提醒、系统关机撤销、Windows 唤醒任务、托盘运行、自定义图标以及 OmniNotify 通知适配。 修复关闭到托盘时的运行提醒,并支持单击托盘图标打开或收起主界面。 补充 README、发布配置和 win-x64 Release 输出要求。 Release: win-x64
472 lines
16 KiB
C#
472 lines
16 KiB
C#
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)!;
|
||
}
|
||
}
|