You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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="&#xE72E;" />
                </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的非托管底层。

修复步骤

  1. 通过DispatcherQueue切换到UI线程更新控件
    利用DispatcherQueue.TryEnqueue将UI更新逻辑调度到UI线程执行,确保所有UI操作都在正确的线程上下文进行。

  2. 优化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);
}
  1. 补充钩子释放的安全性
    在页面卸载或窗口关闭时,确保钩子被正确释放,避免内存泄漏:
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的自动隐藏定时器,避免长时间显示:
    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();
    }
    
    在UI更新后调用ResetFlyoutTimer()即可实现自动隐藏。

内容的提问来源于stack exchange,提问作者ChenYiLins

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 04:55:58