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

以前、PhotoCollectorについてちょっと書いた時に、RemovableStorageMonitor.csに少し触れた。これについて、もうちょっと技術的な話を書いておこうと思う。


※注この話はWindowsプラットフォーム限定である。あと、WindowsでもOSが違うと挙動が違う可能性がある。私はWindows XP HomeとWindows XP Professionalでしか確認していない。

.NET Framework 3.5で(USBメモリ等の)リムーバブルストレージの一覧を取得したかったら、DriveInfo.GetDrivesを使ってドライブの一覧を取得して、その中からDriveTypeがRemovableなものを選べば良い。

var drives = DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Removable);

ある瞬間のリムーバブルストレージ一覧を取得するにはこの方法でで十分だ。でも、GetDrivesで返されるドライブの一覧はリムーバブルストレージの着脱で増えたり減ったりする。リムーバブルストレージが装着された、或いは取り外されたタイミングを捉えて何か処理をするにはこのAPIは向いていない。(これは不可能というわけではなく、あくまで向いていないという話。多分GetDrivesの戻り値を定期的にチェックするスレッドを作ればできると思う。)

ただしこれは、.NET Frameworkで用意された機能としては適したものがないだけであって、次のWindowメッセージをハンドルすれば実現できる。

#define WM_DEVICECHANGE 0x0219

Windowメッセージを処理するには、Control.WndProcをオーバーライドすれば良い。WM_DEVICECHANGEを受け取れるのはトップレベルウィンドウだけである。そのため、WndProcを定義するクラスはFormのサブクラスでないといけないし、同時に別のウィンドウの子ウィンドウであってはいけない。この点は要注意。

private enum WM : uint
{
    WM_DEVICECHANGE = 0x0219,
    //...
}

protected override void WndProc(ref Message m)
{
    switch ((WM)m.Msg)
    {
        case WM.WM_DEVICECHANGE:
        HandleDeviceChangeMessage(ref m);//ここで何か処理を実行する
        break;
    }

    base.WndProc(ref m);
}

WM_DEVICECHANGEを検知したとして、それがデバイスの追加なのか削除なのか、またそれがどのドライブに対応するのかを知らないと使い物にならない。これらの情報を得るには、WndProcの引数を使う。
Message構造体にはLParamとWParamという2つのパラメータがある。

WParamの値を見るとイベントの種類がわかる。こんな感じ。

private enum DBT
{
    DBT_DEVICEARRIVAL           = 0x8000,
    DBT_DEVICEQUERYREMOVE       = 0x8001,
    DBT_DEVICEQUERYREMOVEFAILED = 0x8002,
    DBT_DEVICEREMOVEPENDING     = 0x8003,
    DBT_DEVICEREMOVECOMPLETE    = 0x8004,
}

switch ((DBT)m.WParam.ToInt32())
{
    case DBT.DBT_DEVICEARRIVAL:
        //ドライブが装着された時の処理を書く
        break;
    case DBT.DBT_DEVICEREMOVECOMPLETE:
        //ドライブが取り外されたされた時の処理を書く
        break;
}

それからLParamを調べると、ドライブレターがわかる。LParamにはDEV_BROADCAST_VOLUME構造体のアドレスが格納される。

struct DEV_BROADCAST_VOLUME
{
    public uint dbcv_size;
    public uint dbcv_devicetype;
    public uint dbcv_reserved;
    public uint dbcv_unitmask;
}

private enum DBT_DEVTP
{
    DBT_DEVTYP_OEM              = 0x0000,
    DBT_DEVTYP_DEVNODE          = 0x0001,
    DBT_DEVTYP_VOLUME           = 0x0002,
    DBT_DEVTYP_PORT             = 0x0003,
    DBT_DEVTYP_NET              = 0x0004,
    DBT_DEVTYP_DEVICEINTERFACE  = 0x0005,
    DBT_DEVTYP_HANDLE           = 0x0006,
}

char driveLetter = '\0';
var volume = (DEV_BROADCAST_VOLUME)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_VOLUME));
//volume.dbcv_devicetypeとvolume.dbcv_unitmaskを調べる

このうち、dbcv_devicetypeはデバイスの種類、dbcv_unitmaskメンバはドライブレターを表す。リムーバブルメディアの場合、dbcv_devicetypeの値はDBT_DEVTYP_VOLUME(0x0002)になる。

dbcv_unitmaskにはドライブレターをビットマスクで表した値が入る。32ビットの各ビットを次のように、順にアルファベットに割り当てる。アルファベットは26個だから、A-Zまでを表現するにはこれで足りるのだ。

  • A : 00000000000000000000000000000001
  • B : 00000000000000000000000000000010
  • C : 00000000000000000000000000000100

これでイベントの種類とドライブレターが取得できたので、後は好きなようにやれば宜しい。