C#でリムーバブルメディアの着脱を検知する方法 その2

その1ではリムーバブルメディアの着脱を検知する方法を説明した。この方法はUSBメモリなどではうまく機能するが、多数のメディアの読み書きに対応したメディアリーダ(例: google:image:USB メディア リーダー)では問題が起こるかもしれない。

例えば、私が使っているBuffaloのメディアリーダでは、リーダーをUSBポートに接続した時点で6つぐらいのドライブが認識される。同時にWindowメッセージ(WM_DEVICECHANGE)もドライブの数だけ飛ぶ。そして、実際にSDカードなどのメディアをメディアリーダーにセットした時には、何も検知できない。そういう場合にどうするかというのがここで説明する内容である。


やり方はその1の方法と似ていて、Windowメッセージを使う。今度使うのは次のメッセージ。

#define WM_SHNOTIFY 0x0401

これを受け取るのにウィンドウが必要だが、トップレベルウィンドウである必要はない。
その代わり、事前に関数SHChangeNotifyRegisterを呼んでおく必要がある。

private enum SHCNF
{
    SHCNF_IDLIST      = 0x0000,
    SHCNF_PATHA       = 0x0001,
    SHCNF_PRINTERA    = 0x0002,
    SHCNF_DWORD       = 0x0003,
    SHCNF_PATHW       = 0x0005,
    SHCNF_PRINTERW    = 0x0006,
    SHCNF_TYPE        = 0x00FF,
    SHCNF_FLUSH       = 0x1000,
    SHCNF_FLUSHNOWAIT = 0x2000,
}

private enum SHCNE : uint
{
    SHCNE_MEDIAINSERTED    = 0x00000020,
    SHCNE_MEDIAREMOVED     = 0x00000040,
    //...
}

var notifyEntry = new SHChangeNotifyEntry() { pIdl = IntPtr.Zero, Recursively = true };
var notifyId = SHChangeNotifyRegister(Handle,
                                      SHCNF.SHCNF_TYPE | SHCNF.SHCNF_IDLIST,
                                      SHCNE.SHCNE_MEDIAINSERTED | SHCNE.SHCNE_MEDIAREMOVED,
                                      (uint) WM.WM_SHNOTIFY,
                                      1,
                                      ref notifyEntry);

逆にメッセージを受け取らないようにするには、SHChangeNotifyUnregisterを呼ぶ。

SHChangeNotifyUnregister(notifyId); 

メッセージを受け取れる間は、WndProcでイベントを処理する。

private enum WM : uint
{
    WM_SHNOTIFY = 0x0401,
    //...
}

protected override void WndProc(ref Message m)
{
    switch ((WM)m.Msg)
    {
        case WM.WM_SHNOTIFY:
        //ここでイベントを処理する
        break;
    }

    base.WndProc(ref m);
}

メディアが接続されたのか、取り外されたのかは、Message構造体のLParamの値を見ればわかる。

switch ((SHCNE)m.LParam)
{
    case SHCNE.SHCNE_MEDIAINSERTED:
        //メディアがセットされた
        break;
    case SHCNE.SHCNE_MEDIAREMOVED:
        //メディアが取り外された
        break;
}

イベントが発生したドライブは、WParamから取得する。WParamにはSHNOTIFYSTRUCT構造体へのポインタが入っている。SHNOTIFYSTRUCT構造体経由でドライブのパス名(例. "D:\\")を取得する。

struct SHChangeNotifyEntry
{
    public IntPtr pIdl;
    public Boolean Recursively;
}

struct SHNOTIFYSTRUCT
{
    public uint dwItem1;
    public uint dwItem2;
} 

var shNotify = (SHNOTIFYSTRUCT)Marshal.PtrToStructure(m.WParam, typeof(SHNOTIFYSTRUCT));
var driveRootPathBuffer = new StringBuilder("A:\\");
SHGetPathFromIDList((IntPtr)shNotify.dwItem1, driveRootPathBuffer);

ここで示した方法は、RemovableStorageMonitor.csで実装されている。