WinForms의 고DPI 대응 - 4K 모니터에서 흐려지거나 깨지는 원인과 현실적인 대처법

· · WinForms, 고 DPI, Windows, .NET, C#, .NET Framework, 레거시 유지보수, UI, 기술 상담

“새 노트북으로 교체했더니 업무 앱의 글자가 뿌옇게 번져서 읽기 어렵다”, “4K 모니터에 연결했더니 버튼과 라벨이 겹쳐 보인다”, “고객사의 125% 환경에서만 보고서 미리보기가 깨진다”. 최근 몇 년 사이 PC를 교체하는 시기에 이런 종류의 상담이 급증하고 있습니다. Full HD를 넘는 고해상도 디스플레이와 125~200%의 화면 스케일링이 업무용 PC에서도 표준이 되면서, 96 DPI·100% 표시 시대에 만들어진 WinForms 앱이 그대로는 편하게 표시되지 않게 되었기 때문입니다. 오래된 WinForms 앱의 유지보수 상담 중에서 지금 가장 빈도가 높은 주제라고 해도 좋을 정도입니다.

까다로운 점은 증상이 “흐려진다”, “깨진다”, “일부만 작다”처럼 제각각으로 보인다는 것입니다. 실제로는 원인을 몇 가지로 정리할 수 있으며, 어떤 증상이 어떤 원인에 대응하는지 알면 고치는 방법도, “어디까지 대응할지”의 판단도 가능해집니다. 이 글에서는 Windows의 DPI 스케일링 구조와 DPI 인식 모드를 바탕으로, WinForms 앱(.NET / .NET Framework 양쪽 모두)의 고DPI 대응을 설정 방법·대표적인 함정·단계적 대응 전략까지 실무 관점에서 정리합니다.

1. 결론부터

  • 고DPI 환경에서 오래된 앱이 흐려지는 것은 버그가 아니라 OS의 구제 조치(DPI 가상화)입니다. DPI에 대응하지 않는 앱을 Windows가 96 DPI로 그리게 한 다음 비트맵으로 확대하기 때문에, 레이아웃은 깨지지 않는 대신 흐려집니다.1
  • 반대로 “선명한데 깨진다”는 DPI 대응을 선언했지만 레이아웃이 따라가지 못하는 상태입니다. 흐려짐과 깨짐은 원인이 정반대이므로, 대처하기 전에 어느 쪽인지 구분하시기 바랍니다.
  • 대응의 첫걸음은 자신의 앱이 지금 어떤 DPI 인식 모드(Unaware / System Aware / Per-Monitor V2)로 동작하고 있는지 파악하는 것입니다. 매니페스트·app.config·API 호출 중 어느 것으로(또는 아무것도) 선언하고 있는지 확인합니다.2
  • 설정 방법은 세대에 따라 다릅니다. .NET(Core 3.1~.NET 8)이라면 프로젝트 파일이나 Application.SetHighDpiMode, .NET Framework 4.7 이상이라면 app.config, 4.6 이하는 매니페스트로 System Aware까지가 현실적인 선입니다.34
  • 디자이너는 반드시 100%(96 DPI) 모니터에서 열 것. 150% 환경에서 폼을 열고 저장하면 AutoScaleDimensions가 바뀌어, 팀 전체의 레이아웃이 깨지는 대표적인 사고로 이어집니다.5
  • 전면 대응(Per-Monitor V2)은 작업량이 큽니다. “System Aware로 주 모니터만 선명하게”를 1단계, PMv2 완전 대응을 2단계로 삼는 2단계 접근으로, 앱의 수명과 이용 환경에 맞는 투자를 하는 것이 현실적입니다.
  • 자체적으로 개선할 수 없거나 시간이 부족한 경우의 임시 대책으로, exe의 속성 → 호환성에서 사용자 측이 재정의할 수 있는 “고DPI 설정”도 알아두면 문의 대응이 한층 편해집니다.6

2. 왜 흐려지는가 - DPI 가상화의 원리

Windows의 화면 스케일링은 96 DPI를 100%로 하는 배율로 표현됩니다. 125% = 120 DPI, 150% = 144 DPI, 200% = 192 DPI입니다. 4K(3840×2160) 27인치 모니터를 100%로 사용하면 글자가 너무 작아지기 때문에, Windows는 기본적으로 150% 전후를 권장합니다. 즉 고해상도 디스플레이의 확산은 곧 “96 DPI가 아닌 환경의 확산”을 의미합니다.

