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

快速调整WinUI 3窗口大小时触发System.ExecutionEngineException异常

解决WinUI3窗口调整大小时PInvoke.CallWindowProc抛出ExecutionEngineException的问题

核心原因

WinUI3桌面应用的窗口在快速调整大小时,可能触发窗口句柄的重建或销毁操作,导致你通过SetWindowLongPtr子类化窗口时保存的旧窗口过程指针(_oldWndProc)失效。后续窗口消息触发CallWindowProc调用时,引用了已被回收或无效的内存地址,从而抛出System.ExecutionEngineException。另外,窗口过程委托若被GC回收,也会引发此类内存访问异常。

解决方案步骤

  1. 绑定窗口子类化到窗口句柄生命周期
    监听窗口的HandleChanged事件,在句柄创建时设置子类化,句柄销毁时恢复原窗口过程,避免使用失效的句柄或窗口过程指针:

    private IntPtr _oldWndProc;
    private readonly PInvoke.WNDPROC _wndProcDelegate = WndProc; // 类级别委托,防止GC回收
    
    public MainWindow()
    {
        InitializeComponent();
        HandleChanged += OnWindowHandleChanged;
    }
    
    private void OnWindowHandleChanged(object sender, EventArgs e)
    {
        var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
        
        if (hwnd != IntPtr.Zero)
        {
            // 仅在句柄有效时设置子类化
            _oldWndProc = PInvoke.SetWindowLongPtr(hwnd, PInvoke.GWL_WNDPROC, 
                Marshal.GetFunctionPointerForDelegate(_wndProcDelegate));
        }
        else
        {
            // 句柄失效时恢复原窗口过程
            if (_oldWndProc != IntPtr.Zero)
            {
                PInvoke.SetWindowLongPtr(hwnd, PInvoke.GWL_WNDPROC, _oldWndProc);
                _oldWndProc = IntPtr.Zero;
            }
        }
    }
    
  2. 安全调用CallWindowProc
    在窗口过程回调中,先校验_oldWndProc的有效性,若无效则调用DefWindowProc兜底,避免访问无效内存:

    private static IntPtr WndProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        // 处理热键消息(F6切换ToggleButton)
        const int HOTKEY_ID = 1001;
        if (msg == PInvoke.WM_HOTKEY && wParam == (IntPtr)HOTKEY_ID)
        {
            // 通过DispatcherQueue回到UI线程操作控件
            Application.Current.DispatcherQueue.TryEnqueue(() =>
            {
                var mainWindow = (MainWindow)Application.Current.Windows[0];
                mainWindow.MyToggleButton.IsChecked = !mainWindow.MyToggleButton.IsChecked;
            });
            return IntPtr.Zero;
        }
    
        // 获取当前窗口的旧窗口过程指针
        var mainWindow = (MainWindow)Application.Current.Windows[0];
        if (mainWindow._oldWndProc != IntPtr.Zero)
        {
            return PInvoke.CallWindowProc(mainWindow._oldWndProc, hwnd, msg, wParam, lParam);
        }
        
        // 兜底调用默认窗口过程
        return PInvoke.DefWindowProc(hwnd, msg, wParam, lParam);
    }
    
  3. 关键注意事项

    • 窗口过程委托必须声明为类级别的字段,不能是局部变量,否则会被.NET GC回收,导致CallWindowProc调用无效的内存地址。
    • 操作UI控件时必须通过DispatcherQueue.TryEnqueue切换到UI线程,窗口过程回调运行在消息循环线程,直接操作UI会引发线程安全问题。
    • 快速调整窗口时会频繁触发WM_SIZE、WM_SIZING等消息,确保窗口过程中的处理逻辑尽可能轻量化,避免阻塞消息循环。
  4. 额外排查点

    • 检查热键注册逻辑,确保窗口销毁时调用UnregisterHotKey释放资源,避免残留的热键消息干扰后续窗口实例。
    • 使用CsWin32的安全封装,尽量避免直接操作原始IntPtr,必要时使用SafeHandle管理窗口句柄,减少悬垂指针风险。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 04:27:16