像 MultiMonitor 工具一样读取监视器名称

Read monitor name like MultiMonitor tool does

我想知道 Delphi 中是否有办法读取 监视器名称,正如我在 MultiMonitorTool[= 中看到的那样30=] 程序:

目前我尝试通过以下方式阅读:

  • MonitorInfo.szDevice (它returns "\.\DISPLAY1")
  • monitorname(Screen.Monitors[i].MonitorNum) (它returns "Generic-Non-PnP-Monitor")
  • Screen.Monitors[i].FriendlyName (它returns "Generic-Non-PnP-Monitor")
procedure TMonitorOrientationDemoForm.RefreshMonitorsList;
var
  i: integer;
  li: TListItem;
  MonitorInfo: TMonitorInfoEx;
  LTmp: string;

  function ArrayToString(const a: array of Char): string;
  begin
    if Length(a) > 0 then
      Result := StrPas(PChar(@a[0]))
    else
      Result := '';
  end;

begin
  lvMonitors.Items.BeginUpdate;
  try
    lvMonitors.Items.Clear;
    for i := 0 to Screen.MonitorCount - 1 do
    begin
      li := lvMonitors.Items.Add;
      MonitorInfo.cbSize := SizeOf(MonitorInfo);
      if not GetMonitorInfo(Screen.Monitors[i].Handle, @MonitorInfo) then
        continue;
      LTmp := ArrayToString(MonitorInfo.szDevice);
      //Screen.Monitors[i].FriendlyName -> returns the string "Generic-Non-PnP-Monitor"
      //MonitorInfo.szDevice -> returns the string "\.\DISPLAY1"
      //monitorname(Screen.Monitors[i].MonitorNum) -> returns the string "Generic-Non-PnP-Monitor"
      LI.Caption := Screen.Monitors[i].FriendlyName;
      li.SubItems.Add(MonitorOrientationToString(Screen.Monitors[i].Orientation));
      if Screen.Monitors[i].SupportsRotation then
        li.SubItems.Add('Yes')
      else
        li.SubItems.Add('No');
    end;
  finally
    lvMonitors.Items.EndUpdate;
  end;
end;

function TMonitorOrientationDemoForm.monitorname(numberof: integer): string;
Var
  Cntr: Cardinal;
  Info: TDisplayDevice;
  AdapterName: PChar;
  OldPos, j, i: integer;
  a: tstringlist;
Begin
  a := tstringlist.create;
  Cntr := numberof;
  Info.cb := SizeOf(Info);
  While EnumDisplayDevices(Nil, Cntr, Info, 0) Do
  Begin
    AdapterName := StrAlloc(SizeOf(Info.DeviceName));
    StrCopy(AdapterName, Info.DeviceName);
    EnumDisplayDevices(AdapterName, 0, Info, 0);
    a.Add(Info.DeviceString);
    for i := 1 to a.count - 1 do
    begin
      a.Delete(i);
      Result := a.Strings[0];
      StrDispose(AdapterName);
      Inc(Cntr);
    End;
  End;
End;

基于EnumDisplayDevices and has described in several previous Qs [1, 2]的标准方法在Delphi中可以这样写:

procedure TForm1.FormCreate(Sender: TObject);
var
  dd, md: TDisplayDevice;
begin

  ListBox1.Items.BeginUpdate;
  try
    ListBox1.Clear;
    FillChar(dd, SizeOf(dd), 0);
    dd.cb := SizeOf(dd);
    FillChar(md, SizeOf(md), 0);
    md.cb := SizeOf(md);
    var i := 0;
    while EnumDisplayDevices(nil, i, dd, 0) do
    begin
      var j := 0;
      while EnumDisplayDevices(@dd.DeviceName[0], j, md, 0) do
      begin
        ListBox1.Items.Add(md.DeviceString);
        Inc(j);
      end;
      Inc(i);
    end;
  finally
    ListBox1.Items.EndUpdate;
  end;