문제는 오래된 데스크톱 앱 대부분이 “화면은 항상 96 DPI”라는 전제로 작성되어 있다는 점입니다. 좌표도 폰트도 아이콘도 픽셀 값을 그대로 사용하기 때문에, 150% 환경에서 그대로 그리면 모든 것이 물리적으로 2/3 크기로 보이게 됩니다. 그래서 Windows는 DPI 대응을 선언하지 않은 앱에게 “화면은 96 DPI다”라고 속이고, 앱이 96 DPI라고 생각하고 그린 결과를 비트맵으로 확대해서 표시합니다. 이것이 DPI 가상화(비트맵 스트레치)입니다.1

이 구조를 이해하면 증상과 원인이 깔끔하게 대응됩니다. 처음 증상을 구분할 때 다음 표를 활용하십시오.

증상 원인 상태
전체가 균일하게 번지고 흐려진다. 레이아웃은 깨지지 않는다 DPI 가상화(OS가 비트맵을 확대) DPI 비대응(Unaware). 안전하지만 지저분함
글자는 선명한데 컨트롤이 겹치거나 잘려 보인다 DPI 대응을 선언했지만 레이아웃이 따라가지 못함 어중간한 DPI 대응. 여기부터가 개선 대상
주 모니터에서는 선명한데 보조 모니터로 옮기면 흐려진다 System Aware(시작 시 주 모니터 DPI로 고정) 사양대로 동작 중. PMv2로 갈지 판단할 대상
글자 크기는 괜찮은데 아이콘·이미지만 작거나 거칠다 비트맵 자산이 96 DPI용 그대로임 이미지 자산 미대응(6장)
특정 화면·컨트롤만 깨진다 고정 좌표 레이아웃, 자체 드로잉, 서드파티 컨트롤 개별 개선 대상(6장)

중요한 것은 “흐려지는” 앱이 오히려 망가진 것이 아니라는 점입니다. OS의 구제 조치가 제대로 작동하고 있는 것입니다. 개선이란 이 구제 조치를 끊고 “스케일링은 내가 직접 하겠다”고 선언하는 것(=DPI 인식 모드를 올리는 것)이므로, 선언한 순간 레이아웃 문제가 전부 자기 책임이 됩니다. “일단 PMv2로 바꿔봤더니 오히려 더 심해졌다”는 상담은 바로 이런 구조에서 발생합니다.

3. DPI 인식 모드 정리

앱은 프로세스(정확히는 Windows 10 이후로는 최상위 창) 단위로, 자신이 어디까지 DPI를 다룰 수 있는지를 OS에 선언합니다. 모드는 실질적으로 4가지입니다.16

모드 도입 OS 앱에서 보이는 DPI 모니터 간 이동·DPI 변경 시 WinForms에서의 현실적인 위치
Unaware ── 항상 96 OS가 비트맵을 확대(흐려짐) 아무것도 선언하지 않았을 때의 기본값. 흐려지지만 깨지지 않음
System Aware Vista 로그인 시 주 모니터의 DPI로 고정 주 모니터 이외·변경 후에는 OS가 확대(흐려짐) 모든 세대에서 사용 가능. 현실적인 대응의 본선
Per-Monitor (V1) 8.1 창이 있는 모니터의 DPI 최상위 창에 알림만. 스케일링은 전부 직접 처리 프레임워크 지원이 없어 실용성이 낮음. 선택하지 않음
Per-Monitor V2 10 (1703) 창이 있는 모니터의 DPI 자식 창까지 알림. 비클라이언트 영역은 OS가 추적 완전 대응의 본선. .NET Framework 4.7+ / .NET에서 사용 가능

Per-Monitor V1과 V2의 차이는 실무에서 결정적입니다. V1은 “알림은 오지만 나머지는 전부 직접 처리”하는 순수한 Win32용 방식으로, WinForms에서 사용할 이유가 없습니다. V2는 타이틀 바·스크롤 바·메뉴 등의 비클라이언트 영역을 OS가 자동으로 스케일링하며, WinForms 쪽의 프레임워크 지원(뒤에서 설명)도 V2를 전제로 만들어져 있습니다. Per-Monitor를 선택한다면 V2가 유일한 답입니다.6

또 하나, GDI 스케일링(DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED, Windows 10 1809~)이라는 모드가 있습니다. 앱 자체는 Unaware인 채로, GDI로 그려지는 텍스트나 도형만 OS가 벡터 수준에서 확대해 주는 개선된 형태의 비트맵 확대입니다.6 코드를 전혀 바꾸지 않고도 글자의 흐려짐만 줄일 수 있는 가능성이 있어, 개선이 어려운 앱의 임시 대책으로 기억해 둘 가치가 있습니다(4.3절).

