Wechseldatenträger automatisch erkennen

Schnittstellen wie USB oder Firewire ermöglichen es während des laufenden Betriebs Geräte im System hinzuzufügen oder zu entfernen. Hier stelle ich eine verwaltete Komponente vor, mit der sich Hardwareänderungen in Windows Forms Anwendungen verfolgen lassen - um beispielsweise Aktualisierungen der Benutzeroberfläche zu veranlassen. Dabei kommen spezielle Methoden des Windows API zum Einsatz.

Um die gewünschte Funktionalität zu erreichen, werden lediglich die Methoden RegisterDeviceNotification()und UnregisterDeviceNotification() benötigt - wie in der MSDN im Abschnitt Device Management beschrieben. Die notwendigen Deklarationen für Konstenten sind in folgenden Header-Dateien zu finden: dbt.h, devguid.hund winuser.h. Daraus ergeben sich die Deklarationen aus Listing 1.

Um den Beispielcode an dieser Stelle nicht zu umfangreich werden zu lassen, sind hier nur die Konstanten und Strukturen zu sehen, die für USB-Geräte zutreffend sind. Als ich das Beispiel geschrieben habe, ging es mir nur um die Verwendung von USB-Storage Devices - kann also nicht genau sagen, ob der Code auch einwandfrei mit anderen Gerätetypen funktioniert.

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

[DllImport("user32.dll", EntryPoint="UnregisterDeviceNotification")]
  public static extern Boolean UnregisterDeviceNotification(
  ref IntPtr hRecipient);

public const int WM_DEVICECHANGE = 0x0219;

public const int BROADCAST_QUERY_DENY = 0x424D5144;

public const int DBT_DEVICEARRIVAL = 0x8000; // system detected a new device
public const int DBT_DEVICEQUERYREMOVE = 0x8001; // wants to remove, may fail
public const int DBT_DEVICEQUERYREMOVEFAILED = 0x8002; // removal aborted
public const int DBT_DEVICEREMOVEPENDING = 0x8003; // about to remove, still available
public const int DBT_DEVICEREMOVECOMPLETE = 0x8004; // device is gone
public const int DBT_DEVICETYPESPECIFIC = 0x8005; // type specific event
public const int DBT_CUSTOMEVENT = 0x8006;
public const int DBT_DEVNODES_CHANGED = 0x0007;
public const int DBT_CONFIGCHANGECANCELED = 0x0019;
public const int DBT_CONFIGCHANGED = 0x0018;
public const int DBT_QUERYCHANGECONFIG = 0x0017;

public const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005;

public const int DBTF_MEDIA = 0x0001;

public static Guid GUID_DEVCLASS_VOLUME;

[StructLayout(LayoutKind.Sequential)]
public struct DEV_BROADCAST_DEVICEINTERFACE
{ 
  public int dbcc_size;
  public int dbcc_devicetype;
  public int dbcc_reserved;
  public Guid dbcc_classguid; // GUID
  public char dbcc_name1; // tchar[1]
  public char dbcc_name2;
}

static DeviceBroadcast()
{
  GUID_DEVCLASS_VOLUME = new Guid(
    0x71a27cdd, 0x812a, 0x11d0,
    0xbe, 0xc7, 0x08, 0x00, 0x2b, 0xe2, 0x09, 0x2f );
}

Einsatz von Unmanged Code

Um das API in einfacher Weise in Windows Forms Anwendungen nutzen zu können, muss zuerst ein Wrapper geschaffen werden. Einen Wrapper kann man sich als eine Art Hülle vorstellen, die speziellen Infrastrukturcode verdeckt und mit wenigen, leicht anzuwendenden Methoden und Eigenschaften zugänglich macht. Die Aufgabe des benötigten Wrappers ist es für ein Fenster (in der Regel das Hauptfenster der Anwendung) für den Empfang von Benachrichtigungen (Notifications) zu registrieren, empfangene Benachrichtigungen auszuwerten und diese der Anwendung durch die Verwendung von Events zur Verfügung zu stellen. Entwickler, die die Klasse verwenden werden, müssen das durch den Wrapper verdeckte API nicht mehr kennen.

Auswerten von Benachrichtigungen

Im ersten Schritt erzeuge ich die Klasse DeviceNotificationWindow, welche von NativeWindow erbt und die WndProc()-Methode überschreibt. Ein beliebiges Fenster der Anwendung, das mittels RegisterDeviceNotification()für den Empfang von Benachrichtigungen registriert wurde, wird an eine Instanz von DeviceNotificationWindow gebunden. So erhält man Zugriff auf die Meldungsschleife des Fensters und kann Benachrichtigungen auswerten.

internal delegate void DeviceChange(ref Message m);

internal class DeviceNotificationWindow : NativeWindow
{
  private readonly DeviceChange _devChange;

  public DeviceNotificationWindow(IntPtr handle, DeviceChange callback)
  { 
    this._devChange = callback;
    this.AssignHandle( handle );
  }

