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

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;
Advertisements

11 thoughts on “Using WMI to get a drive’s ‘friendly name’

  1. 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

    • 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.)

    • Try adding a manual declaration of ISWbemObjectSet —

      type
        ISWbemObjectSet = interface(IDispatch)
          ['{76A6415F-CB41-11D1-8B02-00600806D9B6}']
          function Get__NewEnum: IUnknown; safecall;
          function Item(const strObjectPath: WideString; iFlags: Integer): ISWbemObject; safecall;
          function Get_Count: Integer; safecall;
          function Get_Security_: ISWbemSecurity; safecall;
          function ItemIndex(lIndex: Integer): ISWbemObject; safecall;
          property _NewEnum: IUnknown read Get__NewEnum;
          property Count: Integer read Get_Count;
          property Security: ISWbemSecurity read Get_Security_;
        end;
      

      Unless you don’t use the default import at all by redefining other stuff too, you’ll need to add casts or ‘as’ queries to the example code I gave whenever a ISWbemObjectSet object is returned —

      Drives := WMIServices.ExecQuery('Select * FROM Win32_DiskDrive',
        'WQL', 0, nil) as ISWbemObjectSet;
      
  2. My version is 1.88.1.0.1.0, imported with Delphi Project/Import Type Library and the same problem: ItemIndex() doesn`t exist in ISWbemObjectSet.

    Any advice? Thanks.

      • This code works for me:

        procedure TFriendlyDriveNames.Refresh;
        var
        Ch: Char;
        Drive, Partition, LogicalDrive: ISWbemObject;
        Drives, Partitions, LogicalDrives: ISWbemObjectSet;
        I, J, K: Integer;
        S: string;

        enumDrive, enumPartition, enumLogicalDrive: IEnumVariant;
        ovVar: OleVariant;
        lwValue: LongWord;
        begin
        for Ch := Low(FNames) to High(FNames) do
        FNames[Ch] := ”;
        Drives := FWMIServices.ExecQuery(‘Select * FROM Win32_DiskDrive’,
        ‘WQL’, 0, nil) as ISWbemObjectSet;

        enumDrive := Drives._NewEnum as IEnumVariant;
        for I := 0 to Drives.Count – 1 do
        begin
        enumDrive.Next (1, ovVar, lwValue);
        Drive := IUnknown(ovVar) as SWBemObject;

        Partitions := Drive.Associators_(‘Win32_DiskDriveToDiskPartition’,
        ”, ”, ”, False, False, ”, ”, 0, nil);

        enumPartition := Partitions._NewEnum as IEnumVariant;
        for J := 0 to Partitions.Count – 1 do
        begin
        enumPartition.Next (1, ovVar, lwValue);
        Partition := IUnknown(ovVar) as SWBemObject;

        LogicalDrives := Partition.Associators_(
        ‘Win32_LogicalDiskToPartition’, ”, ”, ”, False, False, ”, ”, 0, nil);

        enumLogicalDrive := LogicalDrives._NewEnum as IEnumVariant;
        for K := 0 to LogicalDrives.Count – 1 do
        begin
        enumLogicalDrive.Next (1, ovVar, lwValue);
        LogicalDrive := IUnknown(ovVar) as SWBemObject;

        S := Variant(LogicalDrive).Name;
        if S ” then FNames[UpCase(S[1])] := Variant(Drive).Caption;
        end;
        end;
        end;
        end;

      • In what way? Possibly connected to that, is the GUID for ISWbemObjectSet in your import unit the same as mine, or is it different?

    • Sorry it’s v1.2. The code generated is this:

      *********************************************************************//
      // Interface: ISWbemObjectSet
      // Flags: (4560) Hidden Dual NonExtensible OleAutomation Dispatchable
      // GUID: {76A6415F-CB41-11D1-8B02-00600806D9B6}
      // *********************************************************************//
      ISWbemObjectSet = interface(IDispatch)
      [‘{76A6415F-CB41-11D1-8B02-00600806D9B6}’]
      function Get__NewEnum: IUnknown; safecall;
      function Item(const strObjectPath: WideString; iFlags: Integer): ISWbemObject; safecall;
      function Get_Count: Integer; safecall;
      function Get_Security_: ISWbemSecurity; safecall;
      property _NewEnum: IUnknown read Get__NewEnum;
      property Count: Integer read Get_Count;
      property Security: ISWbemSecurity read Get_Security_;
      end;

      If I add manually:

      function ItemIndex(lIndex: Integer): ISWbemObject; safecall;

      then your code crashes (Access Violation).

      • Where have you added it? Also, I take it you did try what I suggested in reply to a previous commenter? The only thing that’s missing in your definition is ItemIndex — it needs to be immediately after Get_Security.

        • I followed all the explanations in older posts. Tested thousand times.

          Anyway, my modifications works great for me. Thanks for publish your code that helps me a lot.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s