Using WMI to get a drive’s ‘friendly name’

2009 July 5
by CR

There was an deceptively simple question in the Embarcadero forums recently — how can you retrieve a drive’s friendly name? I wasn’t the only person to think the questioner meant either the volume label or ‘display name’ (as the shell API calls it), the latter being whatever Windows Explorer labels the drive, which is typically the volume label followed by ‘ [drive letter](:)’. Neither was what the poster wanted however — rather, he wanted the ‘friendly name’ as retrievable from Windows’ Device Manager.

Googling, it appears the only documented (or even semi-documented!) way of getting it is via the WMI API. I say ‘the’ WMI API — in fact, there seems to be several to the extent that C++, ‘classic’ VB and Windows Scripting (VBScript/JScript), .Net and PowerShell all have their own particular interface, though the available information seems the same. Anyhow, to get going in Delphi, we need the interface designed for VB — in D2007, go to Component|Import Component…; have ‘Import type library’ selected and click Next; find ‘Microsoft WMIScripting V1.2 Library’ (the version might be slightly different on your machine); don’t generate component wrappers (there’s no need); and let the IDE just create the unit. There should now be a WbemScripting_TLB.pas created, saved to the Imports directory and so ready to be added to any given uses clause.

The next problem is figuring out how to create an instance of the main WMI object. In VBScript you can use the slightly odd-looking ‘moniker’ version of the GetObject standard function:

Dim WMIServices
Set WMIServices = GetObject("winmgmts:\\.\root\cimv2")

Unfortunately, Delphi’s CreateOleObject doesn’t support this syntax, and while there should be a way to write one that does, the following creates the needed object just as well –

uses WbemScripting_TLB;

var
  WMIServices: ISWbemServices;
begin
  WMIServices := CoSWbemLocator.Create.ConnectServer('.', 'root\cimv2',
    '', '', '', '', 0, nil);

With respect to actually using the object, the ExecQuery method may be called. This returns a further object that you can enumerate in Delphi using its Count and ItemIndex methods; to actually read any subclass-specific properties (see here for a list), cast a returned object to Variant first or assign it to a Variant variable, against which you can use late binding –

procedure ShowDriveInterfaceTypes;
var
  WMIServices: ISWbemServices;
  Drives: ISWbemObjectSet;
  Drive: Variant;
  I: Integer;
begin
  WMIServices := CoSWbemLocator.Create.ConnectServer('.', 'root\cimv2',
    '', '', '', '', 0, nil);
  Drives := WMIServices.ExecQuery('Select * FROM Win32_DiskDrive',
    'WQL', 0, nil);
  for I := 0 to Drives.Count - 1 do
  begin
    Drive := Drives.ItemIndex(I);
    ShowMessage(Drive.DeviceID + ' = ' + Drive.InterfaceType);
  end;
end;

So, how can we use this to retrieve a drive’s ‘friendly name’? An issue here is that the name concerns a physical drive whilst a drive letter concerns a logical drive — and WMI doesn’t seem to provide any way to directly get a physical drive from a logical one (this is not as ridiculous as it may seem, since a logical drive can span more than one physical drive). What we need to do, then, is to go the other way first, loading what drives we have, data that can then be accessed to map a drive letter to a physical drive. Here’s a simple class I came up with, together with a wrapper function for ‘one-off’ calls:

uses WbemScripting_TLB;

type
  TFriendlyDriveNames = class
  private
    FNames: array['A'..'Z'] of string;
    FWMIServices: ISWbemServices;
    function GetFriendlyName(DriveLetter: Char): string;
  public
    constructor Create;
    procedure Refresh;
    property FriendlyNames[DriveLetter: Char]: string read
      GetFriendlyName; default;
  end;

constructor TFriendlyDriveNames.Create;
begin
  FWMIServices := CoSWbemLocator.Create.ConnectServer('.', 'root\cimv2',
    '', '', '', '', 0, nil);
  Refresh;
end;

function TFriendlyDriveNames.GetFriendlyName(DriveLetter: Char): string;
begin
  DriveLetter := UpCase(DriveLetter);
  case DriveLetter of
    Low(FNames)..High(FNames): Result := FNames[DriveLetter];
  else
    Result := '';
  end;
end;

procedure TFriendlyDriveNames.Refresh;
var
  Ch: Char;
  Drive: ISWbemObject;
  Drives, Partitions, LogicalDrives: ISWbemObjectSet;
  I, J, K: Integer;
  S: string;
begin
  for Ch := Low(FNames) to High(FNames) do
    FNames[Ch] := '';
  Drives := FWMIServices.ExecQuery('Select * FROM Win32_DiskDrive',
    'WQL', 0, nil);
  for I := 0 to Drives.Count - 1 do
  begin
    Drive := Drives.ItemIndex(I);
    Partitions := Drive.Associators_('Win32_DiskDriveToDiskPartition',
      '', '', '', False, False, '', '', 0, nil);
    for J := 0 to Partitions.Count - 1 do
    begin
      LogicalDrives := Partitions.ItemIndex(J).Associators_(
        'Win32_LogicalDiskToPartition', '', '', '', False, False, '', '', 0, nil);
      for K := 0 to LogicalDrives.Count - 1 do
      begin
        S := Variant(LogicalDrives.ItemIndex(K)).Name;
        if S <> '' then FNames[UpCase(S[1])] := Variant(Drive).Caption;
      end;
    end;
  end;
end;

function GetFriendlyDriveName(const DriveLetter: Char): string;
begin
  with TFriendlyDriveNames.Create do
  try
    Result := FriendlyNames[DriveLetter];
  finally
    Free;
  end;
end;
2 Responses leave one →
  1. 2009 October 27
    miguel angel permalink

    Thanks for your help, but I have a problem in the next line
    Drive := Drives.ItemIndex(I);
    you use ItemIndex() property but in WbemScripting_TLB class that method doesn`t exist

    • 2009 October 27

      Are you sure – what version of the type library have you imported? On my machine it’s v1.2. (ItemIndex is a method of the ISWbemObjectSet property by the way – WbemScripting_TLB is the import unit.)

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS