Skip to content

Instantly share code, notes, and snippets.

@retorillo
Last active April 21, 2024 08:03
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save retorillo/4e0c4a3cf4c7096e05ac to your computer and use it in GitHub Desktop.
Save retorillo/4e0c4a3cf4c7096e05ac to your computer and use it in GitHub Desktop.
GetDeviceCapsが常にDPI96を返す問題と解決方法について

GetDeviceCapsが常にDPI96を返す問題と解決方法について

このドキュメントについて

このドキュメントは次の環境で記述されていますが、 他の開発環境でもほぼ同様の考え方で問題なく使えます。

  • オペレーシングシステム:Windows 10 (Windows Vista以降が必要)
  • 対応言語: C / C++ / C#
  • 対応コンパイラ: gccまたはg++ (minGW) / csc

互換性を維持するために常にDPI96を返す

GetDeviceCapsドキュメントには LOGPIXELSXLOGPIXELSYを受け渡すことで 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ファイルを使う

方法1: SetProcessDPIAwareを呼び出す

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の設定を引き継ぐために用意されたもののようで、 上記のような使い方は本来の目的とは異なるようです)

方法2: manifestファイルを使う

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の/win32manifestオプションを使う(C#)

この方法が一番簡単ですが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の内容にスペルミスなどがないかご確認ください。

resファイルに埋め込む方法(C/C++/C#)

この方法は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 や System.Windows.Forms.Screen などについて

System.Drawing.GraphicsクラスのDpiXおよびDpiYプロパティも常に96を返しますが、 上記の手順を踏むことで正しい数字を返すようになります。 また、System.Windows.Forms.ScreenクラスのBoundsなども正しいピクセル数を返すようになります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment