Detect new monitor arrival with C# and WPF

Recently, the need arose to detect if a new monitor or beamer was connected to the PC. When this happened the WPF application had to perform some refresh action. There were some examples online such as on StackOverflow but none of the examples was complete so it took some time to find the right solution. This article aims to provide a complete solution for people needing the same functionality.

Register notification and process message loop

The first step is to register for a device notification, in this case the monitor device but it could also be an usb device. To register for the notification we need the window handle which we can get from the main wpf window. The code for obtaining the handle is in the OnSourceInitialized method. Furthermore, we need to listen to the message loop and finally we register for the notification using the device notification class. The WndProc callback listens for the WM_DEVICE_CHANGE message and filters for the Device arrival and removal. The code for the DeviceNotification helper class is listed below the code for the wpf window.

    public partial class MainWindow : Window
    {
        #region Construction

        public MainWindow()
        {
            this.InitializeComponent();

            this.Closed += MainWindow_Closed;
        }

        private static void MainWindow_Closed(object sender, EventArgs e)
        {
            DeviceNotification.UnRegisterUsbDeviceNotification();
            DeviceNotification.UnRegisterMonitorDeviceNotification();
        }

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);

            if (!(PresentationSource.FromVisual(this) is HwndSource source))
                return;

            source.AddHook(this.WndProc);

            DeviceNotification.RegisterUsbDeviceNotification(source.Handle);
            DeviceNotification.RegisterMonitorDeviceNotification(source.Handle);
        }

        private IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg != DeviceNotification.WmDeviceChange)
                return IntPtr.Zero;

            Debug.WriteLine("WM_DEVICE_CHANGE TRIGGERED");

            switch ((int)wParam)
            {
                case DeviceNotification.DbtDeviceRemoveComplete:
                case DeviceNotification.DbtDeviceArrival:
                {
                    if (DeviceNotification.IsMonitor(lParam))
                        Debug.WriteLine($"Monitor {((int)wParam == DeviceNotification.DbtDeviceArrival ? "arrived" : "removed")}");

                    if (DeviceNotification.IsUsbDevice(lParam))
                       Debug.WriteLine($"Usb device {((int)wParam == DeviceNotification.DbtDeviceArrival ? "arrived" : "removed")}");
                }
                    break;
            }

            return IntPtr.Zero;
        }

        #endregion
    }

DeviceNotification helper class

The RegisterDeviceNotification method registers for the wanted notification. It uses a device interface struct that gets created by CreateBroadcastDeviceInterface that needs the class Guid for the wanted device type. IsDeviceOfClass checks if the parameter passed in from the message loop is from the wanted device by checking the class Guid.

    internal static class DeviceNotification
    {
        #region Constants & types

        public const int DbtDeviceArrival = 0x8000; // System detected a new device        
        public const int DbtDeviceRemoveComplete = 0x8004; // Device is gone      
        public const int WmDeviceChange = 0x0219; // Device change event      

        #endregion

        #region Methods

        public static bool IsMonitor(IntPtr lParam)
        {
            return IsDeviceOfClass(lParam, GuidDeviceInterfaceMonitorDevice);
        }

        public static bool IsUsbDevice(IntPtr lParam)
        {
            return IsDeviceOfClass(lParam, GuidDeviceInterfaceUSBDevice);
        }

        /// Registers a window to receive notifications when Monitor devices are plugged or unplugged.
        public static void RegisterMonitorDeviceNotification(IntPtr windowHandle)
        {
            var dbi = CreateBroadcastDeviceInterface(GuidDeviceInterfaceMonitorDevice);
            monitorNotificationHandle = RegisterDeviceNotification(dbi, windowHandle);
        }

        /// Registers a window to receive notifications when USB devices are plugged or unplugged.
        public static void RegisterUsbDeviceNotification(IntPtr windowHandle)
        {
            var dbi = CreateBroadcastDeviceInterface(GuidDeviceInterfaceUSBDevice);
            usbNotificationHandle = RegisterDeviceNotification(dbi, windowHandle);
        }

        /// UnRegisters the window for Monitor device notifications
        public static void UnRegisterMonitorDeviceNotification()
        {
            UnregisterDeviceNotification(monitorNotificationHandle);
        }

        /// UnRegisters the window for USB device notifications
        public static void UnRegisterUsbDeviceNotification()
        {
            UnregisterDeviceNotification(usbNotificationHandle);
        }

        #endregion

        #region Private or protected constants & types

        private const int DbtDeviceTypeDeviceInterface = 5;

        // https://docs.microsoft.com/en-us/windows-hardware/drivers/install/guid-devinterface-usb-device
        private static readonly Guid GuidDeviceInterfaceUSBDevice = new Guid("A5DCBF10-6530-11D2-901F-00C04FB951ED"); // USB devices

        // https://docs.microsoft.com/en-us/windows-hardware/drivers/install/guid-devinterface-monitor
        private static readonly Guid GuidDeviceInterfaceMonitorDevice = new Guid("E6F07B5F-EE97-4a90-B076-33F57BF4EAA7"); // Monitor devices
        private static IntPtr usbNotificationHandle;
        private static IntPtr monitorNotificationHandle;

        [StructLayout(LayoutKind.Sequential)]
        private struct DevBroadcastDeviceInterface
        {
            internal int Size;
            internal int DeviceType;
            internal int Reserved;
            internal Guid ClassGuid;
            internal short Name;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct DevBroadcastHdr
        {
            internal UInt32 Size;
            internal UInt32 DeviceType;
            internal UInt32 Reserved;
        }

        #endregion

        #region Private & protected methods

        private static bool IsDeviceOfClass(IntPtr lParam, Guid classGuid)
        {
            var hdr = Marshal.PtrToStructure(lParam);

            if (hdr.DeviceType != DbtDeviceTypeDeviceInterface)
                return false;

            var devIF = Marshal.PtrToStructure(lParam);

            return devIF.ClassGuid == classGuid;

        }

        private static DevBroadcastDeviceInterface CreateBroadcastDeviceInterface(Guid classGuid)
        {
            var dbi = new DevBroadcastDeviceInterface
            {
                DeviceType = DbtDeviceTypeDeviceInterface,
                Reserved = 0,
                ClassGuid = classGuid,
                Name = 0
            };

            dbi.Size = Marshal.SizeOf(dbi);

            return dbi;
        }

        private static IntPtr RegisterDeviceNotification(DevBroadcastDeviceInterface dbi, IntPtr windowHandle)
        {
            var buffer = Marshal.AllocHGlobal(dbi.Size);
            IntPtr handle;

            try
            {
                Marshal.StructureToPtr(dbi, buffer, true);

                handle = RegisterDeviceNotification(windowHandle, buffer, 0);
            }
            finally
            {
                // Free buffer
                Marshal.FreeHGlobal(buffer);
            }

            return handle;
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr RegisterDeviceNotification(IntPtr recipient, IntPtr notificationFilter, int flags);

        [DllImport("user32.dll")]
        private static extern bool UnregisterDeviceNotification(IntPtr handle);

        #endregion
    }