그리고 Windows 10 1607 이후로는 최상위 창 단위로 서로 다른 모드를 혼용하는 Mixed-Mode도 있어, “메인 화면만 PMv2로 만들고, 아직 고치지 못한 대화상자는 Unaware로 남겨 OS가 확대하게 한다”는 단계적 전환이 Win32 수준에서 지원됩니다.7 WinForms에서 손쉽게 쓸 수 있는 것은 아니지만, “모든 화면을 한 번에 고치지 않아도 된다”는 설계 사상은 7장의 단계적 전략으로 이어집니다.

4. 설정 방법 - 세대별 정답

자신의 앱이 어떤 세대인지에 따라 DPI 인식 모드를 선언하는 방법이 다릅니다. 여기서 잘못 짚으면 “설정했는데 반영되지 않는다”는 문제로 시간을 낭비하게 되므로, 세대별로 나누어 설명합니다.

4.1 .NET(Core 3.1~.NET 8)의 WinForms

.NET에는 Application.SetHighDpiMode가 마련되어 있으며, 템플릿이 생성하는 ApplicationConfiguration.Initialize()(.NET 6 이상)가 이를 호출합니다. 기본값은 SystemAware입니다.3 설정은 프로젝트 파일에 작성하는 것이 권장되며, Visual Studio의 디자이너도 이 설정을 참조합니다.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <!-- SystemAware(기본값) / PerMonitorV2 / DpiUnaware / DpiUnawareGdiScaled -->
    <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
  </PropertyGroup>
</Project>

주의할 점으로, 프로젝트 파일의 ApplicationHighDpiModeApplicationConfiguration.Initialize().NET 6에서 도입된 방식입니다.3 .NET Core 3.1 / .NET 5 프로젝트에서는 이 속성을 작성해도 반영되지 않으므로, 다음과 같이 Application.SetHighDpiMode를 코드에서 직접 호출합니다(ApplicationConfiguration.Initialize()를 사용하지 않는 예전 스타일의 Main에서도 마찬가지입니다). 두 경우 모두 창을 하나라도 만들기 전에 호출해야 합니다.

[STAThread]
static void Main()
{
    Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new MainForm());
}

.NET 6 이후로는 PMv2 상태에서의 컨테이너 컨트롤이나 MDI 자식 창의 스케일링이 개선되어, “200% 모니터에서 100% 모니터로 옮기면 컨트롤이 어긋난다”와 같은 .NET 5까지의 문제가 상당히 해소되었습니다.3 PMv2에서 진지하게 대응하려 한다면, .NET Framework보다 새로운 .NET 쪽이 확실히 다루기 쉽다는 것이 체감입니다. .NET Framework에서의 이전을 검토할 때 참고할 자료는 “.NET Framework에서 .NET으로의 이전 전 체크리스트“에 정리해 두었습니다.

4.2 .NET Framework 4.7 이상

.NET Framework는 4.7에서 고DPI 대응이 크게 강화되었습니다. 컨트롤 스케일링 개선, DPI 변경 이벤트(DpiChanged 계열), DeviceDpi 속성 등이 추가되었습니다. 다만 옵트인 방식이라, 다음 두 가지를 세트로 설정해야 비로소 활성화됩니다.4

먼저 매니페스트에서 Windows 10 호환성을 선언합니다(이것이 없으면 4.7의 고DPI 기능 자체가 활성화되지 않습니다).

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
  <application>
    <!-- Windows 10 호환성 선언 -->
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
  </application>
</compatibility>

다음으로 app.config의 System.Windows.Forms.ApplicationConfigurationSection에서 DPI 인식 모드를 선언합니다.

<configuration>
  <System.Windows.Forms.ApplicationConfigurationSection>
    <add key="DpiAwareness" value="PerMonitorV2" />
    <!-- 이미 자체적으로 스케일링을 처리한 화면이 있다면 개별 기능을 끌 수 있음 -->
    <!-- <add key="EnableWindowsFormsHighDpiAutoResizing" value="false" /> -->
  </System.Windows.Forms.ApplicationConfigurationSection>
</configuration>

아울러 Main의 앞부분에서 Application.EnableVisualStyles()를 호출하고 있는지 확인하십시오. 여기서 한 가지 주의할 점이 있습니다. 예전 방식인 매니페스트의 <dpiAware> / <dpiAwareness> 기술은 app.config 설정을 덮어쓰기 때문에, 4.7의 WinForms에서는 함께 사용하지 않는 것이 공식 권장 사항입니다.4 과거에 System Aware화를 위해 매니페스트에 <dpiAware>true</dpiAware>를 작성했던 앱을 4.7+PMv2로 올릴 때는, 매니페스트 쪽 선언을 정리하는 것부터 시작합니다. “app.config에 작성했는데 반영되지 않는다”의 원인은 대부분 이것입니다.

4.3 .NET Framework 4.6 이하 · VB6/MFC 혼재

4.6 이하의 WinForms에는 PMv2를 지원하는 프레임워크 코드가 없어서, 무리하게 선언해도 레이아웃 깨짐을 전부 스스로 처리해야 합니다. 이 세대의 현실적인 해법은 매니페스트로 System Aware를 선언하는 것까지입니다. OS 수준의 선언은 매니페스트가 권장되는 방법이며, <dpiAware>(Vista~)와 <dpiAwareness>(Windows 10 1607~)를 함께 작성하면 신구 OS 모두에서 의도한 대로 해석됩니다.2

<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:windowsSettings>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">system</dpiAwareness>
  </asmv3:windowsSettings>
</asmv3:application>

System Aware로 설정하면 시작 시 주 모니터의 DPI에 맞춰 WinForms의 자동 스케일링(5장)이 한 번 작동하여, 주 모니터에서는 선명하게 표시됩니다. AutoScaleMode.Font가 올바르게 설정된 폼이라면 이 선언만으로도 상당히 볼만한 상태가 되는 경우가 많습니다. 참고로 SetProcessDpiAwareness 같은 API로 실행 중에 선언하는 방법도 있지만, 창을 만든 뒤에는 변경할 수 없어서 매니페스트에서 선언하는 방법이 공식적으로 권장됩니다.8 VB6나 MFC 코드가 혼재된 앱도 사고방식은 같아서, exe의 매니페스트가 프로세스 전체의 모드를 결정합니다(MFC 자체에는 자동 스케일링 지원이 없으므로 System Aware 선언 후 화면 점검이 필수입니다. 이 세대의 자산을 다루는 방법은 “Windows의 MFC란 무엇인가“도 참고하십시오).

개선 자체가 불가능한 경우를 위한 사용자 측의 임시 대책도 소개합니다. exe를 오른쪽 클릭 → 속성 → 호환성 탭 → “고DPI 설정 변경”에서 해당 앱의 DPI 스케일링을 재정의할 수 있습니다. “고DPI 스케일 설정 재정의”를 “시스템”으로 하면 DPI 가상화를 강제(=깨지지 않지만 흐려짐)하고, “시스템(고급)”으로 하면 3장에서 다룬 GDI 스케일링이 적용되어 GDI로 그려지는 텍스트만 선명해집니다.6 만능은 아니지만, “고객사에서 오늘 당장 해결하고 싶다”는 상황에 전화로 안내할 수 있는 방법으로 지원 매뉴얼에 넣어 둘 가치가 있습니다.

5. AutoScaleMode와 디자이너의 함정

WinForms에는 DPI 인식 모드와는 별도로, 폼 자체의 자동 스케일링 메커니즘이 있습니다. 이 부분을 이해하지 못하면 System Aware로 설정해도 올바르게 스케일링되지 않습니다.

원리는 이렇습니다. 디자인 시에 각 폼(ContainerControl)은 스케일링 기준을 AutoScaleMode에, 디자인한 환경의 기준값을 AutoScaleDimensions에 기록합니다. 실행 시에는 현재 환경의 값(CurrentAutoScaleDimensions)과 비교하여, 차이가 있으면 자식 컨트롤을 한꺼번에 확대·축소합니다.9 즉 “디자인 시와 실행 시의 환경 차이”를 흡수하는 메커니즘이며, 기준값은 코드(Designer.cs)에 새겨집니다.

AutoScaleMode의 선택지는 실질적으로 2가지입니다.

AutoScaleMode 기준 특징
Font(권장·기본값) 폼의 폰트 크기 DPI가 올라가면 시스템 폰트의 실제 크기도 변하므로 DPI 추적을 겸함. 사용자의 폰트 설정 변경도 추적
Dpi 화면의 DPI DPI에만 비례. 그래픽 중심 화면에 적합
None ── 자동 스케일링 비활성. 96 DPI 값을 직접 쓴 앱이 이 상태인 경우가 많음

헷갈린다면 Font를 선택하십시오. 주의할 점으로, 기반 폼과 파생 폼에서 서로 다른 모드를 섞어 쓰면 예기치 않은 결과가 발생한다는 것이 공식적으로도 명시되어 있습니다.9 폼 상속을 사용하는 앱이라면, 모든 폼의 모드를 통일하는 점검이 먼저입니다.

그리고 이제 본론인 함정입니다. AutoScaleDimensions는 “디자인한 환경의 값”을 기록하는 메커니즘이므로, 150% 모니터에서 Visual Studio 디자이너를 열고 폼을 저장하면 Designer.cs의 AutoScaleDimensions가 150% 값(Font 모드라면 6F, 12F가 9F, 18F 등)으로 바뀝니다. 좌표·크기도 1.5배 값으로 저장됩니다. 이 폼을 100% 환경의 팀원이 빌드·실행하면 전체가 축소되어 표시되고, 차이 검토에서는 모든 컨트롤의 좌표가 바뀐 거대한 diff가 나옵니다. “고DPI 노트북을 지급받은 신입 팀원이 폼 하나를 만졌더니 저장소의 레이아웃이 깨졌다”는 고DPI 상담의 대표적인 사고입니다.

대책은 원칙적으로 하나, 디자이너는 100%(96 DPI)에서 열 것입니다. Visual Studio 자체는 DPI 대응 앱이지만, WinForms 디자이너(.NET Framework용)는 DPI에 대응하지 않기 때문에 고DPI 모니터에서 열면 노란색 정보 표시줄이 나타나 “100% 스케일링으로 Visual Studio를 재시작”하도록 안내합니다.5 이 표시줄에 따라 DPI 비대응 모드로 재시작한 다음 폼을 편집하고, 끝나면 일반 모드로 돌아가는 운영 방식을 팀의 규칙으로 정해 두십시오. 명령줄에서는 devenv /noScale로도 실행할 수 있습니다. 참고로 .NET 6 이상 프로젝트에서는 Visual Studio 2022 17.8 이상에서 프로젝트 파일에 <ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>를 설정하면, VS 전체를 재시작하지 않고도 디자이너 탭만 DPI 비대응으로 동작시킬 수 있습니다(.NET Framework 프로젝트에서는 사용할 수 없습니다).5

점검을 체계화하는 방법으로는 “Designer.cs의 AutoScaleDimensions가 규정값 이외로 바뀐 차이를 CI나 pre-commit에서 거부한다”가 손쉽고 효과적입니다. 사고는 일어난 뒤에 고치는 것보다 커밋 입구에서 막는 쪽이 비용이 적게 듭니다.

6. 깨짐의 대표적인 패턴과 고치는 법

DPI 인식 모드를 올렸을 때(또는 올리려 할 때) 깨지는 부분은 대략 다음 패턴에 해당합니다.

깨지는 것 원인 고치는 법
컨트롤이 겹치거나 잘림 고정 좌표·고정 크기 레이아웃 Anchor/Dock, TableLayoutPanel / FlowLayoutPanel로 교체. AutoSize 활용
아이콘·이미지가 작거나 거칢 96 DPI용 비트맵을 그대로 사용 여러 해상도의 이미지를 준비해 DPI에 따라 전환. 가능하면 큰 이미지 1장에서 축소
자체 드로잉(그래프, 도면, 보고서 미리보기) Graphics에 픽셀 값을 직접 사용 DeviceDpi 기준으로 좌표·선 너비·폰트를 스케일링
DataGridView의 행이 좁아짐 RowHeight 등의 픽셀 지정 AutoSizeRowsMode 활용, 또는 DPI로 스케일링한 값 설정
ToolStrip 아이콘이 콩알만 함 16×16 고정 ImageScalingSize를 DPI에 맞춰 설정
특정 서드파티/ActiveX 컨트롤만 깨짐 컨트롤 자체가 DPI 비대응 벤더의 대응 버전으로 업데이트. 없다면 그것이 대응 수준의 한계

레이아웃 교체가 작업량의 중심입니다. 반대로 말하면, 원래부터 TableLayoutPanel과 Dock으로 구성된 화면은 DPI 인식 모드를 올려도 거의 손이 가지 않습니다. 앞으로 만들 새 화면은 처음부터 레이아웃 패널로 구성한다는 규칙만 세워도 미래의 부채가 줄어듭니다.

자체 드로잉의 스케일링은 Control.DeviceDpi(.NET Framework 4.7+ / .NET)를 기준으로 96 DPI 환산 값을 변환하는 것이 기본형입니다.

public partial class ChartPanel : Panel
{
    // 96 DPI 기준의 설계 값을 현재 DPI로 변환
    private int Scale(int value96) => value96 * DeviceDpi / 96;

    protected override void OnPaint(PaintEventArgs e)
    {
        using var pen = new Pen(Color.Navy, Scale(2));
        e.Graphics.DrawRectangle(pen,
            Scale(16), Scale(16), Scale(320), Scale(120));
    }

    // PMv2에서는 모니터 간 이동 시 DPI가 바뀌므로 다시 그리도록 유도
    protected override void OnDpiChangedAfterParent(EventArgs e)
    {
        base.OnDpiChangedAfterParent(e);
        Invalidate();
    }
}

System Aware까지라면 DPI가 시작 시에 고정되므로 Scale을 도입하는 것만으로 충분하지만, PMv2에서는 모니터 간 이동마다 DeviceDpi가 바뀌기 때문에 캐시해 둔 폰트·이미지·레이아웃 값을 DpiChanged 계열 이벤트에서 다시 만드는 설계가 필요합니다.4 “자체 드로잉 화면이 몇 개인가”는 PMv2 대응의 작업량 추정에 그대로 영향을 미칩니다.

서드파티 컨트롤과 ActiveX/OCX는 이 개선의 한계를 결정하는 요소입니다. 컨트롤 자체가 DPI에 대응하지 않는다면, 호스트 쪽에서 아무리 노력해도 그 화면은 완전해질 수 없습니다. 벤더의 대응 상황을 확인하고, 업데이트를 기대할 수 없는 ActiveX에 대해서는 “ActiveX/OCX를 유지할지, 감쌀지, 교체할지“의 판단표로 처리 방침을 정한 다음, 고DPI 대응의 목표를 설정하십시오.

7. 단계적 대응 전략 - 어디까지 할지에 대한 판단표

지금까지의 내용을 바탕으로 하면, 대응 수준은 3단계로 정리할 수 있습니다. 모든 앱을 PMv2로 만드는 것이 기술적인 이상이지만, 개선 예산과 앱의 수명을 고려하면 어느 정도 타협하는 것이 정답이 되는 경우가 많습니다.

  (1) 아무것도 하지 않음(Unaware) (2) System Aware화 (3) PMv2 완전 대응
화면 모습 모든 환경에서 흐려짐(깨지지 않음) 주 모니터에서는 선명함. 보조 모니터·DPI 변경 시에는 흐려짐 모든 모니터에서 선명함
주요 작업 없음 매니페스트/설정 선언 + AutoScaleMode 점검 + 전체 화면 표시 확인 (2) + 레이아웃 전면 점검 + 자체 드로잉·이미지 자산의 DPI 대응 + DpiChanged 대응
작업량 없음 소~중(화면 수에 비례한 확인 작업이 중심) 대(깨짐 개선이 중심. 서드파티 컨트롤이 한계를 좌우)
적합한 경우 몇 년 내 폐지 예정. 사용자가 감내하고 있음 고정 데스크톱·단일 모니터 중심의 사내 앱. 1단계로 노트북 + 외부 모니터 혼용. 장기간 사용하는 주력 앱. 고객에게 배포하는 제품

판단의 축은 세 가지입니다. 앱의 수명(앞으로 몇 년 더 사용할지), 이용 환경(모두가 같은 고정 데스크톱을 쓴다면 System Aware만으로 실질적으로 마무리됩니다. 노트북 + 외부 모니터 조합이 많다면, 모니터를 옮길 때마다 흐려지는 System Aware에는 불만이 남습니다), 그리고 개선 예산입니다. 추천하는 진행 방식은, 먼저 (2)를 모든 앱의 표준으로 실시하고, 그 과정에서 발견된 깨짐의 양과 서드파티 컨트롤의 제약을 근거로 (3)으로 나아갈지, 어느 화면만 진행할지를 결정하는 것입니다. (2)는 “선언 + 점검”이 중심이라 실패 위험이 작으면서도 사용자의 체감은 크게 개선됩니다.

테스트에 대해서도 짚어 두겠습니다. 고DPI 문제는 개발 머신에서 재현되지 않는 경우가 많으므로, 다음과 같은 환경을 의도적으로 만들어 확인합니다.1

  • 스케일링이 다른 모니터 2대를 연결하고(예: 150% 노트북 내장 화면 + 100% 외부 모니터) 창을 오가며 확인한다
  • 주 모니터를 바꾼 뒤 다시 로그인하고 나서 실행한다(System DPI는 로그인 시에 고정되므로, 다시 로그인하지 않으면 전환되지 않습니다)
  • 앱이 실행 중인 상태에서 스케일링 설정을 변경한다
  • 원격 데스크톱으로 고DPI 클라이언트에서 접속한다(RDP는 클라이언트 쪽의 DPI가 그대로 반영되므로, “서버 콘솔에서는 정상인데 RDP로 접속하면 깨진다”는 흔한 보고입니다)

“125%에서만 깨진다”는 보고가 들어오면, 실제로 그 배율을 만들어서 확인하는 것이 결국 가장 빠르다는 것이 경험적인 법칙입니다. 가상 머신에서도 스케일링 설정을 바꿀 수 있으므로, 100 / 125 / 150 / 200%의 검증 환경을 미리 준비해 두면 문제 구분이 빨라집니다.

8. 정리

고DPI 환경에서의 “흐려짐”은 OS의 구제 조치(DPI 가상화), “깨짐”은 어중간한 DPI 대응 - 이 대응 관계만 파악하면 증상에서 원인과 대책을 거꾸로 찾아낼 수 있습니다. 설정 방법은 세대별로, .NET이라면 프로젝트 파일의 ApplicationHighDpiMode, .NET Framework 4.7 이상이라면 app.config(매니페스트의 dpiAware와 함께 쓰지 않음), 4.6 이하는 매니페스트로 System Aware까지입니다. 여기에 AutoScaleMode.Font로 통일하고 디자이너는 100%에서 열기라는 운영 규칙을 더하면, 눈에 띄지는 않지만 사고를 가장 많이 줄여 줍니다.

그 위에서 어디까지 할지는 7장의 3단계 중에서 고르십시오. 먼저 System Aware화로 주 모니터를 선명하게 만들고, 이용 환경과 앱의 수명이 맞는다면 PMv2로 - 라는 2단계 접근이 저희가 실제 개선 프로젝트에서 추천하는 진행 방식입니다. UI 프레임워크 자체를 재검토할 시점(WPF는 처음부터 DPI에 강한 설계입니다)이 왔다면, “WinForms / WPF / WinUI를 어떻게 선택할까“도 함께 검토 자료로 활용하십시오. 지금 다루고 있는 앱을 어느 단계까지 고칠 수 있을지 판단하기 어렵다면, 화면 구성과 컨트롤 구성의 점검부터 도와드릴 수 있습니다.

관련 글

관련 상담 영역

합동회사 코무라소프트는 오래된 WinForms / MFC 앱의 고DPI 대응(현황 조사, 대응 수준 판단, 레이아웃 개선), PC 교체에 따른 표시 오류의 원인 조사, UI 개편에 관한 상담을 다루고 있습니다.

참고 링크

  1. Microsoft Learn, High DPI Desktop Application Development on Windows. DPI 스케일링의 배경, 각 DPI 인식 모드의 동작, DPI 비대응 앱이 비트맵으로 확대되는 원리, 혼합 DPI 환경에서의 테스트 관점에 대해 설명합니다.  2 3 4

  2. Microsoft Learn, Setting the default DPI awareness for a process. 매니페스트의 dpiAware / dpiAwareness 요소를 통한 선언 방법, 두 요소의 우선순위 관계, API를 통한 선언이 권장되지 않는 이유에 대해 설명합니다.  2

  3. Microsoft Learn, What’s new in Windows Forms .NET 6. ApplicationConfiguration.Initialize를 통한 부트스트랩, 프로젝트 파일의 ApplicationHighDpiMode 설정(기본값 SystemAware), .NET 6에서의 PerMonitorV2 스케일링 개선에 대해 설명합니다.  2 3 4

  4. Microsoft Learn, High DPI support - Windows Forms. .NET Framework 4.7의 고DPI 강화 내용, app.config의 System.Windows.Forms.ApplicationConfigurationSection(DpiAwareness=PerMonitorV2), 매니페스트에서의 선언이 app.config를 덮어쓰기 때문에 권장되지 않는다는 점, DpiChanged 계열 이벤트와 DeviceDpi에 대해 설명합니다.  2 3 4

  5. Microsoft Learn, Fix DPI display issues in Windows Form Designer. WinForms 디자이너가 DPI에 대응하지 않는다는 점, 고DPI 모니터에서 표시되는 정보 표시줄을 통한 100% 스케일링 재시작, devenv /noScale, .NET 6+ 대상 ForceDesignerDPIUnaware에 대해 설명합니다.  2 3

  6. Microsoft Learn, DPI_AWARENESS_CONTEXT handle. Unaware / System Aware / Per-Monitor / Per-Monitor V2 / UNAWARE_GDISCALED(GDI 스케일링, Windows 10 1809~) 각 컨텍스트의 정의와 동작에 대해 설명합니다.  2 3 4 5

  7. Microsoft Learn, Mixed-Mode DPI Scaling and DPI-aware APIs. SetThreadDpiAwarenessContext를 통한 최상위 창 단위의 DPI 인식 모드 혼용과 GetDpiForWindow 등 DPI 대응 API에 대해 설명합니다. 

  8. Microsoft Learn, SetProcessDpiAwareness function. API를 통한 프로세스 기본 DPI 인식 설정, 매니페스트에서의 선언이 권장되는 이유, 한 번 설정하면 변경할 수 없다는 점에 대해 설명합니다. 

  9. Microsoft Learn, Automatic form scaling - Windows Forms. AutoScaleMode / AutoScaleDimensions / CurrentAutoScaleDimensions에 의한 자동 스케일링의 동작, Font와 Dpi 모드의 혼용이 지원되지 않는다는 점에 대해 설명합니다.  2

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

