Notification of CD-ROM insertion and removal

by Tom Deprez - Tom.Deprez@uz.kuleuven.ac.be

I’m not a real programmer. I mean, everything I know of programming is from experimenting and reading books. But I think this can be interesting for everybody.

I often visit the Delphi site of experts exchange, there I can ask or answer a lot of Delphi questions. One day somebody asked if it was possible to get notified when a user pushed the insert/eject button on a CD-ROM drive. I found this a really interesting question and started to look for a solution. I searched a lot on the net, but nothing could still my hunger. All the detection’s were based on polling for media changes, i.e. looking for every e.g. millisecond if the media changed. Luckily I found some interesting information on the Microsoft Technical Support pages (article ID: Q163503).

Here is my solution based on the information I found in this article :

The notification message

When a compact disc is removed or inserted, then Windows 95 and Windows NT send a WM_DEVICECHANGE message to all top-level windows. This message contains an event (wParam) which describes the change and a structure (lParam) that provides detailed information about the change.

Here is the definition of WM_DEVICECHANGE: (from Win32 programmers reference)

As you can see in the definition, this message isn’t only send when a CD is inserted or removed. This message will be send whenever new devices or media are added and when existing devices or media are removed. But the purpose of this text is only to explain how to get notified when a compact disc is removed or inserted in the CD-ROM player.

Processing the wm_devicechange message (in case of a compact disc)

Now, when a new compact disc is inserted into a drive, a WM_DEVICECHANGE message is broadcasted. This message contains a DBT_DEVICEARRIVAL event.
When a compact disc is removed, the WM_DEVICECHANGE message contains the DBT_DEVICEREMOVECOMPLETE event. In this case (inserting and removing compact discs), we know that the device is of type volume (DBT_DEVTYP_VOLUME) and the event’s media flag is set (DBTF_MEDIA).

Getting to work

Great, intercepting WM_DEVICECHANGE and we’re done! Pity that all the needed constants and structures for interpreting this message aren’t declared in Delphi. So that’s the first thing we’ve got to do. Making some declarations which makes life easier. For that you can look at some libraries (in which these declarations are made) or have a look at the SDK kit of Microsoft. If you found them the only thing you still have to do is to translate them into something that Delphi understands. When you do this, always try to use the naming conventions. This makes live easier and less confusing for everybody.

Partial Translate of DBT.H

The first thing we make is a message-specific record for this windows message. We could use TMessage, but then we have to work with the ‘meaningless’ words like wParam and lParam.

TWMDeviceChange = record
   Msg : Cardinal;
   Event : UINT;
   dwData : Pointer;
   Result : LongInt;
  end;

When we got a DBT_DEVICECHANGE or DBT_DEVICEREMOVECOMPLETE event, dwData contains an address of a DEV_BROADCAST_HDR structure identifying the device inserted. We also need a pointer so we can point to this structure.

PDEV_BROADCAST_HDR = ^TDEV_BROADCAST_HDR;
 TDEV_BROADCAST_HDR = packed record
   dbch_size : DWORD;
   dbch_devicetype : DWORD;
   dbch_reserved : DWORD;
  end;

When the device is of type volume, then we can get some device specific information, namely specific information about a logical volume.

PDEV_BROADCAST_VOLUME = ^TDEV_BROADCAST_VOLUME;
  TDEV_BROADCAST_VOLUME = packed record
   dbcv_size : DWORD;
   dbcv_devicetype : DWORD;
   dbcv_reserved : DWORD;
   dbcv_unitmask : DWORD;
   dbcv_flags : WORD;
  end;

Next to this structures, necessary constants have to be declared :

(* Events of WM_DEVICECHANGE (wParam) *)

  DBT_DEVICEARRIVAL = $8000;            (* system detected a new device *)
  DBT_DEVICEQUERYREMOVE = $8001;        (* wants to remove, may fail *)
  DBT_DEVICEQUERYREMOVEFAILED = $8002;  (* removal aborted *)
  DBT_DEVICEREMOVEPENDING = $8003;      (* about to remove, still avail *)
  DBT_DEVICEREMOVECOMPLETE = $8004;     (* device is gone *)
  DBT_DEVICETYPESPECIFIC = $8005;       (* type specific event *)
  DBT_CONFIGCHANGED = $0018;

 (* type of device in DEV_BROADCAST_HDR *)
  DBT_DEVTYP_OEM = $00000000;           (* OEM- or IHV-defined *)
  DBT_DEVTYP_DEVNODE = $00000001;       (* Devnode number *)
  DBT_DEVTYP_VOLUME = $00000002;        (* Logical volume *)
  DBT_DEVTYP_PORT = $00000003;          (* Port (serial or parallel *)
  DBT_DEVTYP_NET = $00000004;           (* Network resource *)

  (* media types in DBT_DEVTYP_VOLUME *)
  DBTF_MEDIA = $0001;                (* change affects media in drive *)
  DBTF_NET = $0002;                     (* logical volume is network volume *)

Making the component

Our component needs to intercept messages. For that we’ve to create an invisible window, which is able to intercept these messages. This is done with the AllocateHWnd function. With it we’ve to send the window-procedure and in return we get the handle to this invisible window.  We may not forget to free this window when we don’t need it anymore! This can be done with the DeAllocateHWnd procedure.  The correct place for creating and destroying this invisible window would be in the respective Create and Destroy procedures of our component. For this we’ve to override these methods in the public declarations sector :

constructor TCDEvents.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FWindowHandle := AllocateHWnd(WndProc);
end;

destructor TCDEvents.Destroy;
begin
  DeallocateHWnd(FWindowHandle);
  inherited Destroy;
end;

Offcourse we must also declare a variable for storing the handle of the invisible window and our window-procedure in the private sector. This procedure will intercept all the messages, so here we can check if the message is of  WM_DEVICECHANGE. To be sure that any other will be processed, we call the DefWindowProc function if our message is different from WM_DEVICECHANGE.

procedure TCDEvents.WndProc(var Msg: TMessage);
begin
     if (Msg.Msg = WM_DEVICECHANGE) then
      try
        WMDeviceChange(TWMDeviceChange(Msg));
      except
        Application.HandleException(Self);
      end
    else
      Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg, Msg.wParam, Msg.lParam);