  protected override void WndProc(ref Message m)
  { 
    bool doDefault = true;
    switch (m.Msg)
    { 
      case DeviceBroadcast.WM_DEVICECHANGE:
      { 
        InvokeDeviceChange(ref m);
        m.Result = new IntPtr(1);
        break;
      }
    }
    
    if (doDefault)
      base.WndProc (ref m);

  }
  
  private void InvokeDeviceChange(ref Message m)
  {
    DeviceChange callback = this._devChange;
    if (callback != null)
      callback(ref m);
  }
}

Die Klasse DeviceNotification

In die Klasse DeviceNotification implementiere ich nun die Funktionen für den direkten Einsatz des API. Die Methode Register() nimmt das Handle eines Fensters entgegen, das an eine neue Instanz von DeviceNotificationWindowgebunden wird, den Typ der Benachrichtigung sowie den Gerätetyp und führt mit diesen Informationen die Registrierung für den Empfang der Benachrichtigungen durch. Die Methode DeviceChange_Callback() dient, wie der Name bereits vermuten lässt, als Callback , der die Auswertung der Benachrichtigungen vornimmt.

public void Register(IntPtr handle, NotifyType flags, DeviceType devtype)
{ 
  DEV_BROADCAST_DEVICEINTERFACE dbci;
  if ((m_devWindow = new DeviceNotificationWindow(handle, new DeviceChange(this.DeviceChange_Callback))) != null)
  { 
    dbci = new DEV_BROADCAST_DEVICEINTERFACE();
    dbci.dbcc_size = Marshal.SizeOf(dbci);
    dbci.dbcc_devicetype = (int) devtype;
    dbci.dbcc_classguid = DeviceBroadcast.GUID_DEVCLASS_VOLUME;
    dbci.dbcc_reserved = 0;

    m_devNotifyHandle = DeviceBroadcast.RegisterDeviceNotification(
    m_devWindow.Handle, ref dbci, (int) flags);
  }
}

private void DeviceChange_Callback(ref Message m)
{
  DEV_BROADCAST_HDR dbch;
  switch (m.WParam.ToInt32())
  {
    case DeviceBroadcast.DBT_DEVICEARRIVAL:
    {
      dbch = (DEV_BROADCAST_HDR) m.GetLParam(typeof(DEV_BROADCAST_HDR));
      switch (dbch.dbch_devicetype)
      {
        case DeviceBroadcast.DBT_DEVTYP_VOLUME:
        {
          DEV_BROADCAST_VOLUME dbchv = (DEV_BROADCAST_VOLUME) m.GetLParam(typeof(DEV_BROADCAST_VOLUME));
          if ((dbchv.dbcv_flags & DeviceBroadcast.DBTF_MEDIA) == DeviceBroadcast.DBTF_MEDIA)
          { 
            string driveLetter = DeviceBroadcast.FirstDriveFromMask(dbchv.dbcv_unitmask).ToString();
            var e = new DeviceChangeEventArgs(0, DeviceChangeAction.MediaAdd, driveLetter, true, "Media has arrived.");
            OnDeviceChange(this, e);
          }
          break;
        }
      }

      break;
    }

    default:
  //  weitere Auswertungen im Beispielprojekt
      break;

  }
}

Laufwerksbuchstaben ermitteln

Mit Hilfe der Funktion FirstDriveFromMask(), die ich in einem C++ Beispiel in der MSDN gefunden habe, lassen sich bequem Laufwerksbuchstaben ermitteln. Eine C#-Variante möchte ich natürlich nicht vorenthalten...

public static char FirstDriveFromMask(int unitmask)
{ 
  int i = 0;
  for (i = 0; i < 26; ++i)
  {
    if ((unitmask & 0x1) == 0x1) break;
      unitmask = unitmask >> 1;
  }
  
  return (char) (Convert.ToChar(i) + 'A');
}

Einsatz der Komponente in einer Anwendung

Mit wenigen Zeilen Code können die Informationen nun in die Anwendung eingebracht und verarbeitet werden. Dazu sind keine Kenntnisse des API mehr notwendig. Sieht doch recht freundlich aus, oder?

private void Form1_Load(object sender, System.EventArgs e)
{
  m_devNotify.DeviceChange += new DeviceChangeHandler(this.DeviceChange);
  m_devNotify.Register(this.Handle,
  DeviceNotification.NotifyType.WindowHandle,
  DeviceNotification.DeviceType.Interface);
}

private void DeviceChange(object sender, DeviceChangeEventArgs e)
{
  switch (e.Action)
  {
    case DeviceChangeAction.DeviceAdd:
    {
      Console.WriteLine("Laufwerk {0}: wurde hinzugefügt und kann jetzt verwendet werden.", e.DriveLetter);
      break;
    }

    case DeviceChangeAction.DeviceRemove:
    { 
      Console.WriteLine("Laufwerk {0}: wurde erfolgreich entfernt.", e.DriveLetter);
      break;
    }
  }
}

Links

MSDN: Device Management Reference
http://msdn.microsoft.com/en-us/library/aa363234(VS.85).aspx

Sample: Visual C# Beispielprojekt
Downloads/devnotify.zip
Visual Studio 2005 Solution