Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - USB 장치의 연결 및 해제 알림을 위한 WM_DEVICECHANGE 메시지 처리

다음과 같은 질문이 있군요. ^^

MFC 에서 WM_DEVICECHANGE 메시지의 Wparam이 항상 7로 들어옵니다.
; https://social.msdn.microsoft.com/Forums/ko-KR/bd667c77-428b-439f-9cf0-9229de544b65/mfc-wmdevicechange-wparam-7-?forum=visualcplusko

편하게 C# WinForm으로 구현해 보겠습니다. 우선, 기본 상태에서 WM_DEVICECHANGE의 상태 알림을 확인해 보면,

using System;
using System.Diagnostics;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        const int WM_DEVICECHANGE = 0x0219;

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_DEVICECHANGE)
            {
                Debug.WriteLine(DateTime.Now + ": " + m.Msg + ", " + m.WParam.ToInt64() + ", " + m.LParam.ToInt64());
            }

            base.WndProc(ref m);
        }
    }
}

USB 장치를 연결 및 해제 시 다음과 같은 결과를 얻을 수 있습니다.

[연결 시]
2018-12-05 오전 8:52:35: 537, 7, 0
2018-12-05 오전 8:52:36: 537, 7, 0
2018-12-05 오전 8:52:36: 537, 7, 0

[해제 시]
2018-12-05 오전 8:52:43: 537, 7, 0
2018-12-05 오전 8:52:43: 537, 7, 0

보는 바와 같이, 연결과 해제에 따른 WPARAM의 값이 7로 동일하기 때문에 구분을 할 수 있는 기준이 없습니다. 이제 문서에 따라,

RegisterDeviceNotification function
; https://learn.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-registerdevicenotificationa

RegisterDeviceNotification을 처리해 줄 UsbAlarm이라는 타입을 다음과 같이 만들고,

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace WindowsFormsApp1
{
    public class UsbAlarm : IDisposable
    {
        [DllImport("user32.dll", SetLastError = true)]
        static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr NotificationFilter, uint Flags);

        [DllImport("user32.dll", SetLastError = true)]
        static extern bool UnregisterDeviceNotification(IntPtr Handle);

        List<IntPtr> _notifications = new List<IntPtr>();

        public const int WM_DEVICECHANGE = 0x0219;
        public const int DBT_DEVTYP_DEVICEINTERFACE = 0x05;
        public const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;

        // 연결된 장치 알아내기
        // http://bboogugu.egloos.com/v/591770
        Guid[] GUID_DEVINTERFACE_LIST =
        {
            // GUID_DEVINTERFACE_USB_DEVICE
            new Guid(0xA5DCBF10, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED),
            // GUID_DEVINTERFACE_COMPORT
            new Guid(0x86e0d1e0, 0x8089, 0x11d0, 0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73),
            // GUID_DEVINTERFACE_MODEM
            new Guid(0x2c7089aa, 0x2e0e, 0x11d1, 0xb1, 0x14, 0x00, 0xc0, 0x4f, 0xc2, 0xaa, 0xe4),
            // GUID_DEVINTERFACE_DISK
            new Guid(0x53f56307, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b),
            // GUID_DEVINTERFACE_HID, 
            new Guid(0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30),
            // GUID_NDIS_LAN_CLASS
            new Guid(0xad498944, 0x762f, 0x11d0, 0x8d, 0xcb, 0x00, 0xc0, 0x4f, 0xc3, 0x35, 0x8c),
            // GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR
            new Guid(0x4D36E978, 0xE325, 0x11CE, 0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18),
            // GUID_DEVINTERFACE_PARALLEL
            new Guid(0x97F76EF0, 0xF883, 0x11D0, 0xAF, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x84, 0x5C),
            // GUID_DEVINTERFACE_PARCLASS
            new Guid(0x811FC6A5, 0xF728, 0x11D0, 0xA5, 0x37, 0x00, 0x00, 0xF8, 0x75, 0x3E, 0xD1)
        };

        unsafe public UsbAlarm(IntPtr windowHandle)
        {
            DEV_BROADCAST_DEVICEINTERFACE filter = new DEV_BROADCAST_DEVICEINTERFACE();
            filter.dbcc_size = DEV_BROADCAST_DEVICEINTERFACE.Size;
            filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;

            foreach (Guid guid in GUID_DEVINTERFACE_LIST)
            {
                filter.dbcc_classguid = guid;

                DEV_BROADCAST_DEVICEINTERFACE* ptr = &filter;
                IntPtr ptrStruct = new IntPtr(ptr);
                IntPtr hDevNotify = RegisterDeviceNotification(windowHandle, ptrStruct, DEVICE_NOTIFY_WINDOW_HANDLE);
                if (hDevNotify == IntPtr.Zero)
                {
                    Debug.WriteLine("Failed to register: " + guid);
                }
                else
                {
                    _notifications.Add(hDevNotify);
                }
            }
        }

        public void Dispose()
        {
            foreach (IntPtr handle in _notifications)
            {
                UnregisterDeviceNotification(handle);
            }
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    struct DEV_BROADCAST_DEVICEINTERFACE
    {
        public int dbcc_size;
        public int dbcc_devicetype;
        public int dbcc_reserved;
        public Guid dbcc_classguid;
        public char dbcc_name;
        public static readonly int Size = Marshal.SizeOf(typeof(DEV_BROADCAST_DEVICEINTERFACE));
    }
}

이렇게 Form에서 사용해 봅니다.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        UsbAlarm _usbAlarm;

        public Form1()
        {
            InitializeComponent();
            _usbAlarm = new UsbAlarm(this.Handle);
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            if (_usbAlarm != null)
            {
                _usbAlarm.Dispose();
            }

            base.OnClosing(e);
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == UsbAlarm.WM_DEVICECHANGE)
            {
                Debug.WriteLine("Form1: " + DateTime.Now + ": " + m.Msg + ", " + m.WParam.ToInt64() + ", " + m.LParam.ToInt64());
            }

            base.WndProc(ref m);
        }
    }
}