4K 모니터에서 WinForms 앱이 흐려지는 이유는 무엇인가요?
버그가 아니라 OS의 구제 조치(DPI 가상화)입니다. DPI 대응을 선언하지 않은 앱에 대해 Windows는 '화면은 96 DPI다'라고 속이고, 앱이 96 DPI라고 생각하고 그린 결과를 비트맵으로 확대해서 표시합니다. 이 때문에 레이아웃은 깨지지 않는 대신 흐려집니다. 반대로 '선명한데 깨진다'는 DPI 대응을 선언했지만 레이아웃이 따라가지 못하는 상태로, 원인이 정반대입니다. 대처하기 전에 어느 쪽 증상인지 구분하는 것이 첫걸음입니다.
WinForms의 고DPI 대응은 어떻게 설정하나요?
세대에 따라 방법이 다릅니다. .NET 6 이상이면 프로젝트 파일의 ApplicationHighDpiMode 속성(기본값은 SystemAware)을, .NET Core 3.1/.NET 5라면 창을 만들기 전에 Application.SetHighDpiMode를 호출합니다. .NET Framework 4.7 이상은 매니페스트에서 Windows 10 호환성을 선언한 다음 app.config의 DpiAwareness 설정을 사용하는데, 매니페스트의 dpiAware 기술은 app.config 설정을 덮어쓰기 때문에 함께 쓰지 않는 것이 공식 권장 사항입니다. 4.6 이하는 매니페스트로 System Aware를 선언하는 것까지가 현실적인 선입니다.
System Aware와 Per-Monitor V2 중 어느 쪽을 선택해야 하나요?
2단계 접근을 권장합니다. 1단계는 System Aware화로, 시작 시 주 모니터의 DPI에 맞춰 스케일링되어 주 모니터에서는 선명하게 표시됩니다. 선언과 점검이 중심이라 실패 위험이 작고, 고정 데스크톱 중심의 사내 앱이라면 실질적으로 이것만으로 충분합니다. Per-Monitor V2는 모니터 간 이동에도 전체 화면이 선명해지지만, 레이아웃 전면 점검, 자체 드로잉·이미지 자산의 DPI 대응, DpiChanged 이벤트 대응이 필요해 작업량이 크고, 서드파티 컨트롤이 지원하지 않으면 그 지점이 한계가 됩니다. 앱의 수명, 이용 환경, 개선 예산으로 판단합니다.
디자이너에서 폼을 저장했더니 레이아웃이 깨진 이유는 무엇인가요?
150% 등 고DPI 모니터에서 Visual Studio의 WinForms 디자이너를 열고 폼을 저장하면, Designer.cs의 AutoScaleDimensions가 150% 값으로 바뀌고 좌표·크기도 1.5배 값으로 저장되기 때문입니다. 이것을 100% 환경의 팀원이 빌드하면 전체가 축소되어 표시됩니다. 대책은 디자이너를 100%(96 DPI)에서 여는 것으로, 고DPI 모니터에서는 노란색 정보 표시줄에서 100% 스케일링으로 재시작할 수 있습니다. .NET 6 이상 프로젝트에서는 ForceDesignerDPIUnaware 설정도 사용할 수 있습니다. AutoScaleDimensions의 예상치 못한 차이를 CI나 pre-commit에서 걸러내는 체계화도 효과적입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기