1. 背景
Windows 8.1ではマルチモニターにしているときに、モニターごとに違うDPIで使えるようになった。これはユーザーがDPI(scaling level)を大ざっぱに指定するとOSの方で自動的に設定するもので、個別に手動設定できるわけではない。具体的には、「ディスプレイ」設定でスライダーをSmallerからLargerの間で動かして指定する(無段階ではない)。
下にある「Let me choose one scaling level for all my displays」をチェックすれば直接DPIを指定できるようになるが(これも無段階ではない)、その場合DPIは共通になるので、モニターごとに変えることはできない。
どうせならモニターごとに全て手動設定できるようにして欲しい気もするが、推測すれば、大多数のユーザーにとっては自動設定の方がよいと判断した、あるいは細かいカスタマイズを許すとUIの動作検証で死ぬ、という事情があるのかなと思う。アプリを作る側としても後者は無視できない問題なので。
ともかく、デスクトップアプリはこうしたモニターごとに違うDPIに対応できるように、というのがMicrosoftからの宿題になる。
2. コーディング
基本情報としては以下のとおり。
- BUILD 2013におけるプレゼンテーション(Making Your Desktop Apps Shine on High-DPI Displays)
- ペーパー(Writing DPI-Aware Desktop Applications in Windows 8.1 Preview)
- サンプルコード(DPI Tutorial sample)
- DPIの変化を知らせるウィンドウメッセージ: WM_DPICHANGED
- モニターごとのDPIを取得するAPI: GetDpiForMonitor
- アプリがDPI Awareであることを示すマニフェストの拡張: 「Per Monitor」と「True/PM」
WM_DPICHANGED
このウィンドウメッセージのID番号はhiyohiyoさんに教えていただいて0x02E0と分かったので(Visual Studio 2013 PreviewのSDK中のWinUser.hにある)、後はLOWORDとHIWORDのマクロをメソッド化して作成。
Protected Overrides Sub WndProc(ByRef m As Message)
MyBase.WndProc(m)
Const WM_DPICHANGED As Integer = &H2E0 '0x02E0 from WinUser.h
If (m.Msg = WM_DPICHANGED) Then
'wParam
Dim lo As Single = GetLoWord(m.WParam.ToInt32)
Dim hi As Single = GetHiWord(m.WParam.ToInt32)
Me.TextBox_LoWord.Text = lo.ToString()
Me.TextBox_HiWord.Text = hi.ToString()
'lParam
Dim r As RECT = CType(Marshal.PtrToStructure(m.LParam, GetType(RECT)), RECT)
Me.TextBox_Position.Text = String.Format("{0},{1}", r.top, r.left)
Me.TextBox_Size.Text = String.Format("{0}x{1}", r.bottom - r.top, r.right - r.left)
End If
End Sub
Private Function GetLoWord(dword As Int32) As Int16
Return Convert.ToInt16(dword And &HFFFF)
End Function
Private Function GetHiWord(dword As Int32) As Int16
Return Convert.ToInt16(dword >> 16)
End Function
<StructLayout(LayoutKind.Sequential)>
Private Structure RECT
Private left As Integer
Private top As Integer
Private right As Integer
Private bottom As Integer
End Structure
GetDpiForMonitor
これはまずアプリのウィンドウハンドルを取得し、次に現在のモニターのハンドルをMonitorFromWindowをP/Invokeで実行して取得し、その後にGetDpiForMonitorをP/Invokeで実行する流れで作成。
Private Sub Button_GetDpi_Click(sender As Object, e As EventArgs) Handles Button_GetDpi.Click
'Get handle to this window.
Dim windowHandle As IntPtr = Process.GetCurrentProcess().MainWindowHandle
'Get handle to monitor that contains this window.
Dim monitorHandle As IntPtr = MonitorFromWindow(windowHandle, MONITOR_DEFAULTTOPRIMARY)
'Get DPI (If the OS is not Windows 8.1 or newer, calling GetDpiForMonitor will cause exception).
Dim dpiX As UInteger
Dim dpiY As UInteger
Dim result As Integer
Try
result = GetDpiForMonitor(monitorHandle,
Monitor_DPI_Type.MDT_Default,
dpiX, dpiY)
Catch ex As Exception
result = 1 'Not S_OK (= 0)
End Try
If (result = 0) Then 'If S_OK (= 0)
Me.TextBox_dpiX.Text = dpiX.ToString()
Me.TextBox_dpiY.Text = dpiY.ToString()
Else
Me.TextBox_dpiX.Text = "Failed"
Me.TextBox_dpiY.Text = "Failed"
End If
End Sub
<DllImport("user32.dll", SetLastError:=True)>
Private Shared Function MonitorFromWindow(ByVal hwnd As IntPtr,
ByVal dwFlags As Integer) As IntPtr
End Function
Private Const MONITORINFOF_PRIMARY As Integer = &H1
Private Const MONITOR_DEFAULTTONEAREST As Integer = &H2
Private Const MONITOR_DEFAULTTONULL As Integer = &H0
Private Const MONITOR_DEFAULTTOPRIMARY As Integer = &H1
<DllImport("Shcore.dll", SetLastError:=True)>
Private Shared Function GetDpiForMonitor(ByVal hmonitor As IntPtr,
ByVal dpiType As Monitor_DPI_Type,
ByRef dpiX As UInteger,
ByRef dpiY As UInteger) As Integer
End Function
Private Enum Monitor_DPI_Type As Integer
MDT_Effective_DPI = 0
MDT_Angular_DPI = 1
MDT_Raw_DPI = 2
MDT_Default = MDT_Effective_DPI
End Enum
MonitorFromWindowの第2引数とGetDpiForMonitorの第2引数の選択は要検討。なお、GetDpiForMonitorは「Shcore.dll」のない環境では当然ながら例外を起こす。DPI Awareのマニフェスト
プロジェクトにマニフェストファイルを追加し、そこに<dpiAware>を記述。
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>Per Monitor</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
ここの値を「Per Monitor」(ハイフンなし)か「True/PM」にしないとWM_DPICHANGEDは来ない模様。3. テスト
このアプリの見た目は以下のとおりで、コード中の記述との対応関係は見れば分かると思う。「Get DPI」のボタンは手動でGetDpiForMonitorを実行するもの。最初にDPI120のモニター上にある状態。
これをドラッグしてDPI96のモニターに移すとこうなる。
さらにドラッグしてDPI120のモニターに戻すとこうなる。
まずは成功で、見ているとアプリのウィンドウがモニターの境界を半分過ぎたぐらいでWM_DPICHANGEDが来ている感じ。これで問題なしかというとそうでもなくて、スライダーを動かして「適用」したときにDPIが変わっても反応がなかったり、一旦サインアウトしないと効かないこともあったりして、「あれー?」というところはある。
ともあれ、以上のように割と少ない手間で実装はできると思う。その上でDPIに応じたUIを作り込むのはまた別問題だけど。
[追記] Windows 8.1のバージョンの取得
Windows 8.1のバージョン番号は6.3で、Windows 8の6.2からマイナー番号が1つ上がっている。これはverコマンドでも確認できる。
が、.NETのSystem.Environment.OSVersionで見ても、Win32のGetVersionExで見てもバージョンは「6.2.9200」と返ってくるので、「ベータだからか~」と思っていた(念のため、Visual Studio 2013 Previewでアプリを作成し、Visual Studioの中から実行するとちゃんとWindows 8.1のバージョンが返ってくる。ただし、同じアプリをVisual Studioの外から実行するとダメ)。
実際はそういうことではなくて、意図的なものだった。
GetVersionExはあえて古いバージョンを返すように変更されたということで、System.Environment.OSVersionについて言及はないが、内部的にGetVersionExを呼び出しているようなので同じ結果になる。
この対応策としては以下の二通り。
- 対応しているOSとしてWindows 8.1をマニフェストに追加する。
- Win32のVerifyVersionInfoを使う。
具体的には、以下のようなもの。
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
</application>
</compatibility>
これでSystem.Environment.OSVersionも、GetVersionExも普通に「6.3.9431」を返してくるようになった。というわけで、OSのバージョンを見てPer-Monitor DPI対応を切り替えることができる。一応VerifyVersionInfoを使った方も。Windows 8.1以降か否かを判別可能。
Private Sub CheckEightOneOrNewer()
'Set expected OS version.
Dim osvi As New OSVERSIONINFOEX()
osvi.dwMajorVersion = 6 'Major version number
osvi.dwMinorVersion = 3 'Minor version number
osvi.dwOSVersionInfoSize = Convert.ToUInt32(Marshal.SizeOf(osvi))
'Set condition mask (equal to or newer than designated OS version).
Dim cm As UInt64 = 0
cm = VerSetConditionMask(cm, VER_MAJORVERSION, VER_GREATER_EQUAL)
cm = VerSetConditionMask(cm, VER_MINORVERSION, VER_GREATER_EQUAL)
'Perform VerifyVersionInfo.
Dim result As Boolean = VerifyVersionInfoW(osvi,
VER_MAJORVERSION Or VER_MINORVERSION,
cm)
If (result = True) Then
MessageBox.Show("OS is Windows 8.1 or newer.")
Else
If (Marshal.GetLastWin32Error() = 1150) Then 'If ERROR_OLD_WIN_VERSION
MessageBox.Show("OS is older than Windows 8.1.")
Else
MessageBox.Show("Failed to check OS version.")
End If
End If
End Sub
<DllImport("kernel32", SetLastError:=True)>
Private Shared Function VerifyVersionInfoW(ByVal osvi As OSVERSIONINFOEX,
ByVal dwTypeMask As UInt32,
ByVal dwlConditionMask As UInt64) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<DllImport("kernel32", SetLastError:=True)>
Private Shared Function VerSetConditionMask(ByVal dwlConditionMask As UInt64,
ByVal dwTypeBitMask As UInt32,
ByVal dwConditionMask As Byte) As UInt64
End Function
<StructLayout(LayoutKind.Sequential)>
Private Structure OSVERSIONINFOEX
Public dwOSVersionInfoSize As UInt32
Public dwMajorVersion As UInt32
Public dwMinorVersion As UInt32
Public dwBuildNumber As UInt32
Public dwPlatformId As UInt32
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=128)>
Public szCSDVersion As String
Public wServicePackMajor As UInt16
Public wServicePackMinor As UInt16
Public wSuiteMask As UInt16
Public wProductType As Byte
Public wReserved As Byte
End Structure
Private VER_MINORVERSION As UInt32 = &H1
Private VER_MAJORVERSION As UInt32 = &H2
Private VER_BUILDNUMBER As UInt32 = &H4
Private VER_PLATFORMID As UInt32 = &H8
Private VER_SERVICEPACKMINOR As UInt32 = &H10
Private VER_SERVICEPACKMAJOR As UInt32 = &H20
Private VER_SUITENAME As UInt32 = &H40
Private VER_PRODUCT_TYPE As UInt32 = &H80
Private VER_EQUAL As Byte = 1
Private VER_GREATER As Byte = 2
Private VER_GREATER_EQUAL As Byte = 3
Private VER_LESS As Byte = 4
Private VER_LESS_EQUAL As Byte = 5
Private VER_AND As Byte = 6
Private VER_OR As Byte = 7







2 コメント :
次期OfficeもModernUI版としてリリースすると公式に示したように
MSの動きを見ていると、てっきりModernUIのみ高DPI環境を整備して、
DesktopUIはあくまでも既存アプリケーションとの互換性維持の為に残すだけで
高DPI対応は事実上放置に近いのではないのかと思ってましたが・・・
そういう訳でもないんですかね?スタンスはっきりして欲しいなぁ。
シノフスキーが辞めた前後で路線に修正があったのかなと推測してますが、元々大画面のモニターで使うデスクトップでWindowsストアアプリは無理があったんだろうと思います。
コメントを投稿