end;

As you can see, we check if we received a WM_DEVICECHANGE message. If not, the procedure send the message to the default window procedure. Otherwise we call another procedure (WMDeviceChange) for further processing of the message. While sending to WMDeviceChange the Msg of type TMessage will be transformed to our self defined tWMDeviceChange type.

procedure TCDEvents.WMDeviceChange(var Msg : TWMDeviceChange);
var lpdb : PDEV_BROADCAST_HDR;
       lpdbv : PDEV_BROADCAST_VOLUME;
begin
 (* received a wm_devicechange message *)
  lpdb := PDEV_BROADCAST_HDR(Msg.dwData);
  (* look at the event send together with the wm_devicechange message *)
   case Msg.Event of
    DBT_DEVICEARRIVAL : begin
      if lpdb^.dbch_devicetype = DBT_DEVTYP_VOLUME then begin
       lpdbv := PDEV_BROADCAST_VOLUME(Msg.dwData);
       if (lpdbv^.dbcv_flags and DBTF_MEDIA) = 1 then
        if Assigned(fAfterArrival) then
         fAfterArrival(Self, GetFirstDriveLetter(lpdbv^.dbcv_unitmask));
      end;
     end;
    DBT_DEVICEREMOVECOMPLETE : begin
      if lpdb^.dbch_devicetype = DBT_DEVTYP_VOLUME then begin
       lpdbv := PDEV_BROADCAST_VOLUME(Msg.dwData);
       if (lpdbv^.dbcv_flags and DBTF_MEDIA) = 1 then
        if Assigned(fAfterArrival) then
         fAfterRemove(Self, GetFirstDriveLetter(lpdbv^.dbcv_unitmask));
      end;
     end;
   end;
 end;

In this procedure we check the message thoroughly. We first look at the event send with the WM_DEVICECHANGE message. Next, we must check if the message is send by a CD-ROM drive. For this we check if the sending device is of type volume (DBT_DEVTYP_VOLUME). This specific information we can find in the record to which the address (dwData) points stored in the message record (TWMDeviceChange).

Then we’ve to look from which device the message is send, this we do by looking at the record through the ‘eyes’ of the TDEV_BROADCAST_HDR type. The dbch_devicetype field defines the type. In our case, we only want to be notified when a CD is removed or ejected from the CD-ROM player. So the device has to be a logical volume (DBT_DEVTYP_VOLUME).

Now, we know from which device the message comes and we can look at the record with the correct ‘glasses’, namely TDEV_BROADCAST_VOLUME. This record especially defined for logical volumes, gives us extra information about this sort of device.
If the device change message comes from  a logical volume, we still have to check if the change was owing to the CD inside the CD-ROM drive or the CD-ROM drive itself. For knowing this, we’ve to check if the media flag is set in the dbcv_flags field. If so, the message is owed by a change in CD.

With the dbcv_unitmask field we can find the valid drive letters, from which the message comes. The function below, returns the first valid drive letter. A valid drive letter is defined when the corresponding bit is set to 1 in the mask of drive letters, namely dbcv_unitmask field.

function TCDEvents.GetFirstDriveLetter(unitmask : longint):char;
var DriveLetter : shortint;
begin
 DriveLetter := Ord('A');
 while (unitmask and 1)=0  do begin
  unitmask := unitmask shr 1;
  inc(DriveLetter);
 end;
 Result := Char(DriveLetter);
end;

Well,  if the message cames from a CD, we can send the events. And that’s it, not much as you can see and it isn’t really that hard to accomplish.
I hope that you liked this article and also I hope you give me some comments about it. Just send it to my e-mail address. You can also look at the source code, included with this article. Please, read the header in the .pas file. That’s all that I ask from you, I don’t think this is much asked.

If you are interested in the source code for this project, you can download it by clicking here.