end;

在我的系统上,这可以正确识别我的三台戴尔显示器,但无法识别我的三星壁挂式电视(“通用 PnP 显示器”)。

首先你必须知道有些显示器在 windows 中没有注册名称,但如果它们有名称,你可以使用 MultiMonitorTool 等程序读取它,所以你也可以用Delphi阅读它。

您可以从注册表中读取 EDID 键值。 EDID 键的值在安装显示器时创建,可以被显示器驱动程序覆盖。

此程序读取活动监视器的 EDID。

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Win.Registry, Vcl.ComCtrls, Winapi.MultiMon,
   Winapi.Windows, Vcl.Forms, System.Classes, System.StrUtils,
   System.Character;

function GetMonitorName(index : Integer) : String;
const
  Key = '\SYSTEM\CurrentControlSet\Enum\DISPLAY\';
type
  IBMString = type Ansistring(1253);
var
  Handle: HMONITOR;
  i, j, k : Integer;
  Registry: TRegistry;
  MonitorName : IBMString;
  DispDev : TDisplayDevice;
  subKeysNames : TStringList;
  MonitorInfo : TMonitorInfoEx;
  DeviceIDSplit : TArray<String>;
  EDID : array [0 .. 127] of Byte;
  DeviceName, DeviceString, DeviceID, DeviceKey, Driver : string;
