
Задачи работы с железом – частая штука для Delphi разработчиков. Но если почти все знают, как работать с COM-портом – открывать его, отправлять и принимать данные, то практически нигде не описан наиболее корректный вариант получения списка портов. Давайте, для начала рассмотрим несколько наиболее часто встречаемые в интернете варианты и попробуем разобраться в них.
Прежде всего определимся, что среди COM-портов в Windows есть 2 типа устройств – непосредственно COM-порты и модемы:

Их следует разделять, например, когда надо автоматически подключится к какому-то определённому устройству или не выводить модемы. Поэтому было бы неплохо получить не просто список, но и типы устройств и их имена.
Вариант 1. Опрос всех портов по очереди
Решение «в лоб», что называется. Давайте будем открывать по очереди список COM-порты, а какие откроются – выведем в списке. Выглядит как-то так:
procedure TForm1.EnumPorts1();
var i: integer;
h: THandle;
begin
for i:=1 to 32 do
begin
h:=CreateFile(PChar('COM'+IntToStr(i)),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
If(h<>INVALID_HANDLE_VALUE)then
ComPort.Items.Add('COM'+IntToStr(i));
CloseHandle(h);
end;
end;
Честно говоря, вариант хреноватенький. Из плюсов – разве что очевидная реализация и возможность получить список COM-портов без прав администратора. Минусов тут огромнейшее множество:
- У пользователя может быть, к примеру, виртуальный порт COM158, следовательно циклом в 32 порта не ограничишься, а опрос большого количества портов занимает немало времени.
- Всякие «левые» устройства – виртуальные Bluetooth порты (которые встречаются в каждом втором ноутбуке) и прочее железо может неадекватно реагировать на открытие порта – вплоть до зависания системы.
- Нельзя определить имя устройства в системе и тип устройства (порт или модем)
Как показывает практика (да и здравый смысл тоже), процент зависаний и отказов для такого варианта довольно велик. Так что для коммерческой программы такой вариант вовсе не подойдёт, хотя процент коммерческих программ, опрашивающих таким способом железо, довольно велико.
Вариант 2. Чтение списка из реестра
Оказывается, в ветке HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM хранится список активных портов системы. Можно их просто прочитать и всё будет ок.
procedure TForm1.EnumPorts2();
var reg : TRegistry;
l : TStringList;
i : integer;
begin
l := TStringList.Create;
reg:=TRegistry.Create;
try
reg.RootKey := HKEY_LOCAL_MACHINE;
reg.OpenKey('HARDWARE\DEVICEMAP\SERIALCOMM', false);
reg.GetValueNames(l);
for i:=0 to l.Count-1 do
ComPort.Items.Add(reg.ReadString(l[i]));
finally
reg.Free;
l.Free;
end;
end;
Кстати, данный вариант пользуется большой популярностью. Причина тому одна – он быстрый и работал (во времена Windows XP) практически безотказно. Однако и у него есть минусы:
- Во времена Windows XP всё работало безотказно, но поскольку ветка HKEY_LOCAL_MACHINE в Windows Vista, Windows 7 и Windows 8 закрыта для не-админа (не для пользователей группы «Администраторы», а для пользователя аналога root в Linux), то программу приходится запускать от имени администратора. Можно, конечно, добавить manifest, что решит 99% проблем, но останется тот 1% (например, home-версия Windows) и неприятный осадок.
- Нельзя определить имя устройства в системе и тип устройства (порт или модем)
Данный вариант вполне приемлемый для коммерческого решения, однако есть приёмчики получше.
Вариант 3. Правильный. Используем Windows Driver Kit
Наиболее сложный, но и наиболее правильный вариант.
const
GUID_DEVICEINTERFACE_COMPORT:TGUID=(
D1 : $86E0D1E0;
D2 : $8089;
D3 : $11D0;
D4 : ($9C,$E4,$08,$00,$3E,$30,$1F,$73);
);
GUID_DEVICEINTERFACE_MODEM:TGUID=(
D1 : $2C7089AA;
D2 : $2E0E;
D3 : $11D1;
D4 : ($B1,$14,$00,$C0,$4F,$C2,$AA,$E4);
);
function TForm1.EnumPorts3(
const guid:TGUID;
PortClass :TPortClass):boolean;
var h : THandle;
devinfo : SP_DEVICE_INTERFACE_DATA;
bMoreItems : boolean;
nIndex : longint;
hDeviceKey : HKEY;
szData : array[0..255]of Char;
dwType,dwSize : DWORD;
item : TComPortInfo;
begin
h:=SetupDiGetClassDevs(
@guid,
nil,
0,
DIGCF_PRESENT or DIGCF_INTERFACEDEVICE
);
if(h=INVALID_HANDLE_VALUE)then
begin
exit(false);
end;
try
nIndex:=0;
bMoreItems:=true;
while(bMoreItems)do
begin
devinfo.cbSize:=sizeof(SP_DEVICE_INTERFACE_DATA);
bMoreItems:=SetupDiEnumDeviceInfo(
h,
nIndex,
@devinfo
);
if(bMoreItems)then
begin
hDeviceKey:=SetupDiOpenDevRegKey(
h,
@devinfo,
DICS_FLAG_GLOBAL,
0,
DIREG_DEV,KEY_QUERY_VALUE);
item.name:='';
item.DevDesc:='';
item.PortClass:=PortClass;
if(hDeviceKey>0)then
begin
szData[0]:=#0;
dwSize:=sizeof(szData);
dwType:=0;
if(RegQueryValueEx(
hDeviceKey,
'PortName',
nil,
@dwType,
@szData,
@dwSize)=0)and(dwSize>=2)then
item.name:=szData;
RegCloseKey(hDeviceKey);
end;
if(item.name<>'')then
begin
szData[0]:=#0;
dwSize:=sizeof(szData);
dwType:=0;
if(SetupDiGetDeviceRegistryPropertyW(
h,
@devinfo,
SPDRP_DEVICEDESC,
dwType,
szData,
dwSize,
dwSize))and(dwType = REG_SZ)then
item.DevDesc:=szData;
FItems.Add(item);
end;
end;
inc(nIndex);
end;
result:=true;
finally
SetupDiDestroyDeviceInfoList(h);
end;
end;
Вызываем следующим образом:
FItems.Clear;
EnumPortsSetupAPI(GUID_DEVICEINTERFACE_COMPORT,pcComPort);
EnumPortsSetupAPI(GUID_DEVICEINTERFACE_MODEM,pcModem);
Плюс ко всему, необходим pas-файл для библиотеки SETUPAPI.dll
Плюсы очевидны – работает быстро, стабильно, мы получаем всю необходимую информацию об устройстве, не требует прав администратора.
Минус единственный – для новичка непонятен код.
Выводы
Лично я рекомендую последний метод как единственный правильный, однако у вас может быть своё мнение – высказывайте его в комментариях.
Комментарии
У тебя же есть аккаунт. Залогинься и забудь о капче.
В каком USES ?
Заголовочную библиотеку можно загуглить по названию любой функции.
На днях скину рабочий пример.
Успешно пользуюсь методом 2 под Vista+ x64 без прав администратора. Просто ключ нужно открывать только на чтение (reg.OpenKeyReadOnly)
Метод 3, насколько я понимаю, чуть более продвинут, чем метод 2, т.к. тоже получает данные из реестра, только из других ключей.
Было бы интересно почитать как определить свободен ли COM-порт (с этим вроде как понятно) и, если занят, то каким приложением (это уже интересно).
Я долгое время пользовался методов 2, пока не встретился с нюансами во всяких Home версиях. Главные плюсы метода 3 - он работает безукоризненно и даёт более информативную картину.
Process Explorer показывает открытые файлы (а значит и COM-порты) и их хендлы. Можно покопать в его сторону или поискать решение на MSDN.
E2010 Incompatible types: 'Cardinal' and 'Pointer' - строка 26
E2010 Incompatible types: 'Pointer' and 'Cardinal' - строка 42
E2033 Types of actual and formal var parameters must be identical - строка 42
E2010 Incompatible types: 'Pointer' and 'Cardinal' - строка 49
E2010 Incompatible types: 'Pointer' and 'Cardinal' - строка 78
E2010 Incompatible types: 'PByte' and 'Array' - строка 78
E2010 Incompatible types: 'Pointer' and 'Cardinal' - строка 94
Да, класы самописные. На днях вырежу из готового проекта небольшой пример и закину исходник.
SetupApi тоже закину.
вроде как уже месяц прошел )
Выложите пример реализации вашего функционала! Есть задача, а ваш пример её решает как нельзя лучше !
Заранее благодарен !
Выложу, может кому еще надо:
Получилось примерно так:
// Юнит с нашими функциями
unit detectmodems;
interface
uses
Windows, SysUtils, Forms, SETUPAPI;
type
TGUIDDeviceInfo = record
name: string;
portname: string;
portnumber: integer;
end;
TDevices = array of TGUIDDeviceInfo;
function GetGUIDdevice ():TDevices;
implementation
function GetGUIDdevice ():TDevices;
var
Guid : TGUID;
PnPHandle: HDevInfo;
DeviceInfoData : SP_DEVINFO_DATA;
DeviceInterfaceData: SP_DEVICE_INTERFACE_DATA;
i : Cardinal;
Success: LongBool;
DataT, buffersize: Cardinal;
buffer : PByte;
//
hDeviceKey : HKEY;
szData : array[0..255]of Char;
dwType,dwSize : DWORD;
//
Const
Modems : TGUID = '{4d36e96d-e325-11ce-bfc1-08002be10318}';
ComLpt : TGUID = '{4d36e978-e325-11ce-bfc1-08002be10318}';
begin
Guid:=Modems;
PnPHandle:= SetupDiGetClassDevs(@Guid, nil, 0, DIGCF_PRESENT);
Try
i:= 0;
DeviceInfoData.cbSize:= SizeOf(SP_DEVINFO_DATA);
DeviceInterfaceData.cbSize := SizeOf(SP_DEVICE_INTERFACE_DATA);
Repeat
Success:= SetupDiEnumDeviceInfo(PnPHandle, i, DeviceInfoData);
If Success then
begin
buffer:= nil;
buffersize:= 0;
while not SetupDiGetDeviceRegistryProperty(PnPHandle,DeviceInfoData,SPDRP_DEVICEDESC,@DataT,buffer,buffersize,@buffersize)
do begin
if (GetLastError() = ERROR_INSUFFICIENT_BUFFER) then
begin
if (buffer <> nil) then FreeMem(buffer);
buffer:= AllocMem(buffersize);
end
else
begin
break;
end;
end;
//ListBox1.Items.Add(Format('%d: %s',[i, StrPas(PChar(buffer))]));
SetLength(Result,i+1);
Result[i].name:=StrPas(PChar(buffer));
if (buffer <> nil) then FreeMem(buffer);
// Ïûòàåìñÿ íîìåð ïîðòà îïðåäåëèòü
hDeviceKey:=SetupDiOpenDevRegKey(PnPHandle,DeviceInfoData, DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_QUERY_VALUE);
if(hDeviceKey>0)then
begin
szData[0]:=#0;
dwSize:=sizeof(szData);
dwType:=0;
if(RegQueryValueEx(hDeviceKey,'PortName',nil,@dwType,@szData,@dwSize)=0)and(dwSize>=2)then
Result[i].portname:=szData;
Result[i].portnumber:=StrToInt(StringReplace(szData, 'COM', '',[rfReplaceAll, rfIgnoreCase]));
RegCloseKey(hDeviceKey);
end;
// Ïûòàåìñÿ íîìåð ïîðòà îïðåäåëèòü
End;
Inc(i);
Application.ProcessMessages;
until not Success;
Finally
SetupDiDestroyDeviceInfoList(PnPHandle);
End;
end;
end.
--------------------
Вызов примерно так:
procedure TMain.sBitBtn2Click(Sender: TObject);
var
mas:TDevices;
i:integer;
begin
mas:=GetGUIDdevice();
for i:=0 to Length(mas)-1 do
begin
sListBox1.Items.Add(intToStr(i+1)+' '+mas[i].name+' '+mas[i].portname+' '+IntToStr(mas[i].portnumber));
end;
end;
Спасибо за поддержку
Я так заработался, что совсем блог забросил.
Вы, наверное, имели в виду диспетчер устройств?
Ни разу не видел, чтобы были COM-порты, которых нет в диспетчере устройств...
Интересно, буду в курсе.
Третий способ - это и есть, по сути, то, что использует диспетчер устройств.
Весьма странно, что есть устройство, но его нет в диспетчере устройств...
А если гаджет попал в другую категорию - смотрите GUID класса в диспетчере устройств на вкладке "Сведения".
Ну и касаясь темы статьи - не поминается ещё один способ, минус которого только в отсутствии названий найденных портов. Имеется в виду получение полного списка девайсов через QueryDosDevice и далее вычленение из него строк, начинающихся с COM.
Справедливости ради, есть и ещё способы
http://stackoverflow.com/questions/1388871/how-do-i-get-a-list-of-available-serial-ports-in-win32/1394301#1394301
http://naughter.com/enumser.html
Internally the CEnumerateSerial provides 9 different ways (yes you read that right: Nine) of enumerating serial ports: Using CreateFile, QueryDosDevice, GetDefaultCommConfig, two ways using the Setup API, EnumPorts, WMI, Com Database & enumerating the values under the registry key HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM.
Поэтому приведу маленький пример на билдере, а там и переделать недолго.
Компилируется как в 32, так и в 64 бита.
Единственно что виндовс нужен версии от XP.
Примечателен тем, что крайне прост и находит абсолютно все порты, включая виртуальные не открывая их.
int coms[256]; // Список портов
int comcnt = 0; // Количество портов
int i=0;
int pos;
TCHAR devs[65535];
DWORD dwChars = QueryDosDevice(NULL, devs, 65535);
if(dwChars){
while(devs[i]){
UnicodeString str = &devs[i];
if((pos = str.Pos("COM"))){
str.Delete(1, pos+2);
try{
coms[comcnt++] = StrToInt(str);
}catch(...){}
}
while(devs[i++] != _T('\0'));
}
}
А вот пример основанный в том числе и на инфе отсюда, только тоже находит все порты включая виртуальные, для наглядности дополнительная инфа о порте сбрасывается в TMemo *m1;
m1 нужно добавить на форму, если хотите посмотреть что находит пример.
Это разумеется необязательно, и порты ищутся только те, что удовлетворяют неким дополнительным условиям, что тоже можно выключить, как и считывание лишних параметров.
#include <setupapi.h>
static const UnicodeString DESC = L"Arduino Uno";
static const UnicodeString CLASS = L"Ports";
static const UnicodeString SERV = L"usbser";
static const UnicodeString ID = L"USB\\VID_2341&PID_0043&REV_0001";
UnicodeString strPort;
UnicodeString strDesc;
UnicodeString strClass;
UnicodeString strServ;
UnicodeString strId;
UnicodeString strName;
int coms[256]; // Список портов
int comcnt = 0; // Количество портов
int pos;
bool succ;
HDEVINFO hDevInfo;
SP_DEVINFO_DATA devinfo;
HKEY hKey;
Char bBuf[256];
DWORD dwSize;
if((hDevInfo = SetupDiGetClassDevs(NULL, NULL, 0, DIGCF_PRESENT | DIGCF_ALLCLASSES)) != INVALID_HANDLE_VALUE){
devinfo.cbSize = sizeof(devinfo);
try{
for(DWORD nDev = 0; SetupDiEnumDeviceInfo(hDevInfo, nDev, &devinfo); nDev++){
if((hKey = SetupDiOpenDevRegKey(hDevInfo, &devinfo, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE)) != INVALID_HANDLE_VALUE){
bBuf[0] = L'\0';
dwSize = sizeof(bBuf);
if(!RegQueryValueEx(hKey, L"PortName", NULL, NULL, (LPBYTE)bBuf, &dwSize)){
strPort = bBuf;
if((pos = strPort.Pos(L"COM"))){
m1->Lines->Add(strPort);
strPort.Delete(1, pos+2);
bool ok = true;
if((succ = SetupDiGetDeviceRegistryProperty(hDevInfo, &devinfo, SPDRP_DEVICEDESC, NULL, (LPBYTE)bBuf, sizeof(bBuf), NULL))){
strDesc = bBuf;
m1->Lines->Add(bBuf);
}
ok &= succ;
if((succ = SetupDiGetDeviceRegistryProperty(hDevInfo, &devinfo, SPDRP_FRIENDLYNAME, NULL, (LPBYTE)bBuf, sizeof(bBuf), NULL))){
strName = bBuf;
m1->Lines->Add(bBuf);
}
if((succ = SetupDiGetDeviceRegistryProperty(hDevInfo, &devinfo, SPDRP_CLASS, NULL, (LPBYTE)bBuf, sizeof(bBuf), NULL))){
strClass = bBuf;
m1->Lines->Add(bBuf);
}
ok &= succ;
if((succ = SetupDiGetDeviceRegistryProperty(hDevInfo, &devinfo, SPDRP_SERVICE, NULL, (LPBYTE)bBuf, sizeof(bBuf), NULL))){
strServ = bBuf;
m1->Lines->Add(bBuf);
}
ok &= succ;
if((succ = SetupDiGetDeviceRegistryProperty(hDevInfo, &devinfo, SPDRP_HARDWAREID, NULL, (LPBYTE)bBuf, sizeof(bBuf), NULL))){
strId = bBuf;
m1->Lines->Add(bBuf);
}
ok &= succ;
if(ok && strDesc == DESC && strClass == CLASS && strServ == SERV && strId == ID)
try{
coms[comcnt++] = StrToInt(strPort);
m1->Lines->Add(UnicodeString(L"Это наш порт № ") + coms[comcnt-1]);
}catch(...){}
else
m1->Lines->Add(L"Это не наш порт");
m1->Lines->Add(L"");
}
RegCloseKey(hKey);
}
}
}
}__finally{
SetupDiDestroyDeviceInfoList(hDevInfo);
}
}
m1->Lines->Add(UnicodeString(L"Всего найдено наших портов: ") + comcnt);
Лень было оформлять в виде функций, так, что всё свалено в кучу, и глобальные с локальными переменными и собственно код.
Но там раскидать что куда недолго, зато ничего самописного и не приведённого в примере нет, есть всё необходимое для запуска, даже заголовочный файл.
int coms[256]; // Список портов
int comcnt = 0; // Количество портов
int i=0;
TCHAR devs[65535];
DWORD dwChars = QueryDosDevice(NULL, devs, 65535);
if(dwChars){
while(devs[i]){
UnicodeString str = &devs[i];
if(str.Pos("COM") == 1){
str.Delete(1, 3);
try{
coms[comcnt++] = StrToInt(str);
}catch(...){}
}
while(devs[i++] != _T('\0'));
}
}
Попробовал "вариант 2. Чтение списка из реестра". Прекрасно работает в Windows 10 и 8 не требуется прав админа. Надо только изменить немного 7 строку.
uses Registry,
В место :
7. reg:=TRegistry.Create;
у меря
reg:=TRegistry.Create(KEY_READ);
viagra from canada <a href="https://sildenafilbag.com/">sildenafil over the counter</a> sildenafil davis pdf
viagra memes <a href="http://viagrawallet.org/">women taking viagra</a> sildenafil citrate 100mg
viagra pill <a href="https://topsildenafilcustomer.net/">viagra cialis levitra</a> sildenafil citrate tablets 100mg
https://sildenafilmg.online/# п»їviagra pills
<a href=https://sildenafilmg.com/#>viagra for men</a> buy real viagra online
https://sildenafilmg.online/# viagra cost per pill
<a href=https://sildenafilmg.shop/#>viagra</a> viagra cost
https://sildenafilmg.online/# viagra price
<a href=https://sildenafilmg.shop/#>viagra shop</a> viagra amazon
https://sildenafilmg.online/# buy viagra online usa
<a href=https://sildenafilmg.com/#>sildenafil 20 mg</a> viagra without a doctor prescription usa
https://zithromaxforsale.shop/# zithromax for sale cheap
<a href=https://zithromaxforsale.shop/#>zithromax online usa</a> zithromax without prescription
https://buylasix.icu/# lasix online
<a href=https://buymetformin.best/#>metformin 850 mg cost</a> best generic metformin
https://buylasix.icu/# buy furosemide online
<a href=https://buylipitor.store/#>lipitor 10mg tablet price</a> lipitor 10 mg tablet price
https://buylasix.icu/# lasix online
<a href=https://buytadalafil.men/#>generic tadalafil canada</a> cheap generic tadalafil 5mg
https://gabapentin.icu/# neurontin 1000 mg
<a href=https://cipro.best/#>purchase cipro</a> buy cipro cheap
https://gabapentin.icu/# buy gabapentin online
<a href=https://withoutprescription.store/#>ed meds online without doctor prescription</a> buy prescription drugs without doctor
https://cipro.best/# buy ciprofloxacin over the counter
<a href=https://erectionpills.best/#>non prescription ed pills</a> best ed pills non prescription
https://gabapentin.icu/# neurontin 300mg
<a href=https://erectionpills.best/#>mens ed pills</a> impotence pills
https://diflucan.icu/# buy generic diflucan
<a href=https://erectionpills.best/#>best ed pills</a> male erection pills
https://erectionpills.best/# the best ed pill
<a href=https://cipro.best/#>cipro online no prescription in the usa</a> ciprofloxacin