Функции, которые осуществляют работу с устройствами, находятся в
системных библиотеках cfgmgr32.dll и setupapi.dll. К сожалению, в
стандартных заголовочных файлах Delphi нет объявлений функций, констант и
структур которые используются этими библиотеками. Эти заголовочные
файлы можно скачать с сайта проекта Delphi-JEDI. Те, кому не нравятся
модули от проекта Delphi-JEDI могут воспользоваться моим модулем
setupapi.pas, но в нём далеко не полный список функции и структур.
Получение списка устройствПервая задача, с которой мы
столкнёмся это получение списка устройств. Устройства в системе
подразделяются на классы, например: класс видеоустройств, принтеров,
модемы, клавиатуры и т.д. Любое устройство должно принадлежать
как-нибудь классу. Каждый класс идентифицируется своим GUID’ом
(глобальный уникальный идентификатор). GUID это 128 битная запись типа:
{C06136A2-43EA-4F43-AF06-7413D07E28B7}. Для получения полного списка
устройств сначала надо получить список классов. Для получения списка
классов используется функция CM_Enumerate_Classes:
CMAPI CONFIGRET WINAPI CM_Enumerate_Classes( IN ULONG ulClassIndex,// индекс класса OUT LPGUID ClassGuid,// указатель GUID класса IN ULONG ulFlags //не используется ); Для
перечисления всех классов мы должны в цикле вызывать функцию, начиная с
индекса 0. Если функция вернула значение CR_NO_SUCH_VALUE, значит, мы
пришли к концу списка. Вторым параметром должен быть указатель на
переменную TGUID, в которую будет сохранён GUID класса. Получение
информации о классе осуществляет функция SetupDiGetClassDescription:
WINSETUPAPI BOOL WINAPI SetupDiGetClassDescription( IN LPGUID ClassGuid,//GUID класса OUT PTSTR ClassDescription,//строка IN DWORD ClassDescriptionSize,//размер строки OUT PDWORD RequiredSize OPTIONAL//требуемый размер ); Вторым
параметром должен идти указатель на буфер, в который будет сохранена
строка с именем класса. Третьим параметром должен идти размер
передаваемого буфера. Если указанного буфера не хватит, то требуемый
размер будет сохранён в переменной указатель, на которую мы передадим
четвёртым параметром. Список классов мы получили. Теперь нам надо
получить список устройств, принадлежащих некоторому классу. Здесь к нам
придёт на помощь функция SetupDiGetClassDevs: HDEVINFO SetupDiGetClassDevs( IN LPGUID ClassGuid, OPTIONAL IN PCTSTR Enumerator, OPTIONAL IN HWND hwndParent, OPTIONAL IN DWORD Flags ); У
этой функции почти все параметры опциональны за исключением последнего.
Первый параметр задаёт класс устройств для перечисления. Если этот
параметр равен нулю, то перечисляться будут все устройства в системе.
Второй и третий параметры (соответственно, имя PnP перечислителя и хендл
формы) могут быть равны нулю. Последний параметр самый важный. Он может
принимать одно из следующий значений или их комбинацию:
- DIGCF_ALLCLASSES
Будет
возвращён список всех устройств и всех классов, установленных в данный
момент в системе. Первый параметр будет проигнорирован. - DIGCF_DEVICEINTERFACE
Возврат
списка устройств, которые поддерживают интерфейсы. - DIGCF_DEFAULT
Возврат
списка устройств, которые ассоциируются с системой по умолчанию. - DIGCF_PRESENT
Будет
возвращён список устройств, которые в настоящее время присутствуют в
системе. - DIGCF_PROFILE
Будет возвращён список устройств,
которые являются частью текущего аппаратного профиля.
В
нашем случае надо указать только класс устройств и указать последним
параметром флаг DIGCF_PRESENT. При успешном вызове функция возвращает
хендл полученного списка. Итак, у нас есть список (вернее его
хендл) и нам надо как-то перечислить все устройства находящиеся в нём.
На помощь к нам придёт функция под названием SetupDiEnumDeviceInfo: WINSETUPAPI BOOL WINAPI SetupDiEnumDeviceInfo( IN HDEVINFO DeviceInfoSet, IN DWORD MemberIndex, OUT PSP_DEVINFO_DATA DeviceInfoData ); С
первых параметром, я думаю, всё ясно. Второй парметр задаёт индекс в
списке. Третий параметр это указатель на структуру SP_DEVINFO_DATA, в
которой будет сохранена информация об устройстве. Если функция вернула
значение TRUE, то информация извлечена успешно, а если FALSE, то в
большинстве случаев это означает что мы пришли к концу списка. Для
перечисления всего списка нам надо будет в цикле вызывать функцию
SetupDiEnumDeviceInfo каждый раз увеличивая значение индекса на единицу
до тех пор пока не получим отрицательный результат. Итак, у нас есть
структура, в которой хранится информация об устройстве:
typedef struct _SP_DEVINFO_DATA { DWORD cbSize; GUID ClassGuid; DWORD DevInst; ULONG_PTR Reserved; } SP_DEVINFO_DATA, *PSP_DEVINFO_DATA; По
сути, главным полем здесь является поле DevInst, которая и хранит хендл
устройства. Для того чтобы получить имя устройства (или его описание)
нам надо использовать функцию SetupDiGetDeviceRegistryProperty. Далее её
описание
WINSETUPAPI BOOL WINAPI SetupDiGetDeviceRegistryProperty( IN HDEVINFO DeviceInfoSet, IN PSP_DEVINFO_DATA DeviceInfoData, IN DWORD Property, OUT PDWORD PropertyRegDataType, OPTIONAL OUT PBYTE PropertyBuffer, IN DWORD PropertyBufferSize, OUT PDWORD RequiredSize OPTIONAL ); Второй
параметр это указатель на структуру SP_DEVINFO_DATA. Третий параметр
задаёт тип информации, которую мы хотим получить. Для нас важны два
флага: SPDRP_FRIENDLYNAME и SPDRP_DEVICEDESC. Далее идёт опциональный
параметр который задаёт указатель на переменную в которой будет сохранён
тип данных ключа реестра, из которого была извлечена информация. Далее
идёт ещё три параметра которые задают соответственно указатель на буфер
для сохранения информации, размер буфера и размер реально скопированных
данных в ненр. Если мы будем использовать флаг SPDRP_FRIENDLYNAME, то
получим вместо модели жёсткого диска «дисковый накопитель», а при
использовании флага SPDRP_DEVICEDESC мы получим модель жёсткого диска.
Не всегда информация для обоих параметров представлена, иногда есть
только для SPDRP_FRIENDLYNAME, а иногда есть только для
SPDRP_DEVICEDESC. Если при использовании первого флага мы получили
пустую строку, то надо получить информацию с использованием второго
флага. Следующая функция получает имя устройства по хендлу
перечисления и структуре SP_DEVINFO_DATA. function GetDeviceName(PnPHandle: HDEVINFO; const DevData: TSPDevInfoData): string; var BytesReturned: DWORD; RegDataType: DWORD; Buffer: array [0..256] of CHAR; begin BytesReturned := 0; RegDataType := 0; Buffer[0] := #0; SetupDiGetDeviceRegistryProperty(PnPHandle, DevData, SPDRP_FRIENDLYNAME, RegDataType, PByte(@Buffer[0]), SizeOf(Buffer), BytesReturned); Result := Buffer; if Result<>” then exit; BytesReturned := 0; RegDataType := 0; Buffer[0] := #0; SetupDiGetDeviceRegistryProperty(PnPHandle, DevData, SPDRP_DEVICEDESC, RegDataType, PByte(@Buffer[0]), SizeOf(Buffer), BytesReturned); Result:=Buffer; end; В
итоге у нас вырисовывается функция, которая получает список устройств
по заданному GUID’у класса. procedure TForm1.AddDevices(aNode: TTreeNode; aGUID: TGUID); var PnPHandle: HDEVINFO; DevData: TSPDevInfoData; RES: LongBool; Devn: Integer; _DN,_PN:ULONG; begin PnPHandle := SetupDiGetClassDevs(@aGUID, nil, 0, DIGCF_PRESENT); if PnPHandle = INVALID_HANDLE_VALUE then Exit; Devn := 0; repeat DevData.cbSize := SizeOf(DevData); RES := SetupDiEnumDeviceInfo(PnPHandle, Devn, DevData); if (RES) and (_DN<>DN_ROOT_ENUMERATED) then begin DeviceTreeView.Items.AddChild(aNode, GetDeviceName(PnPHandle, DevData)); Inc(Devn); end; if Devn=0 then begin DeviceTreeView.Items.Delete(aNode); break; end; until not RES; SetupDiDestroyDeviceInfoList(PnPHandle); end; Данная
функция выводит список устройств заданного класса в компонент TreeView.
Узел дерева TreeView задаётся первым параметром. Теперь мы можем
написать функцию которая и произведёт вывод сего списка устройств в
компонент TreeView. Вот она: procedure TForm1.AddAllDevices; var _i:DWORD; Res:CONFIGRET; GUID: PGUID; Buffer: array [0..1023] of CHAR; BufSize: DWORD; Node:TTreeNode; begin DeviceClassesList:=TStringList.Create; _i:=0; repeat GetMem(GUID, SizeOf(TGUID)); Res := CM_Enumerate_Classes(_i, GUID^, 0); if Res <> CR_NO_SUCH_VALUE then begin SetupDiGetClassDescription(GUID^, @Buffer[0], Length(Buffer), BufSize); DeviceClassesList.AddObject(Pchar(@Buffer[0]), TObject(GUID)); end; Inc(_i); until Res = CR_NO_SUCH_VALUE; for _i:=0 to DeviceClassesList.Count-1 do begin Node:=DeviceTreeView.Items.AddChild(nil,DeviceClassesList.Strings[_i]); GUID := PGUID(DeviceClassesList.Objects[_i]); AddDevices(Node,GUID^); end; end; Сначала
формируется список строк с имена классов и указателей на их GUID’ы.
Потом производится вызов предыдущей функции для каждого класса.
Включение и отключение устройствСостоянием
устройства управляет функция SetupDiSetClassInstallParams. Её описание: WINSETUPAPI BOOL WINAPI SetupDiSetClassInstallParams( IN HDEVINFO DeviceInfoSet, IN PSP_DEVINFO_DATA DeviceInfoData, OPTIONAL IN PSP_CLASSINSTALL_HEADER ClassInstallParams, OPTIONAL IN DWORD ClassInstallParamsSize ); С
первыми двумя параметрами я думаю всё ясно. Третий параметр задаёт
указатель на структуру SP_CLASSINSTALL_HEADER. Четвёртый параметр задаёт
размер третьего параметра. С помощью этой функции можно производить
различные действия с устройствами и, разумеется, для каждого действия
используются различные структуры. Но у каждой из структур первая
составляющая одинаковая – структура SP_CLASSINSTALL_HEADER, вот она: typedef struct _SP_CLASSINSTALL_HEADER { DWORD cbSize; DI_FUNCTION InstallFunction; } SP_CLASSINSTALL_HEADER, *PSP_CLASSINSTALL_HEADER;
Поле
InstallFunction задаёт производимую над устройством операцию. Для
включения/отключения это поле будет равно константе DIF_PROPERTYCHANGE.
Для включения/отключения устройства используется следующая структура: typedef struct _SP_PROPCHANGE_PARAMS { SP_CLASSINSTALL_HEADER ClassInstallHeader; DWORD StateChange; DWORD Scope; DWORD HwProfile; } SP_PROPCHANGE_PARAMS, *PSP_PROPCHANGE_PARAMS; Если
поле StateChange будет равно DICS_ENABLE, то устройство будет включено
иначе DICS_DISABLE. Если поле Scope равно DICS_FLAG_GLOBAL, то изменения
вступят в силу для всех аппаратных профилей, если
DICS_FLAG_CONFIGSPECIFIC, то изменения вступят в силу только для
указанного аппаратного профиля. Поле HwProfile задаёт ID аппаратного
профиля, к которому будут применяться изменения, если он равен нулю, то
текущий аппаратный профиль. Все параметры нуждаются в «утверждении»
перед любыми изменениями. Поэтому функцию надо вызывать два раза. Если
после первого вызова функция возвратила истинное значение, значит можно
вызывать функцию второй раз. После изменения состояния устройства
надо вызвать установщик класса, т.к. после изменения состояния
устройства может потребоваться перезагрузка системы или другие действия,
для того чтобы изменения вступили в силу. Это осуществляется функцией
SetupDiCallClassInstaller WINSETUPAPI BOOL WINAPI SetupDiCallClassInstaller( IN DI_FUNCTION InstallFunction, IN HDEVINFO DeviceInfoSet, IN PSP_DEVINFO_DATA DeviceInfoData OPTIONAL ); Первый
параметр задаёт код произведённой операции. Два остальных параметра я
думаю, проблем не вызовут. В качестве примера можно привести код
включения и отключения сетевого подключения. Для того чтобы
включить/выключить сетевое подключение достаточно включить/выключить
сетевое устройство, через которое осуществляется сетевое подключение.
Это производит следующая функция: procedure EnableNetDevice(aState:boolean;index:integer); var NetPnPHandle:HDEVINFO; PCHP:TSPPropChangeParams; DeviceData:TSPDevInfoData; begin NetPnPHandle:=SetupDiGetClassDevs(@GUID_DEVCLASS_NET, 0, 0, DIGCF_PRESENT); if NetPnPHandle=INVALID_HANDLE_VALUE then exit; DeviceData.cbSize:=sizeof(TSPDevInfoData); SetupDiEnumDeviceInfo(NetPnPHandle, index, DeviceData); PCHP.ClassInstallHeader.cbSize:=sizeof(TSPClassInstallHeader); if SetupDiSetClassInstallParams(NetPnPHandle,@DeviceData,@PCHP,sizeof(TSPPropChangeParams)) then begin PCHP.ClassInstallHeader.cbSize := sizeof(TSPClassInstallHeader); PCHP.ClassInstallHeader.InstallFunction := DIF_PROPERTYCHANGE; PCHP.Scope := DICS_FLAG_CONFIGSPECIFIC; if aState then PCHP.StateChange := DICS_ENABLE else PCHP.StateChange := DICS_DISABLE; SetupDiSetClassInstallParams(NetPnPHandle,@DeviceData,@PCHP,sizeof(TSPPropChangeParams)); SetupDiCallClassInstaller(DIF_PROPERTYCHANGE,NetPnPHandle,@DeviceData); end; DeviceData.cbSize := sizeof(TSPDevInfoData); SetupDiDestroyDeviceInfoList(NetPnPHandle); end; Параметр
index задаёт индекс сетевого устройства в списке сетевых устройств.
Безопасное извлечение устройстваИтак, с
включением/отключением устройств мы разобрались. А как безопасно
извлекать устройство? Безопасное извлечение устройства осуществляет
функция CM_Request_Device_Eject. Вот её описание: CMAPI CONFIGRET WINAPI CM_Request_Device_Eject( IN DEVINST dnDevInst, OUT PPNP_VETO_TYPE pVetoType, OUT LPTSTR pszVetoName, IN ULONG ulNameLength, IN ULONG ulFlags ); Первый
параметр это хендл устройства. Второй параметр это указатель на
переменную, в которую будет сохранён код причины при неудаче. Третий
параметр это указатель на строку, в которую будет сохранена причина
неудачи при неудачном вызове. Оба этих параметра опциональны и могут
быть равны нулю. Пятый параметр это максимальная длина строки. Шестой не
используется. Если pszVetoName равен нулю, то при неудаче сообщение
выведет сама система. Вот и сама функция, которая осуществляет
безопасное извлечение устройства. procedure RemoveDrive(index:integer); var DrivesPnPHandle: HDEVINFO; DevInfo: SP_DEVINFO_DATA; i: Integer; Parent: DWORD; VetoName:array[0..MAX_PATH] of char; begin DevInfo.cbSize := sizeof(SP_DEVINFO_DATA); DrivesPnPHandle := SetupDiGetClassDevsA(@GUID_DEVCLASS_DISKDRIVE, nil, 0, 2); if DrivesPnPHandle = INVALID_HANDLE_VALUE then exit; if SetupDiEnumDeviceInfo(DrivesPnPHandle, index, DevInfo) then begin if (IsUSBDevice(DevInfo.DevInst)) and (CM_Get_Parent(Parent, DevInfo.DevInst, 0) = CR_SUCCESS) then begin CM_Request_Device_Eject(Parent, nil, @VetoName, MAX_PATH, 0); if VetoName=” then ShowMessage(’устройство можно извлечь’) else ShowMessage(’устройство нельзя извлечь’); end else ShowMessage(’Не USB устройство’); end; SetupDiDestroyDeviceInfoList(DrivesPnPHandle); end; Единственный
параметр передаваемой функции это индекс дискового устройства в списке
дисковых устройств. Единственное что модет быть здесь непонятно - это
функция CM_Get_Parent. Она получает родитель устройства. Ведь любая
«флешка» или внешний дисковый накопитель это составное устройство и
отключать надо именно родительское устройство. Код функции IsUSBDevice
есть в исходнике, который прилагается к статье.
Отслеживание изменений в аппаратной конфигурацииКаждый раз,
когда происходят какие-либо изменения в аппаратном профиле, главному
окну приложения посылается сообщение WM_DEVICECHANGE.При получении этого
сообщения WParam содержит код события. Нас интересуют только три кода:
DBT_DEVICEARRIVAL, DBT_DEVICEREMOVECOMPLETE и DBT_DEVNODES_CHANGED. Событие
DBT_DEVNODES_CHANGED обозначает, что произошли изменения в аппаратном
профиле. LParam в данном случае равен нулю. События DBT_DEVICEARRIVAL и
DBT_DEVICEREMOVECOMPLETE идентичны и различаются тем, что первое событие
обозначает присоединение устройства и второе отсоединение устройства.
LParam отличен от нуля при этих событиях и указывает на структуру
DEV_BROADCAST_HDR. В зависимости от поля dbch_devicetype в жтой
структуре дальнейшие поля могут варьироваться. Например, если
dbch_devicetype равен DBT_DEVTYP_VOLUME, то LParam в этом случае
указывает на структуру DEV_BROADCAST_VOLUME и поле dbcv_unitmask в этой
структуре содержит битовую маску новых дисков. (нулевой бит обозначает
букву А, второй букву B, третий букву C и так далее). Приводить
код обработчика этого сообщения не имеет смысла, он содержится в
исходнике, который прилагается к статье. Для того чтобы
«подписаться» на сообщения системы надо вызвать функцию
RegisterDeviceNotification: HDEVNOTIFY WINAPI RegisterDeviceNotification( __in HANDLE hRecipient, __in LPVOID NotificationFilter, __in DWORD Flags ); Первый
параметр это хендл статуса сервиса либо хендл формы. Параметр
NotificationFilter является указателем на структуру DEV_BROADCAST_HDR и
задаёт тип устройств для отслеживания. Для отслеживания всех устройств
поле dbch_devicetype должно быть равно значению
DBT_DEVTYP_DEVICEINTERFACE. Для получения изменений всех классов
устройств. Если третий параметр равен DEVICE_NOTIFY_WINDOW_HANDLE, то
первый параметр должен быть хендлом окна, если
DEVICE_NOTIFY_SERVICE_HANDLE, то первый параметр это хендл статуса
сервиса. Также для получения сообщений об изменении всех классов
устройств этот параметр должен включать
флагDEVICE_NOTIFY_ALL_INTERFACE_CLASSES.
Вот и конец этой статьи.
К статье прилагается исходник на Delphi с примером получения списка
устройств, примером отключения устройства, безопасным отключением
устройства, а также ведением лога изменений в аппаратном профиле. Автор:
rpy3uH, pblog.ru
Исходный код примеров
Источник: http://www.cyberguru.ru/programming/delphi/windows-devices-page4.html |