このドキュメントは次の環境で記述されていますが、 他の開発環境でもほぼ同様の考え方で問題なく使えます。
- オペレーシングシステム:Windows 10 (Windows Vista以降が必要)
- 対応言語: C / C++ / C#
- 対応コンパイラ: gccまたはg++ (minGW) / csc
GetDeviceCaps
のドキュメントには
LOGPIXELSX
やLOGPIXELSY
を受け渡すことで DPIを取得できると書かれていますが、
実際にはこの関数は常に96を返します。このことは以下に明記されています。
This API will always return 96 https://technet.microsoft.com/en-us/library/dn528846.aspx#system
というのも、 高DPIディスプレイは近年普及し始めたもので、 古いアプリケーションのほとんどは、DPI96を前提にして作られています。 そのため、常に96を返すことで、これらのレガシーなアプリケーションとの互換性を維持するためと思われます。
後述するような方法で DPI Aware (高DPI対応) なアプリであることをWindowsに明示することで、
GetDeviceCaps
が正しい値を返すようになります。
ちなみにDPI Awareではない場合、Windows側でアプリケーションの描画内容を自動的に引き延ばして表示し、 互換的に表示サイズを維持する仕組みが取られているようです。 (このため、高DPIのモニタやタブレットで一部のアプリケーションを開くと、ピンボケしたような表示になる経験があると思います)
Windowsに DPI Aware であることを示す方法は次の2通りがあります。
SetProcessDPIAware
を呼び出す- manifestファイルを使う
SetProcessDPIAware
(Windows Vista以降のuser32.dllに実装) を呼び出すだけです。
たとえば次のC#アプリケーションではGetDeviceCaps
は正常なDPIを返します。
//test.cs
using System;
using System.Runtime.InteropServices;
class Program {
const int LOGPIXELSX = 88;
const int LOGPIXELSY = 90;
[DllImport("user32.dll")]
extern static bool SetProcessDPIAware();
[DllImport("user32.dll")]
extern static IntPtr GetWindowDC(IntPtr hwnd);
[DllImport("gdi32.dll")]
extern static int GetDeviceCaps(IntPtr hdc, int index);
[DllImport("user32.dll")]
extern static int ReleaseDC(IntPtr hwnd, IntPtr hdc);
static int Main() {
SetProcessDPIAware();
var dc = GetWindowDC(IntPtr.Zero);
Console.WriteLine("DpiX: {0}", GetDeviceCaps(dc, LOGPIXELSX));
Console.WriteLine("DpiY: {0}", GetDeviceCaps(dc, LOGPIXELSY));
ReleaseDC(IntPtr.Zero, dc);
return 0;
}
}
cscを使ったコンパイルと実行結果は次の通りです。(DPI192環境の場合)
PS> csc test.cs
PS> ./test.exe
DpiX: 192
DpiY: 192
ただしSetProcessDPIAware
のドキュメントには、
以下の manifestファイルを使う方法を推奨することが記されています。
(もともとこの関数はDLLがアプリのDPI Awareの設定を引き継ぐために用意されたもののようで、
上記のような使い方は本来の目的とは異なるようです)
manifestファイルを新規に作成するか、すでにmanifestファイルがある場合は、次のように
xmlns:asmv3=...
の箇所と、asmv3:application
のセクションを書き足してください。
<?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>
</assembly>
manifestファイルは次の2つの方法でexeに埋め込む必要があります。
この方法が一番簡単ですがcscを使う都合上、C#である必要があります。
次のサンプルアプリケーションをコンパイルするとします。
(SetProcessDPIAware
をコメントアウトしている点をご確認ください)
//test.cs
using System;
using System.Runtime.InteropServices;
class Program {
const int LOGPIXELSX = 88;
const int LOGPIXELSY = 90;
// [DllImport("user32.dll")]
// extern static bool SetProcessDPIAware();
[DllImport("user32.dll")]
extern static IntPtr GetWindowDC(IntPtr hwnd);
[DllImport("gdi32.dll")]
extern static int GetDeviceCaps(IntPtr hdc, int index);
[DllImport("user32.dll")]
extern static int ReleaseDC(IntPtr hwnd, IntPtr hdc);
static int Main() {
// SetProcessDPIAware();
var dc = GetWindowDC(IntPtr.Zero);
Console.WriteLine("DpiX: {0}", GetDeviceCaps(dc, LOGPIXELSX));
Console.WriteLine("DpiY: {0}", GetDeviceCaps(dc, LOGPIXELSY));
ReleaseDC(IntPtr.Zero, dc);
return 0;
}
}
cscでコンパイル、実行の結果は次の通りです。(DPI192環境の場合)
PS> csc /win32manifest:test.manifest test.cs
PS> ./test.exe
DpiX: 192
DpiY: 192
DPIは正しく出力されたはずです。 高DPI設定にも関わらず、96が返ってしまう場合は、 manifestのxmlnsの内容にスペルミスなどがないかご確認ください。
この方法はC#に加えて、CやC++でも可能です。
以下はC++とMinGWをだけを使ってDPI Awareなアプリを作ってみます。
次のようなC++コードがあるとします。
// test.cc
#include <iostream>
#include <windows.h>
using namespace std;
int main() {
auto dc = GetWindowDC(NULL);
cout << "DpiX: " << GetDeviceCaps(dc, LOGPIXELSX) << endl;
cout << "DpiY: " << GetDeviceCaps(dc, LOGPIXELSY) << endl;
ReleaseDC(NULL, dc);
}
次のようなRCファイルを作ります。 (DLLを作成する場合は以下の1の箇所を2にする必要があるようです)
// test.rc
1 MANIFEST test.manifest
次のように g++ と windres (どちらもMinGWに付属) を使いコンパイルし、実行した結果が次の通りです。(DPI192環境の場合)
PS> windres test.rc test_rc.o
PS> g++ -c -std=c++11 test.cc -o test.o
PS> g++ test.o test_rc.o -o test.exe -lgdi32
PS> .\test.exe
DpiX: 192
DpiY: 192
違いを確認されたい場合は、試しに g++ -std=c++11 test.cc -o test2.exe -lgdi32
もコンパイルしてみてください。
リソースを埋め込んだtest.exeと違いtest2.exeはディスプレイのDPI設定に関係なく常に96を返します。
上記サンプルはC++ですが、Cコードの場合は上記コンパイル手順のg++をgccに変えるだけで問題ないと思います。
なお、C#の場合も使うツールに違いはあれど手順は同じです。
rc.exe (Windows SDK)を使い、
RCをコンパイルしRESファイルを生成し、cscの/win32res
に指定します。
System.Drawing.Graphics
クラスのDpiX
およびDpiY
プロパティも常に96を返しますが、
上記の手順を踏むことで正しい数字を返すようになります。
また、System.Windows.Forms.Screen
クラスのBoundsなども正しいピクセル数を返すようになります。