feat(app): 初始化自动关机工具首个版本
实现基于 WPF 的 AutoShutdown 主界面,支持关机、重启、睡眠、休眠、唤醒、锁屏和注销等电源任务。 支持指定时间和倒计时计划、执行前提醒、系统关机撤销、Windows 唤醒任务、托盘运行、自定义图标以及 OmniNotify 通知适配。 修复关闭到托盘时的运行提醒,并支持单击托盘图标打开或收起主界面。 补充 README、发布配置和 win-x64 Release 输出要求。 Release: win-x64
This commit is contained in:
471
MainWindow.xaml.cs
Normal file
471
MainWindow.xaml.cs
Normal 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)!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user