DPI 스케일링
DPI 스케일링 (DPI Scaling)
AutoHotkey와 관련된 DPI 스케일링의 두 가지 유형(Gui DPI 스케일링과 OS DPI 스케일링)과 이를 다루는 방법을 설명하는 문서예요.
출처: 문서
본문
DPI 스케일링은 운영 체제나 응용 프로그램이 디스플레이의 "도트 퍼 인치(dots per inch)" 설정에 비례해서 콘텐츠의 시각적 크기를 키우기 위해 수행하는 기능이에요. 일반적으로 서로 다른 디스플레이 해상도를 가진 시스템에서 콘텐츠가 같은 물리적 크기로 보이게 하거나, 매우 높은 해상도의 디스플레이에서도 적어도 사용할 수 있게 해줘요. 때로는 콘텐츠를 더 크고 읽기 편하게 만들기 위해 사용자가 DPI 설정을 올리기도 해요.
A_ScreenDPI는 보통 스크립트가 시작될 때 주 디스플레이가 가졌던 DPI 설정을 반환해요. 이것을 "시스템 DPI(system DPI)"라고 알려져 있으며, 서로 다른 시점에 시작된 프로세스는 다른 값을 가질 수 있어요.
AutoHotkey와 관련된 DPI 스케일링에는 두 가지 유형이 있어요: Gui DPI 스케일링과 OS DPI 스케일링.
Gui DPI 스케일링 (Gui DPI Scaling)
Gui와 GuiControl 메서드/프로퍼티는 기본적으로 자동 스케일링을 수행해서, 하드코딩된 위치·크기·여백을 가진 GUI 스크립트가 높은 DPI 화면에서 적절히 스케일되는 경향이 있어요. 이것이 스크립트를 방해하거나, 스크립트가 자체 스케일링을 할 것이라면 자동 스케일링을 비활성화할 수 있어요. 자세한 내용은 -DPIScale 옵션을 참조해요.
OS DPI 스케일링 (OS DPI Scaling)
DPI를 인식하지 않는 응용 프로그램의 경우, 운영 체제가 일부 시스템 함수에 전달되고 반환되는 좌표를 자동으로 스케일해요. 이런 유형의 스케일링은 보통 두 가지 시나리오에서 AutoHotkey에 영향을 줘요:
- 모든 모니터가 같은 스케일로 설정되지 않은 다중 모니터 시스템.
- 디스플레이 스케일 설정이 프로그램이 시작될 때와 다를 때마다.
실제 스케일링은 어느 시스템 함수가 호출되는지, 스크립트의 DPI 인식도, 그리고 잠재적으로 대상 창의 DPI 인식도에 따라 달라져요.
모니터별 DPI 인식 (Per-Monitor DPI Awareness)
Windows 8.1 이상에서는 보조 화면이 다른 DPI 설정을 가질 수 있으며, "모니터별 DPI 인식(per-monitor DPI-aware)" 응용 프로그램은 현재 있는 화면의 DPI에 따라 창을 스케일하고, 창이 화면 사이를 이동할 때 동적으로 적응할 것으로 기대돼요.
모니터별 DPI 인식을 하지 않는 응용 프로그램의 경우, 시스템이 비트맵 스케일링을 수행해서 창이 화면 사이를 이동할 때 크기를 바꿀 수 있게 하고, 응용 프로그램이 기대하는 전역 DPI 설정으로 스케일된 좌표와 크기를 보고함으로써 이것을 응용 프로그램에게 숨겨요. 예를 들어 11인치 4K 화면에서 96dpi(100%)로 표시되도록 설계된 GUI는 거의 사용할 수 없지만, 200%로 업스케일하면 사용할 수 있게 돼요.
AutoHotkey v2.0은 모니터별 스케일링을 수행하도록 설계되지 않았으므로, 모니터별 DPI 인식으로 표시되지 않았어요. 예를 들어 100% DPI의 큰 외부 화면과 200% DPI의 작은 화면 사이에서 GUI 창을 옮길 때 이것은 장점이에요. 하지만 자동 스케일링에는 부정적인 영향이 있어요.
시스템의 자동 스케일링이 동작하도록, MoveWindow 및 GetWindowRect 같은 시스템 함수는 받아들이거나 반환하는 좌표를 자동으로 스케일해요. AutoHotkey가 이러한 함수를 사용해 외부 창을 다룰 때, 좌표가 주 화면에 없으면 예상치 못한 결과가 나오는 경우가 많아요. 더 혼란스럽게도, 일부 함수는 스크립트의 마지막 활성 창이 표시된 화면을 기준으로 좌표를 스케일해요.
우회 방법 (Workarounds)
Windows 10 버전 1607 이상에서는 SetThreadDpiAwarenessContext 시스템 함수를 사용해 런타임에 프로그램의 DPI 인식 설정을 바꿀 수 있어요. 예를 들어, 모니터별 DPI 인식을 활성화하면 시스템이 수행하는 스케일링이 비활성화되므로 WinMove와 WinGetPos 같은 내장 함수가 DPI 스케일링의 영향을 받지 않은 픽셀 단위의 좌표를 받거나 반환해요. 하지만 100% DPI의 화면용으로 크기가 맞춰진 GUI를 200% DPI의 화면으로 옮기면 자동으로 조정되지 않아 사용하기 매우 어려울 수 있어요.
모니터별 DPI 인식을 활성화하려면 보통 DPI 스케일링의 영향을 받는 함수를 사용하기 전에 다음 함수를 호출해요:
DllCall("SetThreadDpiAwarenessContext", "ptr", -3, "ptr")
Windows 10 버전 1703 이상에서는 -3을 -4로 바꿔 "Per Monitor v2" 모드를 활성화할 수 있어요. 이것은 대화 상자, 메뉴, 툴팁 및 기타 몇 가지의 스케일링을 활성화해요. 하지만 비클라이언트 영역(타이틀 바)도 스케일링되어, 스크립트가 (WM_DPICHANGED 메시지에 응답하는 것처럼) 그것을 조정하도록 설계되지 않으면 창의 클라이언트 영역이 너무 작아질 수 있어요. GUI를 만들기 전에 컨텍스트를 -3으로 설정하고, 툴팁·메뉴·대화 상자를 만들기 전에는 -4로 설정하면 이 문제를 피할 수 있어요.
새 스레드 (New Threads)
시스템이 창의 창 프로시저(window procedure)를 호출할 때, 창이 만들어질 때 사용 중이던 컨텍스트로 현재 DPI 인식 컨텍스트를 자동으로 설정해요. 따라서 새 스크립트 스레드의 컨텍스트는 그것이 AutoHotkey의 메시지 루프에서 직접 시작되었는지, 창 프로시저를 통해 시작되었는지에 따라 달라져요.
- 일반적인 조건에서, 게시된(posted) 메시지는 AutoHotkey의 메시지 루프에서 직접 처리되는데, 이는 이미 적용 중이던 어떤 컨텍스트든 그대로 유지된다는 뜻이에요. 여기에는 핫키, 핫스트링, 타이머처럼 새 스크립트 스레드를 시작하는 대부분의 이벤트가 포함돼요.
- (게시되지 않고) 보내진(sent) 메시지에 해당하는 스크립트 스레드는 창 프로시저 안에서 시작되는데, 이는 컨텍스트가 항상 대상 창을 기준으로 설정된다는 뜻이에요.
- 게시된 메시지는 보통 모달 메시지 루프 중에 받으면 창 프로시저로 디스패치돼요. 모달 메시지 루프는 예를 들어 모달 대화 상자와 메뉴에서, 또는 사용자가 창을 이동하거나 드래그-드롭을 수행하는 동안 사용돼요.
- 비GUI 이벤트는 스크립트의 메인 창과 연관되므로 프로그램 기본 DPI 인식 컨텍스트를 받아요. 이것은 보통 프로그램의 매니페스트에 설정된 대로지만, 응용 프로그램 호환성 설정으로 덮어쓸 수 있어요.
혼합 설정 (Mixed Settings)
모니터별 DPI 인식 GUI 창은 WM_DPICHANGED 메시지를 받으면 자동으로 조정될 것으로 기대돼요. AutoHotkey v2.0 GUI 창은 이 메시지에 기본적으로 응답하지 않아요. 이런 유형의 동적 스케일링을 올바르게 구현하는 것이 너무 어렵다면, 더 간단한 대안은 GUI를 만들기 직전에 모니터별 DPI 인식을 일시적으로 비활성화하는 것이에요. 예를 들어:
*; AutoHotkey v2.0에서 기본인 "시스템 DPI 인식" 모드로 설정:*
try dac := DllCall("SetThreadDpiAwarenessContext", 'ptr', -2, 'ptr')
*; GUI를 만들면 영구적으로 "시스템 DPI 인식"이 됨:*
MyGui := Gui()
*; 이후의 함수 호출을 위해 이전 모드 복원:*
IsSet(dac) && DllCall("SetThreadDpiAwarenessContext", 'ptr', dac, 'ptr')
OS가 SetThreadDpiAwarenessContext를 지원하지 않거나 프로그램이 이미 시스템 DPI 인식 모드였다면 추가 줄들은 효과가 없어요.
GUI의 컨트롤 중 일부만 스케일이 잘 안 된다면, 모니터별 DPI 인식 창에 시스템 DPI 인식(또는 DPI 미인식) 컨트롤을 호스팅할 수 있어요. 혼합 호스팅은 창을 만들기 전에 활성화해야 해요(Windows 10 버전 1803 이상 필요):
*; 덜 인식하는 자식 창을 호스팅할 수 있는 GUI 창 만들기:*
try dhb := DllCall("SetThreadDpiHostingBehavior", 'int', 1)
MyGui := Gui()
IsSet(dhb) && DllCall("SetThreadDpiHostingBehavior", 'int', dhb)
*; "시스템 DPI 인식" 컨트롤 추가:*
try dac := DllCall("SetThreadDpiAwarenessContext", 'ptr', -2, 'ptr')
MyListView := MyGui.AddListView()
IsSet(dac) && DllCall("SetThreadDpiAwarenessContext", 'ptr', dac, 'ptr')
컴파일된 스크립트 (Compiled Scripts)
컴파일된 스크립트의 매니페스트(내장된 XML 리소스)에 "dpiAware" 및 "dpiAwareness" 요소를 설정하면 프로세스 전체에 모니터별 DPI 인식을 활성화할 수 있어요. 이 설정들의 올바른 사용법과 효과에 대한 자세한 내용은 응용 프로그램 매니페스트로 기본 인식 설정을 참조해요. 예를 들어, AutoHotkey v2.0.19의 매니페스트에는 다음이 포함돼요:
<v3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"
xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<dpiAware>true</dpiAware>
<ws2:longPathAware>true</ws2:longPathAware>
</v3:windowsSettings>
Microsoft 문서에 설명된 대로, 서로 다른 XML 네임스페이스에 속하는 "dpiAware"와 "dpiAwareness"를 둘 다 포함하는 것이 좋을 수 있어요. "longPathAware"와 "dpiAwareness"는 같은 네임스페이스에 속하므로 XML은 일부를 옮겨서 최적화할 수 있어요. 다음은 (가능하면 v2, 그렇지 않으면 v1) 모니터별 DPI 인식을 활성화해요:
<v3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness>PerMonitorV2</dpiAwareness>
<longPathAware>true</longPathAware>
</v3:windowsSettings>
호환성 설정 (Compatibility Settings)
프로그램의 기본 DPI 인식은 AutoHotkey 실행 파일의 속성, 바로 가기 파일의 속성에서 설정하거나, __COMPAT_LAYER 환경 변수에 DpiUnaware 키워드 또는 HighDpiAware 키워드를 포함시켜 설정할 수 있는 호환성 설정으로 덮어쓸 수 있어요. 이 방법으로 DPI 인식을 활성화하면 원치 않는 효과가 있을 수 있어요. 특히 MsgBox 창은 화면 사이를 이동할 때 자동으로 조정되지 않을 수 있어요.