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

WinUI 3检测显示器插拔无通知,求问题排查与解决方法

WinUI 3中检测显示器插拔状态失败的问题分析与解决方案

现有代码的问题

你的代码无法收到通知主要有以下几个原因:

1. 未指定显示器设备类GUID

RegisterDeviceNotification调用时使用了Guid.Empty,系统无法精准识别显示器设备的插拔事件。需要替换为显示器设备接口的专用GUID:GUID_DEVINTERFACE_MONITOR,其值为{6BDD1FC6-810F-11D0-BEC7-08002BE2092F}。

2. WinUI 3窗口消息未转发到自定义WndProc

你定义的WndProc方法并没有被系统调用,WinUI 3的窗口消息处理机制和传统Win32不同,需要手动拦截窗口消息并转发到你的处理逻辑中。

3. 窗口句柄获取时机可能过早

在MainWindow构造函数中获取_hwnd可能时机不对,此时窗口尚未完全初始化,导致句柄无效。建议在Window.Activated事件触发后再获取窗口句柄并启动监听。

修复后的代码示例

1. 修改ScreenMonitor类,指定正确的GUID并处理消息转发

public class ScreenMonitor
{
    private const int WM_DEVICECHANGE = 0x0219;
    public const int DBT_DEVICEARRIVAL = 0x8000;
    public const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
    private const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005;
    // 显示器设备接口GUID
    private static readonly Guid GUID_DEVINTERFACE_MONITOR = new Guid("6BDD1FC6-810F-11D0-BEC7-08002BE2092F");

    [StructLayout(LayoutKind.Sequential)]
    public struct DEV_BROADCAST_DEVICEINTERFACE
    {
        public int dbcc_size;
        public int dbcc_devicetype;
        public int dbcc_reserved;
        public Guid dbcc_classguid;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string dbcc_name;
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, ref DEV_BROADCAST_DEVICEINTERFACE NotificationFilter, int Flags);

    [DllImport("user32.dll")]
    public static extern bool UnregisterDeviceNotification(IntPtr hHandle);

    // 用于替换窗口过程的委托
    private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
    private WndProcDelegate _originalWndProc;
    private IntPtr _hwnd;
    private IntPtr notificationHandle;

    public void StartMonitoring(IntPtr hwnd)
    {
        _hwnd = hwnd;
        DEV_BROADCAST_DEVICEINTERFACE dbdi = new DEV_BROADCAST_DEVICEINTERFACE();
        dbdi.dbcc_size = Marshal.SizeOf(dbdi);
        dbdi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
        dbdi.dbcc_reserved = 0;
        // 指定显示器设备GUID
        dbdi.dbcc_classguid = GUID_DEVINTERFACE_MONITOR;

        notificationHandle = RegisterDeviceNotification(hwnd, ref dbdi, 0);

        if (notificationHandle == IntPtr.Zero)
        {
            throw new Exception($"Failed to register: {Marshal.GetLastWin32Error()}");
        }

        // 替换窗口过程,拦截消息
        _originalWndProc = (WndProcDelegate)Marshal.GetDelegateForFunctionPointer(
            GetWindowLongPtr(hwnd, GWLP_WNDPROC), typeof(WndProcDelegate));
        SetWindowLongPtr(hwnd, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(new WndProcDelegate(CustomWndProc)));
    }

    public void StopMonitoring()
    {
        if (notificationHandle != IntPtr.Zero)
        {
            UnregisterDeviceNotification(notificationHandle);
            notificationHandle = IntPtr.Zero;
        }

        // 恢复原窗口过程
        if (_hwnd != IntPtr.Zero && _originalWndProc != null)
        {
            SetWindowLongPtr(_hwnd, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_originalWndProc));
            _originalWndProc = null;
            _hwnd = IntPtr.Zero;
        }
    }

    public event EventHandler<DeviceChangeEventArgs> DeviceChange;

    protected virtual void OnDeviceChange(DeviceChangeEventArgs e)
    {
        DeviceChange?.Invoke(this, e);
    }

    private IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        if (msg == WM_DEVICECHANGE)
        {
            int wParamInt = wParam.ToInt32();
            if (wParamInt == DBT_DEVICEARRIVAL || wParamInt == DBT_DEVICEREMOVECOMPLETE)
            {
                // 验证设备类型是否为显示器
                var dbdi = Marshal.PtrToStructure<DEV_BROADCAST_DEVICEINTERFACE>(lParam);
                if (dbdi.dbcc_classguid == GUID_DEVINTERFACE_MONITOR)
                {
                    OnDeviceChange(new DeviceChangeEventArgs(wParamInt));
                }
            }
        }

        // 调用原窗口过程
        return _originalWndProc(hWnd, msg, wParam, lParam);
    }

    // 补充P/Invoke函数
    private const int GWLP_WNDPROC = -4;
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
}

public class DeviceChangeEventArgs : EventArgs
{
    public int EventType { get; }

    public DeviceChangeEventArgs(int eventType)
    {
        EventType = eventType;
    }
}

2. 修改MainWindow中的初始化逻辑

private ScreenMonitor _screenMonitor;

public MainWindow()
{
    this.InitializeComponent();
    this.Activated += MainWindow_Activated;
    this.Closed += MainWindow_Closed;
}

private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
    // 确保只初始化一次
    if (_screenMonitor == null)
    {
        _screenMonitor = new ScreenMonitor();
        _screenMonitor.DeviceChange += (s, e) =>
        {
            if (e.EventType == ScreenMonitor.DBT_DEVICEARRIVAL)
            {
                Log.Logger.Debug("Screen plugged in");
            }
            else if (e.EventType == ScreenMonitor.DBT_DEVICEREMOVECOMPLETE)
            {
                Log.Logger.Debug("Screen removed");
            }
        };

        // 获取窗口句柄
        var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
        _screenMonitor.StartMonitoring(hwnd);
    }
}

private void MainWindow_Closed(object sender, WindowEventArgs args)
{
    _screenMonitor?.StopMonitoring();
}

替代方案:使用WinUI原生API

如果不想处理Win32消息,可以使用WinUI提供的DisplayInformation类,通过监听DisplayContentsChanged事件来检测显示状态变化,再结合屏幕数量判断是否为插拔事件:

private int _previousScreenCount;

public MainWindow()
{
    this.InitializeComponent();
    this.Activated += MainWindow_Activated;
}

private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
    _previousScreenCount = System.Windows.Forms.Screen.AllScreens.Length;
    DisplayInformation.Current.DisplayContentsChanged += Current_DisplayContentsChanged;
}

private void Current_DisplayContentsChanged(DisplayInformation sender, object args)
{
    int currentCount = System.Windows.Forms.Screen.AllScreens.Length;
    if (currentCount > _previousScreenCount)
    {
        Log.Logger.Debug("Screen plugged in");
    }
    else if (currentCount < _previousScreenCount)
    {
        Log.Logger.Debug("Screen removed");
    }
    _previousScreenCount = currentCount;
}

注意:使用System.Windows.Forms.Screen需要引用System.Windows.Forms NuGet包,或者改用Win32 API获取屏幕数量。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 19:32:22