Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2022年08月19日] [更新 2022年08月23日] Delphi 一般・その他
   
ディスプレイの DPI を取得
動作確認等 Windows 10 ビルド 19043 Pro 64 bit + Delphi XE5(UP2) Pro VCL-32
一部,Windows 10 ビルド 19043 Pro 64 bit + Delphi 10.4 Community VCL-32
一部,Windows 10 ビルド 19043 Pro 64 bit + Delphi 6(UP2) Pro
[整理番号 - 1160]







ディスプレイの DPI ( 拡大率 ) を取得


ディスプレイ (画面,モニタ) のインチ当たりのドット数,DPI を取得するサンプルです.

以下のコマンドを実行すと,下図の [ディスプレイ] の設定ダイアログが起動します.ここで,[テキスト,アプリ,その他の項目のサイズを変更する] でパーセント値を変更すると,画面全体が大表示となります.設定は直ちに反映されます.コンピュータの再起動は必要ありません.
このダイアログは,デスクトップのポップアップメニューから [ディスプレイ] を選択することでも起動できます.


// [ディスプレイ] のダイアログを起動する URI スキーム
ms-settings:display


この拡大表示の機能は,モニタ画面のインチ当たりのドット数 (DPI) を変更することで実現しています.例えば DPI を 144 にします.Windows では DPI の基準値は 96 になっています.したがって,拡大率は以下の計算で求まります.
DPI の基準値は,手定数名 USER_DEFAULT_SCREEN_DPI として定義されています.DPI の値は,横方向,縦方向も同じ値です.


拡大率 (パーセント) = DPI / 96 x 100



図1
[ディスプレイ] ダイアログ
  • [表示スケールの詳細設定] では,100 % 〜 500 % までの任意の倍率が設定可能


Windows におけるディスプレイの拡大表示の機能は,バージョンらよって,設定方法と仕様が変化しています.詳しくは以下の記事で確認してください.






01_アプリケーション・マニフェスト・ファイルを用意する


ディスプレイの DPI を取得する Windows API の関数として,GetDeviceCaps 関数が使われています.しかし,この関数は,Windows 10 では常に基準値である 96 を返します.
Windows 10 でも GetDeviceCaps 関数が実際の DPI の値を返すようにするには,アプリケーション・マニフェストに,DPI を認識させるためのコードを追加して,アプリを DPI 対応にします.
本ページのサンプルのテストだけであれば,以下のマニフェストのコードが利用できます.

このコードには Windows 11 の Id の設定がありませんが,Windows 11 でも使用できます.Windows 11 の Id は Windows 10 と同じです.


リスト1
DPI 対応にするためのアプリケーションマニフェストの例.これを UTF-8 で保存する
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
    xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"
    manifestVersion="1.0">
  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
  <assemblyIdentity
    type="win32"
    name="DelphiApplication"
    version="1.0.0.0" 
    processorArchitecture="*"/>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="win32"
        name="Microsoft.Windows.Common-Controls"
        version="6.0.0.0"
        publicKeyToken="6595b64144ccf1df"
        language="*"
        processorArchitecture="*"/>
    </dependentAssembly>
  </dependency>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel
          level="asInvoker"
          uiAccess="false"/>
        </requestedPrivileges>
    </security>
  </trustInfo>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!--Windows 10-->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
      <!--Windows 8.1-->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
      <!--Windows 8-->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
      <!--Windows 7-->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
      <!--Windows Vista-->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
    </application>
  </compatibility>
</assembly>


上のマニフェストのコートは,以下のリンクの記事に掲載しているコードに,上のコード内の赤線の部分と,以下のコードを追加したものとなっています.dpiAware 属性の値を True または True/PM にすると DPI 対応になります.これを高 DPI 対応とも言います.PM は,Per Monitor の略です.大文字と小文字は区別されません.

このマニフェストを使用して取得する DPI の値は,EXE 起動時の値です.EXE の実行中にディスプレイの拡大率を変更しても,EXE を再起動しないと現在の DPI は取得できません.EXE の実行中にディスプレイの拡大率を検出するには,WM_DPICHANGED メッセージを捕捉します.



  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>


マニフェストの DPI の設定を使用しないで,DPI を取得するサンプルも掲載しています.後述の [06_Delphi 6 / 7 等の旧バージョンにおける DPI の取得] を参考にしてください.




02_GetDeviceCaps 関数を使用しでディスプレイの DPI を取得


Windows API の GetDeviceCaps 関数を使用して DPI を取得するサンプルです.GetDeviceCaps 関数は,プライマリモニタの DPI 取得用の関数です.
下図は,ディスプレイの拡大率が 100 % の時です.拡大率が 100 % の時は,アプリが DPI に対応していも,していなくも,ウインドウとしてのフォームのサイズは同じです.



図2
拡大率が 100 % の場合
  • DPI は 96

下図は,ディスプレイの拡大率を 150 % にした結果です.
最初の図は,プロジェクトのオプションでランタイムを有効にした結果です.これがデフォルト結果です.その次の図は,カスタムマニフェストとして,本ページに掲載しているマニフェストファイルを使用した場合です.DPI として 144 という値が取得できています.これは 150 % に相当します.
この時,TForm.PixelsPerInch の値も 144 になっています.



図3
Delphi XE5 のランタイムテーマが有効な場合
  • 拡大率は 150 %
  • 取得した DPI は 96
図4
本ページに掲載のマニフェストを使用した場合
  • 拡大率は 150 %
  • 取得した DPI は 144

リスト2
//=============================================================================
//  GetDeviceCaps 関数を使用してディスプレイの DPI を取得
//  横方向の DPI と縦方向の DPI は同じ値
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LXDpi : Integer;
  LText : string;
begin
  LXDpi := GetDeviceCaps(GetDC(0), LOGPIXELSX);

  LText := IntToStr(LXDpi) + ' dpi' + sLineBreak;
  LText := LText + IntToStr(Trunc(LXDpi / USER_DEFAULT_SCREEN_DPI * 100.0)) + ' %';
  MessageBox(Handle, PChar(LText), 'DPI', MB_ICONINFORMATION);
end;




03_GetDpiForSystem 関数を使用しでディスプレイの DPI を取得


Windows API の GetDpiForSystem 関数を使用して DPI を取得すサンプルです.マイクロソフトの記事では,GetDpiForSystem 関数の方が GetDeviceCaps 関数よりも後列的であるとしています.
この関数は Windows 10, ビルド 1607 以降で使用可能です.Delphi 10.3 には実装されていますが,Delphi XE5 には実装されていません.そこで,関数の定義コードを追加して利用しています.

下図は,ディスプレイの拡大率が 100 % の時です.拡大率が 100 % の時は,プリが DPI に対応していも,していなくも,ウインドウとしてのフォームのサイズは同じです.



図5
拡大率が 100 % の場合
  • DPI は 96

下図は,ディスプレイの拡大率を 175 % にした結果です.
最初の図は,プロジェクトのオプションでランタイムを有効にした結果です.これがデフォルト結果です.その次の図は,カスタムマニフェストとして,本ページに掲載しているマニフェストファイルを使用した場合です.DPI として 168 という値が取得できています.これは 175 % に相当します.



図6
Delphi XE5 のランタイムテーマを有効にして実行した場合
  • 拡大率は 175 %
  • 取得した DPI は 96
図7
本ページに掲載のマニフェストを使用した場合
  • 拡大率は 175 %
  • 取得した DPI は 168

リスト3
implementation

{$R *.dfm}

  // GetDpiForSystem 関数は Delphi 10.3 には実装されている
  // Windows 10, ビルド 1607 以降で使用可能
  function GetDpiForSystem : UINT; stdcall; external 'user32.dll';

//=============================================================================
//  GetDpiForSystem 関数を使用してディスプレイの DPI を取得
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LDpi   : Integer;
  LValue : Integer;
  LText  : string;
begin
  LDpi   := GetDpiForSystem;
  LValue := Trunc(LDpi / USER_DEFAULT_SCREEN_DPI * 100.0);

  LText := IntToStr(LDpi) + ' dpi' + sLineBreak;
  LText := LText + IntToStr(LValue) + ' %';
  MessageBox(Handle, PChar(LText), 'DPI', MB_ICONINFORMATION);
end;




04_GetDpiForMonitor 関数を使用しでディスプレイの DPI を取得


Windows API の GetDpiForMonitor 関数を使用して DPI を取得すサンプルです.GetDpiForMonitor 関数は,マルチモニタ対応の関数です.本サンプルでは,マウスカーソルがあるモニタを対象にしています.
この関数は Windows 8.1 以降で使用可能です.Delphi 10.3 には実装されていますが,Delphi XE5 には実装されていません.そこで,関数の定義コードを追加して利用しています.必要な列挙型のコードも追加しています.

GetDpiForMonitor 関数を使用する時は,アプリケーション マニフェストの dpiAware 属性の値を True/PM にします.これまでのサンプルも,True/PM で動作します.
下図は,ディスプレイの拡大率が 100 % の時です.拡大率が 100 % の時は,プリが DPI に対応していも,していなくも,ウインドウとしてのフォームのサイズは同じです.



図8
拡大率が 100 % の場合
  • DPI は 96

下図は,ディスプレイの拡大率を 125 % にした結果です.
最初の図は,プロジェクトのオプションでランタイムを有効にした結果です.これがデフォルト結果です.その次の図は,カスタムマニフェストとして,本ページに掲載しているマニフェストファイルを使用した場合です.DPI として 120 という値が取得できています.これは 125 % に相当します.



図9
Delphi XE5 のランタイムテーマが有効な場合
  • 拡大率は 125 %
  • 取得した DPI は 96
図10
本ページに掲載のマニフェストを使用した場合
  • 拡大率は 125 %
  • 取得した DPI は 120

リスト4
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;

type
  MONITOR_DPI_TYPE = (
    MDT_EFFECTIVE_DPI = 0,
    MDT_ANGULAR_DPI = 1,
    MDT_RAW_DPI = 2,
    MDT_DEFAULT = MDT_EFFECTIVE_DPI);
  TMonitorDpiType = MONITOR_DPI_TYPE;

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Winapi.MultiMon;

{$R *.dfm}

  // この関数は Delphi 10.3 には実装されている ( uses に Winapi.ShellScaling が必要 )
  // Windows 8.1 以降で使用可能
  function GetDpiForMonitor(hmonitor: HMONITOR;
                            dpiType: TMonitorDpiType;
                            out dpiX: UINT;
                            out dpiY: UINT): HRESULT; stdcall; external 'Shcore.dll';

//=============================================================================
//  GetDpiForMonitor 関数を使用してディスプレイの DPI を取得
//  
//  uses に Winapi.MultiMon が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LCursorPos : TPoint;
  LhMonitor  : HMONITOR;
  LXDpi      : UINT;
  LYDpi      : UINT;
  LText      : string;
begin
  // マウスカーソルがあるモニタのハンドルを取得
  GetCursorPos(LCursorPos);
  LhMonitor := MonitorFromPoint(LCursorPos, MONITOR_DEFAULTTONEAREST);

  GetDpiForMonitor(LhMonitor, MDT_EFFECTIVE_DPI, LXDpi, LYDpi);

  LText := IntToStr(LXDpi) + ' dpi' + sLineBreak;
  LText := LText + IntToStr(Trunc(LXDpi / USER_DEFAULT_SCREEN_DPI * 100.0)) + ' %';
  MessageBox(Handle, PChar(LText), 'DPI', MB_ICONINFORMATION);
end;

end.


GetDpiForMonitor 関数の第 1 引数はモニタのハンドルです.
モニタの番号が I のモニタのハンドルは,例えば,次のようにして求めることができます.0 ベースなので 1 番目のモニタの番号は 0 です.接続されているモニタの数は, LScreen.MonitorCount で取得できます.


  // モニタ番号が I のモニタのハンドルを取得
  LScreen := TScreen.Create(Self);
  try
    LhMonitor := LScreen.Monitors[i].Handle;
  finally
    FreeAndNil(LScreen);
  end;




05_Delphi 10.4 ではカスタムマニフェストは不要


Delphi 10..4 の場合,本ページに掲載しているマニフェストファイルを作成する必要はありません.IDE の [プロジェクト] [オプション] で設定できます.Delphi 10.3 以前については未確認です.

[プロジェクト オプション] のダイアログで,左ペインの [マニフェスト] を選択します.右ペインの [マニフェスト ファイル] を [自動生成] にします.[ランタイムテーマの有効化] をチェックして,[DPI の認識] で以下のいずれかを選択します.
[システム DPI に対応]
[ディスプレイごとの DPI に対応]
[ディスプレイごとの DPI に対応 V2]
これで,本ページに掲載しているようなマニフェストが自動的に EXE に組み込まれます.必要であれば,EXE に組み込まれたマニフェストのコードを取り出して保存することもできます.
下図は,Delphi 10.4 Community の設定画面です.



図11
Delphi 10.4 のプロジェクトオプション
  • マニフェストに追加する機能が設定できる

下図が実行結果です.ディスプレイの拡大率を 150 % にして,GetDeviceCaps 関数のサンプルを使用しています.Delphi 10.4 で作成したアプリは,[DPI の認識] の設定に関係なく,ウインドウとしてのフォームのサイズは同じです.



図12
Delphi 10.4 の場合
  • 拡大率 150 %
  • [DPI の認識] は [なし]
  • 取得できた値は基本値の 96
図13
Delphi 10.4 の場合
  • 拡大率 150 %
  • [DPI の認識] は [システム DPI に対応]
  • 取得した DPI の値は 144




06_Delphi 6 / 7 等の旧バージョンにおける DPI の取得


Delphi 10.4 は手元にある Delphi の最新バージョンです.手元にある最も古いバージョンは Delphi 6 です.Delphi 6 でもテストしてみました.GetDpiForMonitor 関数を使用するサンプルでテストしました.
そ結果です.

Delph 6 には USER_DEFAULT_SCREEN_DPI が定義されていません.代わりに数値 96 を使用しました.
下図は,ディスプレイの拡大率が 100 % の時です.



図14
拡大率が 100 % の場合
  • DPI は 96

ディプレイの拡大率を 150 % にした時の結果です.



図15
Delphi 6 の場合
  • ディスプレイの拡大率は 150 %
  • マニフェストがない場合
  • 取得した DPI の値は基本値の 96
図16
Delphi 6 の場合
  • ディスプレイの拡大率は 150 %
  • マニフェストを使用した場合
  • 正しい DPI は取得できているが,フォームは拡大されていない

Delphi 6 / 7 のような古いバージョンで作成された,レガシーなアプリの多くは,DPI が 96 であることを前提にした処理が行われます.そのようなアプリが,システムの DPI の値を認識してしまうと,上の図のような結果になります.
現在は高解像度の  (画面のドット数が多い 高 DPI ) ディスプレイが普及しています.GetDeviceCaps 等の関数が,基準値の 96 を返すようになったのは,レガシーなアプリに対する対応策とも考えられます.

下図は,マニフェストから <dpiAware> 関係のタグを除いた結果です.つまり,今回マニフェストに追加した部分を追加しないでコンパイルした結果です.これは,基本的なマニフェストのコードを使用した結果と言えます.



図17
Delphi 6 の場合
  • ディスプレイの拡大率は 150 %
  • マニフェストに DPI の設定がない場合
  • テーマに沿った描画で,フォームも拡大しているが,正しい DPI は取得できない

Delphi 6 / 7 で,システムの DPI を取得する方法として,レジストリから読み取る方法が考えられます.DPI の値はレジストリの以下の値です.図の下のコードはその例です.
この方法であれば,マニフェストの DPI の設定は不要です.
ただし,ティスプレイの拡大率を変更しても,レジストリのこの値は,コンピュータを再起動しないと更新されません.


HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics\AppliedDPI



図18
Delphi 6 で DPI を取得
  • レジストリから読み取れる
  • 拡大率が 175 % の例
  • 取得した DPIの値は 168

リスト5
//=============================================================================
//  レジストリから DPI の値を取得
// 
//  DPI の現在値はレジストリの以下のキーの項目 AppliedDPI にある
//,ディスプレイの拡大率を変更した場合,PC を再起動しないと更新されない
//  HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics
//
//  uses に Registry が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LRegistry  : TRegistry;
  LRegistKey : string;
  LDpi       : Integer;
    LText    : string;
begin
  LDpi := 0;
  LRegistry := TRegistry.Create(KEY_READ);
  try
    LRegistry.RootKey := HKEY_CURRENT_USER;
    LRegistKey := 'Control Panel\Desktop\WindowMetrics';

    if LRegistry.OpenKey(LRegistKey, False) then begin
      LDpi := LRegistry.ReadInteger('AppliedDPI');
    end;
  finally
    FreeAndNil(LRegistry);
  end;

  LText := IntToStr(LDpi) + ' dpi' + sLineBreak;
  LText := LText + IntToStr(Trunc(LDpi / 96 * 100.0)) + ' %';
  MessageBox(Handle, PChar(LText), 'DPI', MB_ICONINFORMATION);
end;


もう 1 つの方法は,DwmGetWindowAttribute 関数で取得したウィンドウのサイズと,GetWindowsRect 関数で取得したウィンドウのサイズの比率で計算する方法です.この方法も,マニフェストの DPI の設定は不要です.
GetWindowRect 関数は,ディスプレイの DPI が 96 であることを前提にしてウィンドウのサイズを取得します.一方, DwmGetWindowAttribute 関数は,実際のドット数でウィンドウのサイズを取得します.

