C# WinUI快速按NumLock/CapsLock触发System.ExecutionEngineException崩溃求助
WinUI 3键盘钩子快速按键导致System.ExecutionEngineException崩溃问题
问题现象
在C# WinUI项目中实现键盘钩子监听NumLock和CapsLock按键,按下时弹出Flyout显示锁键状态,但快速按下按键时,程序在App.g.i.cs中崩溃,提示Microsoft.WinUI.dll中发生未处理的System.ExecutionEngineException类型异常。
崩溃触发点代码:
static void Main(string[] args) { XamlCheckProcessRequirements(); global::WinRT.ComWrappersSupport.InitializeComWrappers(); global::Microsoft.UI.Xaml.Application.Start((p) => { var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); global::System.Threading.SynchronizationContext.SetSynchronizationContext(context); new App(); }); }
猜测是TextBlock属性快速变更导致,但try-catch无法捕获该异常。
项目依赖NuGet包
- Microsoft.Windows.CsWin32 version: 0.3.49-beta
- Microsoft.WindowsAppSDK version: 1.5.240311000
相关代码
FlyoutPage.xaml
<Page x:Class="MoreFlyout.Views.FlyoutPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:MoreFlyout.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Page.Resources> <AcrylicBrush x:Key="CustomAcrylicBrush" AlwaysUseFallback="False" /> </Page.Resources> <Page.ContextFlyout> <Flyout x:Name="FlyoutWindowContextFlyout" ShouldConstrainToRootBounds="False"> <Flyout.SystemBackdrop> <local:AcrylicSystemBackdrop /> </Flyout.SystemBackdrop> <Flyout.FlyoutPresenterStyle> <Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter"> <Setter Property="Background" Value="Transparent" /> </Style> </Flyout.FlyoutPresenterStyle> <Grid x:Name="ContentArea" MinWidth="164" Padding="-8"> <Grid.ColumnDefinitions> <ColumnDefinition Width="48" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <HyperlinkButton x:Name="IconButton" Grid.Column="0" Width="32" Height="32" IsEnabled="False"> <FontIcon x:Name="StatusFontIcon" Margin="-4" FontSize="12" Foreground="White" Glyph="" /> </HyperlinkButton> <TextBlock x:Name="StatusTextBlock" Grid.Column="1" VerticalAlignment="Center" TextAlignment="Center" /> </Grid> </Flyout> </Page.ContextFlyout> </Page>
FlyoutPage.xaml.cs
using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.UI.Composition; using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using MoreFlyout.Helpers; using MoreFlyout.ViewModels; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; namespace MoreFlyout.Views; public sealed partial class FlyoutPage : Page { private readonly Microsoft.UI.Dispatching.DispatcherQueue dispatcherQueue; private static System.Timers.Timer? aTimer; private UnhookWindowsHookExSafeHandle HookID; private const int WM_KEYDOWN = 0x0100; private const int VK_NUMLOCK = 0x90; private const int VK_CAPSLOCK = 0x14; private static bool numKeyState = false; private static bool capsKeyState = false; public FlyoutViewModel ViewModel { get; } public FlyoutPage() { ViewModel = App.GetService<FlyoutViewModel>(); InitializeComponent(); dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); numKeyState = (PInvoke.GetKeyState(VK_NUMLOCK) & 1) == 1; capsKeyState = (PInvoke.GetKeyState(VK_CAPSLOCK) & 1) == 1; HookID = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, HookCallback, null, 0); App.FlyoutWindow.Closed += FlyoutWindowClosed; } private void FlyoutWindowClosed(object sender, WindowEventArgs args) => HookID.Close(); private LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0 && wParam == WM_KEYDOWN) { var vkCode = Marshal.ReadInt32(lParam); if (vkCode == VK_NUMLOCK) { StatusTextBlock.Text = numKeyState ? "StatusWords_NumUnlock".GetLocalized() : "StatusWords_NumLock".GetLocalized(); StatusFontIcon.Glyph = numKeyState ? "\uE785" : "\uE72E"; numKeyState = !numKeyState; } else if (vkCode == VK_CAPSLOCK) { StatusTextBlock.Text = capsKeyState ? "StatusWords_CapsUnlock".GetLocalized() : "StatusWords_CapsLock".GetLocalized(); StatusFontIcon.Glyph = capsKeyState ? "\uE785" : "\uE72E"; capsKeyState = !capsKeyState; } if(FlyoutWindowContextFlyout.IsOpen == false) FlyoutWindowContextFlyout.ShowAt(this); } return PInvoke.CallNextHookEx(null, nCode, wParam, lParam); } }
解决方案
核心问题原因
键盘钩子的HookCallback运行在非UI线程中,直接修改UI控件属性会导致线程不安全操作,快速按键时频繁跨线程访问UI元素,触发WinUI内部的执行引擎异常。System.ExecutionEngineException通常和非托管代码交互时的线程安全问题相关,try-catch无法捕获是因为异常发生在WinUI的非托管底层。
修复步骤
通过DispatcherQueue切换到UI线程更新控件
利用DispatcherQueue.TryEnqueue将UI更新逻辑调度到UI线程执行,确保所有UI操作都在正确的线程上下文进行。优化Flyout显示逻辑,避免重复触发
增加防抖逻辑,防止快速按键时频繁调用ShowAt导致的Flyout状态冲突。
修改后的HookCallback代码:
private LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0 && wParam == WM_KEYDOWN) { var vkCode = Marshal.ReadInt32(lParam); bool needUpdateUI = false; string? newText = null; string? newGlyph = null; if (vkCode == VK_NUMLOCK) { newText = numKeyState ? "StatusWords_NumUnlock".GetLocalized() : "StatusWords_NumLock".GetLocalized(); newGlyph = numKeyState ? "\uE785" : "\uE72E"; numKeyState = !numKeyState; needUpdateUI = true; } else if (vkCode == VK_CAPSLOCK) { newText = capsKeyState ? "StatusWords_CapsUnlock".GetLocalized() : "StatusWords_CapsLock".GetLocalized(); newGlyph = capsKeyState ? "\uE785" : "\uE72E"; capsKeyState = !capsKeyState; needUpdateUI = true; } if (needUpdateUI) { // 调度到UI线程更新控件 dispatcherQueue.TryEnqueue(() => { StatusTextBlock.Text = newText; StatusFontIcon.Glyph = newGlyph; // 防抖处理:仅在Flyout未打开时显示,且避免短时间内重复调用 if (!FlyoutWindowContextFlyout.IsOpen) { FlyoutWindowContextFlyout.ShowAt(this); } }); } } return PInvoke.CallNextHookEx(null, nCode, wParam, lParam); }
- 补充钩子释放的安全性
在页面卸载或窗口关闭时,确保钩子被正确释放,避免内存泄漏:
private void FlyoutWindowClosed(object sender, WindowEventArgs args) { if (!HookID.IsClosed) { HookID.Close(); } } protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); if (!HookID.IsClosed) { HookID.Close(); } }
额外优化建议
- 使用
DispatcherQueuePriority.Low调度非紧急的UI更新,减少主线程压力:dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () => { ... }); - 增加Flyout的自动隐藏定时器,避免长时间显示:
在UI更新后调用private void ResetFlyoutTimer() { aTimer?.Stop(); aTimer = new System.Timers.Timer(2000); // 2秒后自动关闭 aTimer.Elapsed += (s, e) => { dispatcherQueue.TryEnqueue(() => { if (FlyoutWindowContextFlyout.IsOpen) { FlyoutWindowContextFlyout.Hide(); } }); }; aTimer.Start(); }ResetFlyoutTimer()即可实现自动隐藏。
内容的提问来源于stack exchange,提问作者ChenYiLins
相关产品推荐
相关产品推荐