다시 USB 장치를 연결 및 해제하면 이제 그 상태를 WPARAM 값을 통해 구분할 수 있습니다.

[연결 시]
Form1: 2018-12-05 오전 9:08:37: 537, 32768, 5892240
Form1: 2018-12-05 오전 9:08:37: 537, 32768, 5892240

DBT_DEVICEARRIVAL (0x8000 == 32768)

[해제 시]
Form1: 2018-12-05 오전 9:08:44: 537, 32772, 5892240
Form1: 2018-12-05 오전 9:08:44: 537, 32772, 5892240

DBT_DEVICEREMOVECOMPLETE (0x8004 == 32772)

잘 동작하는군요. ^^




다시 질문으로 돌아가서, 검색 결과 다이얼로그의 최상위에서만 정상 결과가 들어온다는 이야기를 하고 있습니다. 이게 좀 이상합니다. RegisterDeviceNotification의 첫 번째 인자가 윈도우 핸들 값인데, 윈도우 시스템이 굳이 저 윈도우 핸들의 부모 핸들을 탐색한 후 최상위 윈도우에만 알림을 전달할 것 같지는 않습니다.

실제로 테스트를 해볼까요? ^^

MainForm에 버튼을 추가하고 그에 대해 MyForm 하위 대화창을 띄워두도록 한 후,

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == UsbAlarm.WM_DEVICECHANGE)
            {
                Debug.WriteLine("Form1: " + DateTime.Now + ": " + m.Msg + ", " + m.WParam.ToInt64() + ", " + m.LParam.ToInt64());
            }

            base.WndProc(ref m);
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MyForm form = new MyForm();
            form.ShowDialog();
        }
    }
}

MyForm 코드에 UsbAlarm 코드를 구현하면,

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class MyForm : Form
    {
        UsbAlarm _usbAlarm;

        public MyForm()
        {
            InitializeComponent();
            _usbAlarm = new UsbAlarm(this.Handle);
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == UsbAlarm.WM_DEVICECHANGE)
            {
                Debug.WriteLine("MyForm: " + DateTime.Now + ": " + m.Msg + ", " + m.WParam.ToInt64() + ", " + m.LParam.ToInt64());
            }

            base.WndProc(ref m);
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            if (_usbAlarm != null)
            {
                _usbAlarm.Dispose();
            }

            base.OnClosing(e);
        }
    }
}

USB 연결/해제 시 다음과 같은 알림을 받을 수 있습니다.

[연결 시]
MyForm: 2018-12-05 오전 9:20:58: 537, 7, 0
Form1: 2018-12-05 오전 9:20:58: 537, 7, 0
MyForm: 2018-12-05 오전 9:20:58: 537, 32768, 7331776
MyForm: 2018-12-05 오전 9:20:58: 537, 32768, 7331776

[해제 시]
MyForm: 2018-12-05 오전 9:21:05: 537, 32772, 7331776
MyForm: 2018-12-05 오전 9:21:05: 537, 32772, 7331776
MyForm: 2018-12-05 오전 9:21:16: 537, 7, 0
Form1: 2018-12-05 오전 9:21:16: 537, 7, 0

정상적으로 Top-level 윈도우가 아니라 RegisterDeviceNotification에 전달한 윈도우로 이벤트가 잘 전달이 되는 것을 확인할 수 있습니다. 그런데, 왜 뜬금없이 Top-level 윈도우에서만 받을 수 있다는 이야기가 나왔을까요? 이쯤에서 다시 문서를 보면,

Applications send event notifications using the BroadcastSystemMessage function. Any application with a top-level window can receive basic notifications by processing the WM_DEVICECHANGE message. Applications can use the RegisterDeviceNotification function to register to receive device notifications.


의문이 풀립니다. 즉, Top-level 윈도우는 RegisterDeviceNotification으로 알림 등록을 하지 않아도 WM_DEVICECHANGE 이벤트를 기본적인 수준에서 받을 수 있다는 내용인데, 질문자가 검색한 글의 작성자는 이것을 잘못 해석한 내용을 써놓았던 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/22/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2022-11-29 04시00분
[StarRang] 안녕하세요.
WndProc 함수는 USB 포함 모든 Portable Device에 대해서 인식할 수 있는데,
모든 Portable Device가 아닌 모바일 기기나 특정 기기만 인식할 수 있도록 가능할까요?
[guest]
2022-11-29 04시12분
문서에도 나오지만 RegisterDeviceNotification의 두 번째 인자에 NotificationFilter를 전달하고 있습니다. 검색해 보면,

Obtaining Device Notification for USB Device Arrival and Surprise Removal for MFC
; https://community.silabs.com/s/article/Obtaining-Device-Notification-for-USB-Device-Arrival-and-Surprise-Removal-for-C-MFC

관련 예제 코드가 있으니 어렵지 않게 만드실 수 있을 것입니다. 저도 해보진 않아서 어느 정도의 필터링 기능을 하는지는 모릅니다. 해보시고 ^^ 후기 좀 남겨주세요.
정성태

... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12655정성태6/1/20217727오류 유형: 722. windbg/sos - savemodule - Fail to read memory
12654정성태5/31/20217760오류 유형: 721. Hyper-V - Saved 상태의 VM을 시작 시 오류 발생
12653정성태5/31/202110419.NET Framework: 1060. 닷넷 GC에 새롭게 구현되는 DPAD(Dynamic Promotion And Demotion for GC)
12652정성태5/31/20218534VS.NET IDE: 164. Visual Studio - Web Deploy로 Publish 시 암호창이 매번 뜨는 문제
12651정성태5/31/20218780오류 유형: 720. PostgreSQL - ERROR: 22P02: malformed array literal: "..."
12650정성태5/17/20218113기타: 82. OpenTabletDriver의 버튼에 더블 클릭을 매핑 및 게임에서의 지원 방법
12649정성태5/16/20219410.NET Framework: 1059. 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미 [1]
12648정성태5/16/20218069사물인터넷: 66. PC -> FTDI -> NodeMCU v1 ESP8266 기기를 UART 핀을 연결해 직렬 통신하는 방법파일 다운로드1
12647정성태5/15/20219325.NET Framework: 1058. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용파일 다운로드1
12646정성태5/15/20218444사물인터넷: 65. C# - Arduino IDE의 Serial Monitor 기능 구현파일 다운로드1
12645정성태5/14/20218155사물인터넷: 64. NodeMCU v1 ESP8266 - LittleFS를 이용한 와이파이 접속 정보 업데이트파일 다운로드1
12644정성태5/14/20219279오류 유형: 719. 윈도우 - 제어판의 "프로그램 및 기능" / "Windows 기능 켜기/끄기" 오류 0x800736B3
12643정성태5/14/20218480오류 유형: 718. 서버 유형의 COM+ 사용 시 0x80080005(Server execution failed) 오류 발생
12642정성태5/14/20219423오류 유형: 717. The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
12641정성태5/13/20219096디버깅 기술: 179. 윈도우용 .NET Core 3 이상에서 Windbg의 sos 사용법
12640정성태5/13/202112058오류 유형: 716. RDP 연결 - Because of a protocol error (code: 0x112f), the remote session will be disconnected. [1]
12639정성태5/12/20218894오류 유형: 715. Arduino: Open Serial Monitor - The module '...\detection.node' was compiled against a different Node.js version using NODE_MODULE_VERSION
12638정성태5/12/20219840사물인터넷: 63. NodeMCU v1 ESP8266 - 펌웨어 내 파일 시스템(SPIFFS, LittleFS) 및 EEPROM 활용
12637정성태5/10/20219458사물인터넷: 62. NodeMCU v1 ESP8266 보드의 A0 핀에 다중 아날로그 센서 연결 [1]
12636정성태5/10/20219696사물인터넷: 61. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - FSR-402 아날로그 압력 센서 연동파일 다운로드1
12635정성태5/9/20219013기타: 81. OpenTabletDriver를 (관리자 권한으로 실행하지 않고도) 관리자 권한의 프로그램에서 동작하게 만드는 방법
12634정성태5/9/20218061개발 환경 구성: 572. .NET에서의 신뢰도 등급 조정 - 외부 Manifest 파일을 두는 방법파일 다운로드1
12633정성태5/7/20219544개발 환경 구성: 571. UAC - 관리자 권한 없이 UIPI 제약을 없애는 방법
12632정성태5/7/20219754기타: 80. (WACOM도 지원하는) Tablet 공통 디바이스 드라이버 - OpenTabletDriver
12631정성태5/5/20219628사물인터넷: 60. ThingSpeak 사물인터넷 플랫폼에 ESP8266 NodeMCU v1 + 조도 센서 장비 연동파일 다운로드1
12630정성태5/5/20219943사물인터넷: 59. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - CdS Cell(GL3526) 조도 센서 연동파일 다운로드1
... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...