begin
  Result := '';

  Handle := Screen.Monitors[index].Handle;
  MonitorInfo.cbSize := sizeof(MonitorInfo);
  if GetMonitorInfo(Handle, @MonitorInfo) then
  begin
    DispDev.cb := sizeof(DispDev);
    EnumDisplayDevices(@MonitorInfo.szDevice, 0, DispDev, 0);
    DeviceName   := StrPas(DispDev.DeviceName);     //'\.\DISPLAY1\Monitor0'    //This Line Can Be Removed
    DeviceString := StrPas(DispDev.DeviceString);   //'Generic PnP Monitor'      //This Line Can Be Removed
    DeviceID     := StrPas(DispDev.DeviceID);
    DeviceKey    := StrPas(DispDev.DeviceKey);                                   //This Line Can Be Removed

    DeviceIDSplit := DeviceID.Split(['\']);
    if Length(DeviceIDSplit) < 3 then Exit;
    Driver := '';
    for i := 2 to High(DeviceIDSplit) do
      Driver := Driver + '\' + DeviceIDSplit[i];
    System.Delete(Driver, 1, 1);

    subKeysNames := TStringList.Create;
    Registry := TRegistry.Create(KEY_READ);
    Registry.RootKey := HKEY_LOCAL_MACHINE;
    try
      try
        Registry.OpenKeyReadOnly(Key);
        Registry.GetKeyNames(subKeysNames);
      finally
        Registry.CloseKey;
      end;
      if subKeysNames.IndexOf(DeviceIDSplit[1]) < 0 then Exit;
      try
        Registry.OpenKeyReadOnly(Key + DeviceIDSplit[1]);
        Registry.GetKeyNames(subKeysNames);
      finally
        Registry.CloseKey;
      end;

      for i := 0 to subKeysNames.Count - 1 do
      begin
        try
          if registry.OpenKeyReadOnly(Key + DeviceIDSplit[1] + '\' + subKeysNames[i]) then
          begin
            if Registry.ReadString('Driver') <> Driver then Continue;
            Registry.CloseKey;
            if registry.OpenKeyReadOnly(Key + DeviceIDSplit[1] + '\' + subKeysNames[i] + '\' + 'Device Parameters') then
            begin
              Registry.ReadBinaryData('EDID', EDID, 128);
              Registry.CloseKey;
            end;
            for j := 0 to 3 do
            begin
              if (EDID[54 + 18 * j] = 0) and
                 (EDID[55 + 18 * j] = 0) and
                 (EDID[56 + 18 * j] = 0) and
                 (EDID[57 + 18 * j] = $FC) and
                 (EDID[58 + 18 * j] = 0) then
              begin
                k := 0;
                while (EDID[59 + 18 * j + k] <> $A) and (k < 13) do
                  Inc(k);
                SetString(MonitorName, PAnsiChar(@EDID[59 + 18 * j]), k);
                Result := MonitorName;
                Break;
              end;
            end;
          end;
        finally
          Registry.CloseKey;
        end;
      end;
    finally
      Registry.Free;
      subKeysNames.Free;
    end;
  end;
end;

var
  i : Integer;

begin
  for i := 0 to Screen.MonitorCount-1 do
  begin
    Writeln(GetMonitorName(i));
  end;
  Readln;
end.

并且这个程序读取了所有的监视器名称,其中一些可能没有名称。

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Win.Registry, Vcl.ComCtrls, Winapi.MultiMon,
   Winapi.Windows, Vcl.Forms, System.Classes, System.StrUtils,
   System.Character;

function GetAllMonitorName : TStrings;
const
  Key = '\SYSTEM\CurrentControlSet\Enum\DISPLAY\';
type
  IBMString = type Ansistring(1253);
var
  Registry: TRegistry;
  H, i, j, k : Integer;
  MonitorName : IBMString;
  EDID : array [0 .. 127] of Byte;
  subKeysNames, subKeys : TStringList;
begin
  Result := TStringList.Create;

  subKeys := TStringList.Create;
  subKeysNames := TStringList.Create;
  Registry := TRegistry.Create(KEY_READ);
  Registry.RootKey := HKEY_LOCAL_MACHINE;
  try
    try
      Registry.OpenKeyReadOnly(Key);
      Registry.GetKeyNames(subKeysNames);
    finally
      Registry.CloseKey;
    end;

    for h := 0 to subKeysNames.Count - 1 do
    begin
      try
        Registry.OpenKeyReadOnly(Key + subKeysNames[h]);
        Registry.GetKeyNames(subKeys);
      finally
        Registry.CloseKey;
      end;

      for i := 0 to subKeys.Count - 1 do
      begin
        try
          if registry.OpenKeyReadOnly(Key + subKeysNames[h] + '\' + subKeys[i]) then
          begin
            if registry.OpenKeyReadOnly(Key + subKeysNames[h] + '\' + subKeys[i] + '\' + 'Device Parameters') then
            begin
              Registry.ReadBinaryData('EDID', EDID, 128);
              Registry.CloseKey;
            end;
            MonitorName := '';
            for j := 0 to 3 do
            begin
              if (EDID[54 + 18 * j] = 0) and
                 (EDID[55 + 18 * j] = 0) and
                 (EDID[56 + 18 * j] = 0) and
                 (EDID[57 + 18 * j] = $FC) and
                 (EDID[58 + 18 * j] = 0) then
              begin
                k := 0;
                while (EDID[59 + 18 * j + k] <> $A) and (k < 13) do
                  Inc(k);
                SetString(MonitorName, PAnsiChar(@EDID[59 + 18 * j]), k);
                Break;
              end;
            end;
            Result.Add(MonitorName);
          end;
        finally
          Registry.CloseKey;
        end;
      end;
    end;
  finally
    subKeys.Free;
    Registry.Free;
    subKeysNames.Free;
  end;
end;

var
  i : Integer;

begin
  Writeln(GetAllMonitorName.Text);
  Readln;
end.

如果你想写一个类似 MultiMonitorTool 的程序,你可以从 EDID 阅读更多信息,更多信息阅读 Extended Display Identification Data

这对我来说很新,要么是因为我在 Windows 7 上执行它,要么是因为我还有一台电视而不是显示器,所以我也只能得到一个使用常规方法的通用“PnP-Monitor(标准)”。但是下面的代码给了我一个有用的结果:

program Whosebug68064094;
{$APPTYPE CONSOLE}

const
  user32= 'user32.dll';
  QDC_ALL_PATHS=                                1;
  DISPLAYCONFIG_MODE_INFO_TYPE_TARGET=          2;
  DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME=    2;
  ERROR_SUCCESS=                                0;

type
  LUID= Int64;
  LONG= LongInt;  // Just anything 32bit
  BOOL= LongBool;

  // ENUMs, all starting with 1
  DISPLAYCONFIG_TARGET_DEVICE_NAME_FLAGS=   Cardinal;
  DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY=    Cardinal;
  DISPLAYCONFIG_ROTATION=                   Cardinal;
  DISPLAYCONFIG_SCALING=                    Cardinal;
  DISPLAYCONFIG_SCANLINE_ORDERING=          Cardinal;
  DISPLAYCONFIG_MODE_INFO_TYPE=             Cardinal;
  DISPLAYCONFIG_PIXELFORMAT=                Cardinal;
  DISPLAYCONFIG_TOPOLOGY_ID=                Cardinal;

  PDISPLAYCONFIG_DEVICE_INFO_HEADER= ^DISPLAYCONFIG_DEVICE_INFO_HEADER;
  DISPLAYCONFIG_DEVICE_INFO_HEADER= packed record
    typ, size: Cardinal;
    adapterId: LUID;
    id: Cardinal;
  end;

  DISPLAYCONFIG_TARGET_DEVICE_NAME= packed record
    header: DISPLAYCONFIG_DEVICE_INFO_HEADER;
    flags: DISPLAYCONFIG_TARGET_DEVICE_NAME_FLAGS;
    outputTechnology: DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
    edidManufactureId, edidProductCodeId: Word;
    connectorInstance: Cardinal;
    monitorFriendlyDeviceName: Array[0.. 64- 1] of WideChar;
    monitorDevicePath: Array[0.. 128- 1] of WideChar;
  end;

  DISPLAYCONFIG_PATH_SOURCE_INFO= packed record
    adapterId: LUID;
    id, modeInfoIdx, statusFlags: Cardinal;
  end;

  DISPLAYCONFIG_RATIONAL= packed record
    Numerator, Denominator: Cardinal;
  end;

  DISPLAYCONFIG_PATH_TARGET_INFO= packed record
    adapterId: LUID;
    id, modeInfoIdx: Cardinal;
    outputTechnology: DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
    rotation: DISPLAYCONFIG_ROTATION;
    scaling: DISPLAYCONFIG_SCALING;
    refreshRate: DISPLAYCONFIG_RATIONAL;
    scanLineOrdering: DISPLAYCONFIG_SCANLINE_ORDERING;
    targetAvailable: BOOL;
    statusFlags: Cardinal;
  end;

  PDISPLAYCONFIG_PATH_INFO= ^DISPLAYCONFIG_PATH_INFO;
  DISPLAYCONFIG_PATH_INFO= packed record
    sourceInfo: DISPLAYCONFIG_PATH_SOURCE_INFO;
    targetInfo: DISPLAYCONFIG_PATH_TARGET_INFO;
    flags: Cardinal;
  end;

  DISPLAYCONFIG_2DREGION= packed record
    cx, cy: Cardinal;
  end;

  DISPLAYCONFIG_VIDEO_SIGNAL_INFO= packed record
    pixelRate: UInt64;
    hSyncFreq, vSyncFreq: DISPLAYCONFIG_RATIONAL;
    activeSize, totalSize: DISPLAYCONFIG_2DREGION;
    videoStandard: Cardinal;
  end;

  DISPLAYCONFIG_TARGET_MODE= packed record
    targetVideoSignalInfo: DISPLAYCONFIG_VIDEO_SIGNAL_INFO;
  end;

  POINTL= packed record  // Or TPOINT right away
    x, y: LONG;
  end;

  DISPLAYCONFIG_SOURCE_MODE= packed record
    width, height: Cardinal;
    pixelFormat: DISPLAYCONFIG_PIXELFORMAT;
    position: POINTL;
  end;

  RECTL= packed record  // Or TRECT right away
    left, top, right, bottom: LONG;
  end;

  DISPLAYCONFIG_DESKTOP_IMAGE_INFO= packed record
    PathSourceSize: POINTL;
    DesktopImageRegion, DesktopImageClip: RECTL;
  end;

  PDISPLAYCONFIG_MODE_INFO= ^DISPLAYCONFIG_MODE_INFO;
  DISPLAYCONFIG_MODE_INFO= packed record
    infoType: DISPLAYCONFIG_MODE_INFO_TYPE;
    id: Cardinal;
    adapterId: LUID;
    case Byte of
    1: ( targetMode: DISPLAYCONFIG_TARGET_MODE );
    2: ( sourceMode: DISPLAYCONFIG_SOURCE_MODE );
    3: ( desktopImageInfo: DISPLAYCONFIG_DESKTOP_IMAGE_INFO );
  end;

  PDISPLAYCONFIG_TOPOLOGY_ID= ^DISPLAYCONFIG_TOPOLOGY_ID;


function GetDisplayConfigBufferSizes( flags: Cardinal;
  numPathArrayElements, numModeInfoArrayElements: PCardinal ): LONG; stdcall; external user32;  // Vista

function QueryDisplayConfig( flags: Cardinal; numPathArrayElements: PCardinal;
  pathArray: PDISPLAYCONFIG_PATH_INFO; numModeInfoArrayElements: PCardinal;
  modeInfoArray: PDISPLAYCONFIG_MODE_INFO;
  currentTopologyId: PDISPLAYCONFIG_TOPOLOGY_ID ): LONG; stdcall; external user32;  // Windows 7

function DisplayConfigGetDeviceInfo(
  requestPacket: PDISPLAYCONFIG_DEVICE_INFO_HEADER ): LONG; stdcall; external user32;  // Vista


// Actually using all this
procedure GetFriendlyMonitorNames();
var
  vName: DISPLAYCONFIG_TARGET_DEVICE_NAME;
  iPath, iMode: Cardinal;
  aPath: Array of DISPLAYCONFIG_PATH_INFO;
  aMode: Array of DISPLAYCONFIG_MODE_INFO;
  i: Integer;
begin
  if not GetDisplayConfigBufferSizes( QDC_ALL_PATHS, @iPath, @iMode )= ERROR_SUCCESS then exit;
  SetLength( aPath, iPath );  // For me: 104...
  SetLength( aMode, iMode );  // ...and 30

  if not QueryDisplayConfig
  ( QDC_ALL_PATHS
  , @iPath, @aPath[0]
  , @iMode, @aMode[0]
  , nil
  )= ERROR_SUCCESS then exit;

  for i:= 0 to Integer(iMode)- 1 do begin
    case aMode[i].infoType of
      DISPLAYCONFIG_MODE_INFO_TYPE_TARGET: begin
        vName.header.size:= SizeOf( DISPLAYCONFIG_TARGET_DEVICE_NAME );
        vName.header.adapterId:= aMode[i].adapterId;
        vName.header.id:= aMode[i].id;
        vName.header.typ:= DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
        if( DisplayConfigGetDeviceInfo( @vName ) )= ERROR_SUCCESS then begin
          Writeln( vName.monitorFriendlyDeviceName );  // For me: M237WD
        end;
      end;
    end;
  end;
end;

begin
  GetFriendlyMonitorNames();
end.

在较旧的上网本上,友好名称为空(成功的函数调用,tho),而通用 .DeviceString 方法给我“Digital-Flachbildschirm (1280x1024 60Hz)”,这更有帮助。我想至少必须考虑这两种方法。

编辑: 包括所有从 C++ 翻译而来的手写定义。现在它是一个完整的程序,运行 和 Delphi 7 没问题。至少需要 Windows 7.