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