下図は,ディスプレイの拡大率が 100 % の時です.
このサンプルでは拡大率を求めています.DPI は,拡大率から次の計算で求まります.


DPI = 拡大率 x 96



図19
拡大率が 100 % の場合
  • DPI は 96
  • 最初の値は,GetWindowRect で取得した幅
    設計時のフォームの Width プロパティの値
  • 次の値は,画面上の実際の幅
  • 最後の値は,その比率
    1.00 なので 100 %
    これは,DWM のフレーム幅を考慮した結果

ディススプレイの拡大率を 175 % にした時の結果です.



図20
ディスプレイの拡大率を取得
  • 拡大率が 175 % の場合
  • 数値の意味は上の図と同じ

テストに使用したコードです.このコードは,TForm.BorderStyle が bsSizeable の時の専用です.このプロパティが別の値の時は,正しい拡大率は取得できません.
Delphi XE5 等では,関数の定義も定数名の定義も必要ありません.Uses に Winapi.Dwmapi を追加すれば利用できます.もちろん,このまま使用しても問題ありません.



リスト6
implementation

{$R *.dfm}

  function DwmGetWindowAttribute(hwnd: HWND; dwAttribute: DWORD;
    pvAttribute: Pointer; cbAttribute: DWORD): HResult; stdcall;  external 'DWMAPI.DLL';

const
  DWMWA_EXTENDED_FRAME_BOUNDS = 9;  

//=============================================================================
//  ディスプレイの拡大率を取得
//  実際のウィンドウのサイズと GetWindowRect の戻り値の比率から計算
//
//  ウィンドウのサ実イズは DwmGetWindowAttribute で取得
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LThick   : Integer;
  LRectDwm : TRect;
  LRectWin : TRect;
  LRatio   : Single;
  LText    : string;
begin
  // ウィンドウの外周の外の部分 ('DWM で描画されない部分 )
  // BorderStyle := bsSizeable の時の値
  LThick := GetSystemMetrics(SM_CXSIZEFRAME) - GetSystemMetrics(SM_CXBORDER);

  DwmGetWindowAttribute(Handle, DWMWA_EXTENDED_FRAME_BOUNDS, @LRectDwm, SizeOf(LRectDwm));
  GetWindowRect(Handle, LRectWin);

  // 拡大率
  LRatio := (LRectDwm.Right - LRectDwm.Left) / (LRectWin.Right - LRectWin.Left - LThick * 2);

  LText := IntToStr(LRectWin.Right - LRectWin.Left) + ' .dots' + sLineBreak;
  LText := LText + IntToStr(LRectDwm.Right - LRectDwm.Left) + ' dots' + sLineBreak;
  LText := LText + sLineBreak + FormatFloat('0.00', LRatio);

  MessageBox(Handle, PChar(LText), 'DPI', MB_ICONINFORMATION);
end;




07_ディスプレイの拡大率 ( DPI ) 変更時のメッセージ受信


ディスプレイの拡大率,つまり,DPI が変更された時のメッセージを捕捉するサンプルです.
このメッセージは,アプリケーション マニフェストの dpiAware 属性の値が True/PM でないと捕捉できません.
Delphi 10.4 では,[マニフェスト] [DPI の認識] を, [ディスプレイごとの DPI に対応] または [ディスプレイごとの DPI に対応 V2] にします.



リスト7
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;

const
  // Delphi 10.4 にはこの定義が実装されている
  WM_DPICHANGED = $02E0;

type
  TForm1 = class(TForm)
  protected
    procedure WMDPIChanged(var message: TMessage); message WM_DPICHANGED;
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//-----------------------------------------------------------------------------
//  WM_DPICHANGED メッセージの受信
//  拡大率 ( DPI ) が変更されたら DPI の値と拡大率を表示
//
//  このメッセージは,マニフェストの dpiAware 属性の値が True/PM でないと捕捉不可
//  Delphi 10.4 では [マニフェスト] の [DPI の認識] で設定する
//-----------------------------------------------------------------------------
procedure TForm1.WMDPIChanged(var message: TMessage);
var
  LDpiX : Integer;
  LPer  : Integer;
  LText : string;
begin
  LDpiX := LoWord(message.WParam);
  LPer  := Trunc(LDpiX / USER_DEFAULT_SCREEN_DPI * 100.0);

  LText := IntToStr(LDpiX) + ' dpi' + sLineBreak + IntToStr(LPer) + ' %';
  MessageBox(Handle, PChar(LText), 'DPI', MB_ICONINFORMATION);
end;

end.