fix: 修复DPI缩放获取和定时触发器同步问题

修复 DpiHelper 在未初始化窗口句柄时获取 DPI 缩放值的问题,现在能正确处理多显示器场景。重构 QuartzSchedulerService 的触发器同步逻辑,确保定时任务正确更新。在添加/编辑触发器时增加 Cron 表达式格式校验,避免无效表达式导致调度失败。同时修复 ScriptService 中任务启动时的线程调度问题。
This commit is contained in:
辉鸭蛋
2026-05-09 02:50:52 +08:00
parent 5a6b074759
commit 769e08edfd
7 changed files with 279 additions and 145 deletions

View File

@@ -83,4 +83,7 @@
编译指令参考,如果出现程序占用场景,直接放弃编译验证即可
```
dotnet build BetterGenshinImpact.sln -c Debug
```
```
###其他要求
1. 改动时候请不要删除已有的注释,你可以修改,但是不要删!

View File

@@ -39,7 +39,7 @@ namespace BetterGenshinImpact.GameTask
GameHandle = hWnd;
PostMessageSimulator = Simulation.PostMessage(GameHandle);
SystemInfo = new SystemInfo(hWnd);
DpiScale = DpiHelper.ScaleY;
DpiScale = DpiHelper.GetScale(hWnd).Y;
//MaskWindowHandle = new WindowInteropHelper(MaskWindow.Instance()).Handle;
IsInitialized = true;
}
@@ -108,4 +108,4 @@ namespace BetterGenshinImpact.GameTask
}
}
}
}
}

View File

@@ -16,8 +16,7 @@ public class DpiHelper
private static float GetScaleY()
{
if (Environment.OSVersion.Version >= new Version(6, 3)
&& UIDispatcherHelper.MainWindow != null)
if (Environment.OSVersion.Version >= new Version(6, 3))
{
HWND hWnd = HWND.NULL;
if (TaskContext.Instance().IsInitialized)
@@ -26,12 +25,15 @@ public class DpiHelper
}
else
{
hWnd = new WindowInteropHelper(Application.Current?.MainWindow).Handle;
hWnd = GetMainWindowHandle();
}
HMONITOR hMonitor = User32.MonitorFromWindow(hWnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST);
SHCore.GetDpiForMonitor(hMonitor, SHCore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out _, out uint dpiY);
return dpiY / 96f;
if (hWnd != HWND.NULL)
{
HMONITOR hMonitor = User32.MonitorFromWindow(hWnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST);
SHCore.GetDpiForMonitor(hMonitor, SHCore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out _, out uint dpiY);
return dpiY / 96f;
}
}
HDC hdc = User32.GetDC(HWND.NULL);
@@ -40,6 +42,27 @@ public class DpiHelper
return scaleY / 96f;
}
private static HWND GetMainWindowHandle()
{
var application = Application.Current;
if (application?.Dispatcher == null)
{
return HWND.NULL;
}
if (application.Dispatcher.CheckAccess())
{
return application.MainWindow == null
? HWND.NULL
: new WindowInteropHelper(application.MainWindow).Handle;
}
return application.Dispatcher.Invoke(() =>
application.MainWindow == null
? HWND.NULL
: new WindowInteropHelper(application.MainWindow).Handle);
}
public static DpiScaleF GetScale(nint hWnd = 0)
{
if (hWnd != IntPtr.Zero)

View File

@@ -4,10 +4,12 @@ using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Model.Gear.Triggers;
using BetterGenshinImpact.Model.Gear.Triggers.QuartzJob;
using BetterGenshinImpact.ViewModel.Pages.Component;
using BetterGenshinImpact.Service.GearTask;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Quartz;
using Quartz.Impl.Matchers;
namespace BetterGenshinImpact.Service;
@@ -19,11 +21,38 @@ public class QuartzSchedulerService(ILogger<QuartzSchedulerService> logger,
GearTriggerStorageService triggerStorageService) : IHostedService
{
private readonly ILogger<QuartzSchedulerService> _logger = logger;
private const string GearGroupName = "gear";
public async Task StartAsync(CancellationToken cancellationToken)
{
var (timedTriggers, _) = await triggerStorageService.LoadTriggersAsync();
await SyncTimedTriggersAsync(timedTriggers, cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async Task SyncTimedTriggersAsync(IEnumerable<GearTriggerViewModel> timedTriggers, CancellationToken cancellationToken = default)
{
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
var existingJobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(GearGroupName), cancellationToken);
if (existingJobKeys.Count > 0)
{
await scheduler.DeleteJobs(existingJobKeys.ToList(), cancellationToken);
}
var jobsDictionary = BuildJobsDictionary(timedTriggers);
if (jobsDictionary.Count > 0)
{
await scheduler.ScheduleJobs(jobsDictionary, replace: true, cancellationToken);
}
}
private static Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>> BuildJobsDictionary(IEnumerable<GearTriggerViewModel> timedTriggers)
{
var allData = timedTriggers
.Where(t => t.IsEnabled && !string.IsNullOrWhiteSpace(t.CronExpression))
.Select(t => t.ToTrigger())
@@ -48,12 +77,12 @@ public class QuartzSchedulerService(ILogger<QuartzSchedulerService> logger,
};
var job = JobBuilder.Create<QuartzGearTaskJob>()
.WithIdentity($"job:{data.Name}", "gear")
.WithIdentity($"job:{data.Name}", GearGroupName)
.UsingJobData(jobDataMap)
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"trigger:{data.Name}", "gear")
.WithIdentity($"trigger:{data.Name}", GearGroupName)
.WithCronSchedule(data.CronExpression)
.ForJob(job)
.Build();
@@ -61,16 +90,7 @@ public class QuartzSchedulerService(ILogger<QuartzSchedulerService> logger,
jobsDictionary.Add(job, new HashSet<ITrigger> { trigger });
}
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
if (jobsDictionary.Count > 0)
{
await scheduler.ScheduleJobs(jobsDictionary, replace: true, cancellationToken);
}
return jobsDictionary;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Core.Script.Dependence;
using BetterGenshinImpact.Core.Script.Group;
@@ -559,7 +560,19 @@ public partial class ScriptService : IScriptService
var homePageViewModel = App.GetService<HomePageViewModel>();
if (!homePageViewModel!.TaskDispatcherEnabled)
{
await homePageViewModel.OnStartTriggerAsync();
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher?.CheckAccess() == true)
{
await homePageViewModel.OnStartTriggerAsync();
}
else if (dispatcher != null)
{
await dispatcher.InvokeAsync(homePageViewModel.OnStartTriggerAsync).Task.Unwrap();
}
else
{
await homePageViewModel.OnStartTriggerAsync();
}
if (waitForMainUi)
{

View File

@@ -1,14 +1,18 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using BetterGenshinImpact.ViewModel.Pages.Component;
using BetterGenshinImpact.Model.Gear.Triggers;
using BetterGenshinImpact.Model;
using BetterGenshinImpact.View.Windows.GearTask;
using BetterGenshinImpact.Model.Gear.Triggers;
using BetterGenshinImpact.Service;
using BetterGenshinImpact.Service.GearTask;
using BetterGenshinImpact.View.Windows;
using BetterGenshinImpact.View.Windows.GearTask;
using BetterGenshinImpact.ViewModel.Pages.Component;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
using Quartz;
namespace BetterGenshinImpact.ViewModel.Pages;
@@ -16,6 +20,7 @@ public partial class GearTriggerPageViewModel : ViewModel
{
private readonly ILogger<GearTriggerPageViewModel> _logger;
private readonly GearTriggerStorageService _storageService;
private readonly QuartzSchedulerService _quartzSchedulerService;
[ObservableProperty]
private ObservableCollection<GearTriggerViewModel> _timedTriggers = new();
@@ -26,18 +31,27 @@ public partial class GearTriggerPageViewModel : ViewModel
[ObservableProperty]
private GearTriggerViewModel? _selectedTrigger;
[ObservableProperty]
private GearTaskDefinitionViewModel? _selectedTaskDefinition;
public GearTriggerPageViewModel(
ILogger<GearTriggerPageViewModel> logger,
GearTriggerStorageService storageService,
QuartzSchedulerService quartzSchedulerService)
{
_logger = logger;
_storageService = storageService;
_quartzSchedulerService = quartzSchedulerService;
}
partial void OnSelectedTriggerChanged(GearTriggerViewModel? value)
{
EditTriggerCommand.NotifyCanExecuteChanged();
}
[ObservableProperty]
private GearTaskDefinitionViewModel? _selectedTaskDefinition;
public GearTriggerPageViewModel(ILogger<GearTriggerPageViewModel> logger, GearTriggerStorageService storageService)
public override void OnNavigatedTo()
{
_logger = logger;
_storageService = storageService;
_ = LoadTriggersAsync();
}
private void UpdateTimedTriggersNextRunTime()
@@ -48,107 +62,155 @@ public partial class GearTriggerPageViewModel : ViewModel
}
}
public override void OnNavigatedTo()
{
_ = LoadTriggersAsync();
}
/// <summary>
/// 异步加载触发器数据
/// </summary>
private async Task LoadTriggersAsync()
{
try
{
var (timedTriggers, hotkeyTriggers) = await _storageService.LoadTriggersAsync();
TimedTriggers.Clear();
HotkeyTriggers.Clear();
foreach (var trigger in timedTriggers)
{
trigger.UpdateNextRunTime();
TimedTriggers.Add(trigger);
}
foreach (var trigger in hotkeyTriggers)
{
HotkeyTriggers.Add(trigger);
}
UpdateTimedTriggersNextRunTime();
_logger.LogInformation("已加载 {TimedCount} 个定时触发器和 {HotkeyCount} 个快捷键触发器",
TimedTriggers.Count, HotkeyTriggers.Count);
_logger.LogInformation("已加载 {TimedCount} 个定时触发器和 {HotkeyCount} 个热键触发器", TimedTriggers.Count, HotkeyTriggers.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "加载触发器数据时发生错误");
}
}
/// <summary>
/// 保存触发器数据
/// </summary>
private async Task SaveTriggersAsync()
private async Task<bool> SaveTriggersAsync()
{
if (!ValidateTimedTriggers())
{
return false;
}
try
{
await _storageService.SaveTriggersAsync(TimedTriggers, HotkeyTriggers);
_logger.LogInformation("触发器数据已保存");
await _quartzSchedulerService.SyncTimedTriggersAsync(TimedTriggers);
_logger.LogInformation("触发器数据已保存,并已同步到 Quartz 调度器");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "保存触发器数据时发生错误");
ThemedMessageBox.Error($"保存触发器失败:{ex.Message}", "保存失败");
return false;
}
}
private bool ValidateTimedTriggers()
{
var invalidTrigger = TimedTriggers.FirstOrDefault(t =>
t.TriggerType == TriggerType.Timed &&
t.IsEnabled &&
!string.IsNullOrWhiteSpace(t.CronExpression) &&
!IsValidCronExpression(t.CronExpression, out _));
if (invalidTrigger == null)
{
return true;
}
IsValidCronExpression(invalidTrigger.CronExpression, out var errorMessage);
ThemedMessageBox.Error($"触发器“{invalidTrigger.Name}”的 Cron 表达式无效。\n{errorMessage}", "保存失败");
return false;
}
private static bool IsValidCronExpression(string? cronExpression, out string errorMessage)
{
errorMessage = string.Empty;
if (string.IsNullOrWhiteSpace(cronExpression))
{
errorMessage = "Cron 表达式不能为空";
return false;
}
try
{
_ = new CronExpression(cronExpression);
return true;
}
catch (FormatException ex)
{
errorMessage = ex.Message;
return false;
}
catch (Exception ex)
{
errorMessage = ex.Message;
return false;
}
}
[RelayCommand]
private void AddTimedTrigger()
private async Task AddTimedTrigger()
{
var dialog = AddTriggerDialog.ShowAddTriggerDialog(TriggerType.Timed);
if (dialog != null)
if (dialog == null)
{
var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Timed)
{
CronExpression = dialog.CronExpression,
TaskDefinitionName = dialog.SelectedTaskDefinitionName,
IsEnabled = true
};
TimedTriggers.Add(newTrigger);
SelectedTrigger = newTrigger;
// 保存数据
_ = SaveTriggersAsync();
return;
}
var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Timed)
{
CronExpression = dialog.CronExpression,
TaskDefinitionName = dialog.SelectedTaskDefinitionName,
IsEnabled = dialog.IsEnabled
};
TimedTriggers.Add(newTrigger);
SelectedTrigger = newTrigger;
newTrigger.UpdateNextRunTime();
await SaveTriggersAsync();
}
[RelayCommand]
private void AddHotkeyTrigger()
private async Task AddHotkeyTrigger()
{
var dialog = AddTriggerDialog.ShowAddTriggerDialog(TriggerType.Hotkey);
if (dialog != null)
if (dialog == null)
{
var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Hotkey)
{
Hotkey = dialog.SelectedHotkey,
HotkeyType = dialog.HotkeyType,
TaskDefinitionName = dialog.SelectedTaskDefinitionName,
IsEnabled = true
};
HotkeyTriggers.Add(newTrigger);
SelectedTrigger = newTrigger;
// 保存数据
_ = SaveTriggersAsync();
return;
}
var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Hotkey)
{
Hotkey = dialog.SelectedHotkey,
HotkeyType = dialog.HotkeyType,
TaskDefinitionName = dialog.SelectedTaskDefinitionName,
IsEnabled = dialog.IsEnabled
};
HotkeyTriggers.Add(newTrigger);
SelectedTrigger = newTrigger;
await SaveTriggersAsync();
}
[RelayCommand]
private void DeleteTrigger()
private async Task DeleteTrigger()
{
if (SelectedTrigger == null) return;
if (SelectedTrigger == null)
{
return;
}
switch (SelectedTrigger.TriggerType)
{
@@ -161,9 +223,7 @@ public partial class GearTriggerPageViewModel : ViewModel
}
SelectedTrigger = null;
// 保存数据
_ = SaveTriggersAsync();
await SaveTriggersAsync();
}
private bool CanEditTrigger()
@@ -172,7 +232,7 @@ public partial class GearTriggerPageViewModel : ViewModel
}
[RelayCommand(CanExecute = nameof(CanEditTrigger))]
private void EditTrigger()
private async Task EditTrigger()
{
if (SelectedTrigger is not { } selectedTrigger)
{
@@ -204,6 +264,6 @@ public partial class GearTriggerPageViewModel : ViewModel
selectedTrigger.ModifiedTime = DateTime.Now;
selectedTrigger.UpdateNextRunTime();
_ = SaveTriggersAsync();
await SaveTriggersAsync();
}
}

View File

@@ -1,24 +1,26 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using BetterGenshinImpact.ViewModel.Pages.Component;
using BetterGenshinImpact.Model.Gear.Triggers;
using BetterGenshinImpact.Model;
using BetterGenshinImpact.Service;
using Microsoft.Extensions.Logging;
using Wpf.Ui.Violeta.Controls;
using BetterGenshinImpact.Helpers.Extensions;
using System.ComponentModel;
using BetterGenshinImpact.Model;
using BetterGenshinImpact.Model.Gear.Triggers;
using BetterGenshinImpact.Service;
using BetterGenshinImpact.View.Windows;
using BetterGenshinImpact.ViewModel.Pages.Component;
using Microsoft.Extensions.Logging;
using Quartz;
namespace BetterGenshinImpact.ViewModel.Windows.GearTask;
/// <summary>
/// 新增触发器对话框 ViewModel
/// 新增/编辑触发器对话框 ViewModel
/// </summary>
public partial class AddTriggerDialogViewModel : ObservableObject
{
private const string DefaultCronExpression = "1 0 4 * * ?";
private readonly GearTaskStorageService _storageService;
private readonly ILogger<AddTriggerDialogViewModel> _logger;
@@ -32,7 +34,7 @@ public partial class AddTriggerDialogViewModel : ObservableObject
private TriggerType _selectedTriggerType = TriggerType.Timed;
[ObservableProperty]
private string _cronExpression = "1 0 4 * * ?"; // 默认每天 04:00:01
private string _cronExpression = DefaultCronExpression;
[ObservableProperty]
private CronInputMode _selectedCronInputMode = CronInputMode.Preset;
@@ -106,33 +108,25 @@ public partial class AddTriggerDialogViewModel : ObservableObject
LoadAvailableTaskDefinitions();
}
/// <summary>
/// 构造函数,用于指定触发器类型
/// </summary>
public AddTriggerDialogViewModel(GearTaskStorageService storageService, ILogger<AddTriggerDialogViewModel> logger, TriggerType? predefinedType = null)
public AddTriggerDialogViewModel(
GearTaskStorageService storageService,
ILogger<AddTriggerDialogViewModel> logger,
TriggerType? predefinedType = null)
: this(storageService, logger)
{
_storageService = storageService;
_logger = logger;
// 如果指定了预定义类型,则设置并禁用选择
if (predefinedType.HasValue)
{
SelectedTriggerType = predefinedType.Value;
IsTriggerTypeSelectionEnabled = false;
}
// 生成默认名称
GenerateDefaultName();
// 加载可用的任务定义
LoadAvailableTaskDefinitions();
}
public AddTriggerDialogViewModel(GearTaskStorageService storageService, ILogger<AddTriggerDialogViewModel> logger, GearTriggerViewModel existingTrigger)
public AddTriggerDialogViewModel(
GearTaskStorageService storageService,
ILogger<AddTriggerDialogViewModel> logger,
GearTriggerViewModel existingTrigger)
: this(storageService, logger)
{
_storageService = storageService;
_logger = logger;
DialogTitle = "编辑触发器";
IsTriggerTypeSelectionEnabled = false;
@@ -142,21 +136,16 @@ public partial class AddTriggerDialogViewModel : ObservableObject
SelectedTaskDefinitionName = existingTrigger.TaskDefinitionName;
CronExpression = existingTrigger.TriggerType == TriggerType.Timed
? (existingTrigger.CronExpression ?? CronExpression)
: CronExpression;
? existingTrigger.CronExpression ?? DefaultCronExpression
: DefaultCronExpression;
SelectedCronInputMode = existingTrigger.TriggerType == TriggerType.Timed
? CronInputMode.Manual
: CronInputMode.Preset;
SelectedHotkey = existingTrigger.TriggerType == TriggerType.Hotkey
? existingTrigger.Hotkey
: null;
SelectedHotkey = existingTrigger.TriggerType == TriggerType.Hotkey ? existingTrigger.Hotkey : null;
HotkeyType = existingTrigger.TriggerType == TriggerType.Hotkey
? existingTrigger.HotkeyType
: HotKeyTypeEnum.KeyboardMonitor;
LoadAvailableTaskDefinitions();
}
/// <summary>
@@ -188,9 +177,9 @@ public partial class AddTriggerDialogViewModel : ObservableObject
AvailableTaskDefinitions.Add(taskDefinition.Name);
}
}
_logger.LogInformation("已加载 {Count} 个可用的任务定义", AvailableTaskDefinitions.Count);
// 如果有任务定义,默认选择第一个
if (AvailableTaskDefinitions.Count > 0 && string.IsNullOrWhiteSpace(SelectedTaskDefinitionName))
{
@@ -229,44 +218,42 @@ public partial class AddTriggerDialogViewModel : ObservableObject
partial void OnSelectedCronInputModeChanged(CronInputMode value)
{
if (SelectedTriggerType != TriggerType.Timed)
if (SelectedTriggerType == TriggerType.Timed && string.IsNullOrWhiteSpace(CronExpression))
{
return;
}
if (string.IsNullOrWhiteSpace(CronExpression))
{
CronExpression = "1 0 4 * * ?";
CronExpression = DefaultCronExpression;
}
}
/// <summary>
/// 确认创建触发器
/// </summary>
[RelayCommand]
private void Confirm()
{
if (string.IsNullOrWhiteSpace(TriggerName))
{
Toast.Error("请输入触发器名称");
ThemedMessageBox.Error("请输入触发器名称", "保存失败");
return;
}
if (SelectedTriggerType == TriggerType.Timed && string.IsNullOrWhiteSpace(CronExpression))
{
Toast.Error(SelectedCronInputMode == CronInputMode.Manual
var message = SelectedCronInputMode == CronInputMode.Manual
? "请输入 Cron 表达式"
: "请先完成定时选择");
: "请先完成定时选择";
ThemedMessageBox.Error(message, "保存失败");
return;
}
if (SelectedTriggerType == TriggerType.Timed && !IsValidCronExpression(CronExpression, out var cronErrorMessage))
{
ThemedMessageBox.Error(cronErrorMessage, "Cron 表达式错误");
return;
}
if (SelectedTriggerType == TriggerType.Hotkey && SelectedHotkey == null)
{
Toast.Error("请选择热键");
ThemedMessageBox.Error("请选择热键", "保存失败");
return;
}
// 创建触发器 ViewModel
CreatedTrigger = new GearTriggerViewModel(TriggerName, SelectedTriggerType)
{
IsEnabled = IsEnabled,
@@ -294,7 +281,9 @@ public partial class AddTriggerDialogViewModel : ObservableObject
[RelayCommand]
private void SwitchHotKeyType()
{
HotkeyType = HotkeyType == HotKeyTypeEnum.GlobalRegister ? HotKeyTypeEnum.KeyboardMonitor : HotKeyTypeEnum.GlobalRegister;
HotkeyType = HotkeyType == HotKeyTypeEnum.GlobalRegister
? HotKeyTypeEnum.KeyboardMonitor
: HotKeyTypeEnum.GlobalRegister;
}
/// <summary>
@@ -303,8 +292,33 @@ public partial class AddTriggerDialogViewModel : ObservableObject
[RelayCommand]
private void SelectHotkey()
{
// 移除旧的示例代码现在使用HotKeyTextBox直接设置
// HotKeyTextBox会直接绑定到SelectedHotkey属性
}
private static bool IsValidCronExpression(string? cronExpression, out string errorMessage)
{
errorMessage = string.Empty;
if (string.IsNullOrWhiteSpace(cronExpression))
{
errorMessage = "Cron 表达式不能为空";
return false;
}
try
{
_ = new CronExpression(cronExpression);
return true;
}
catch (FormatException ex)
{
errorMessage = $"Cron 表达式格式无效:{ex.Message}";
return false;
}
catch (Exception ex)
{
errorMessage = $"Cron 表达式校验失败:{ex.Message}";
return false;
}
}
}
@@ -312,6 +326,7 @@ public enum CronInputMode
{
[Description("可视化选择")]
Preset,
[Description("手动 Cron")]
Manual
}