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
ディスプレイ (画面,モニタ) のインチ当たりのドット数,DPI を取得するサンプルです.
以下のコマンドを実行すと,下図の [ディスプレイ] の設定ダイアログが起動します.ここで,[テキスト,アプリ,その他の項目のサイズを変更する] でパーセント値を変更すると,画面全体が大表示となります.設定は直ちに反映されます.コンピュータの再起動は必要ありません.
このダイアログは,デスクトップのポップアップメニューから [ディスプレイ] を選択することでも起動できます.
この拡大表示の機能は,モニタ画面のインチ当たりのドット数 (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 で保存する
copy code
<?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 メッセージを捕捉します.
02_ GetDeviceCaps 関数を使用しでディスプレイの DPI を取得
Windows API の GetDeviceCaps 関数を使用して DPI を取得するサンプルです.GetDeviceCaps 関数は,プライマリモニタの DPI 取得用の関数です.
下図は,ディスプレイの拡大率が 100 % の時です.拡大率が 100 % の時は,アプリが DPI に対応していも,していなくも,ウインドウとしてのフォームのサイズは同じです.
図2
拡大率が 100 % の場合
下図は,ディスプレイの拡大率を 150 % にした結果です.
最初の図は,プロジェクトのオプションでランタイムを有効にした結果です.これがデフォルト結果です.その次の図は,カスタムマニフェストとして,本ページに掲載しているマニフェストファイルを使用した場合です.DPI として 144 という値が取得できています.これは 150 % に相当します.
この時,TForm.PixelsPerInch の値も 144 になっています.
図3
Delphi XE5 のランタイムテーマが有効な場合
図4
本ページに掲載のマニフェストを使用した場合
拡大率は 150 %
取得した DPI は 144
copy code
// =============================================================================
// 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 % の場合
下図は,ディスプレイの拡大率を 175 % にした結果です.
最初の図は,プロジェクトのオプションでランタイムを有効にした結果です.これがデフォルト結果です.その次の図は,カスタムマニフェストとして,本ページに掲載しているマニフェストファイルを使用した場合です.DPI として 168 という値が取得できています.これは 175 % に相当します.
図6
Delphi XE5 のランタイムテーマを有効にして実行した場合
図7
本ページに掲載のマニフェストを使用した場合
拡大率は 175 %
取得した DPI は 168
copy code
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 % の場合
下図は,ディスプレイの拡大率を 125 % にした結果です.
最初の図は,プロジェクトのオプションでランタイムを有効にした結果です.これがデフォルト結果です.その次の図は,カスタムマニフェストとして,本ページに掲載しているマニフェストファイルを使用した場合です.DPI として 120 という値が取得できています.これは 125 % に相当します.
図9
Delphi XE5 のランタイムテーマが有効な場合
図10
本ページに掲載のマニフェストを使用した場合
拡大率は 125 %
取得した DPI は 120
copy code
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 % の場合
ディプレイの拡大率を 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
copy code
// =============================================================================
// レジストリから 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 は,拡大率から次の計算で求まります.
図19
拡大率が 100 % の場合
DPI は 96
最初の値は,GetWindowRect で取得した幅
設計時のフォームの Width プロパティの値
次の値は,画面上の実際の幅
最後の値は,その比率
1.00 なので 100 %
これは,DWM のフレーム幅を考慮した結果
ディススプレイの拡大率を 175 % にした時の結果です.
図20
ディスプレイの拡大率を取得
拡大率が 175 % の場合
数値の意味は上の図と同じ
テストに使用したコードです.このコードは,TForm.BorderStyle が bsSizeable の時の専用です.このプロパティが別の値の時は,正しい拡大率は取得できません.
Delphi XE5 等では,関数の定義も定数名の定義も必要ありません.Uses に Winapi.Dwmapi を追加すれば利用できます.もちろん,このまま使用しても問題ありません.
copy code
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] にします.
copy code
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 .