수정 이력(6건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 서두에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건) 대응으로 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- 결론을 골격과 계통별 표로 정리하고, 용어 설명을 추가했습니다. 증상을 직접 재현·확인하는 절차(돋보기로 구분하는 방법 포함)와 매니페스트 추가 조작 절차도 넣었습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635354)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「WPF의 고DPI 대응 ── 「DPI에 강해야 하는데」 흐려지고 번지는 원인과 대처」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/wpf-high-dpi-guide/
- DOI(등록된 아카이브)
- 10.5281/zenodo.21635354
- DOI(마지막 등록 버전)
- 10.5281/zenodo.21635355
「WPF라면 DPI 대응은 아무 것도 안 해도 되는 거 아닌가요?」라는 질문을, 고DPI 관련 상담에서 자주 받습니다. 반은 맞고, 반은 틀립니다. 실제로 가져오는 증상은 「노트북 단독에서는 선명한데, 회의실 외부 모니터에 연결하면 화면 전체가 번진다」「글자는 또렷한데 툴바 아이콘만 흐리다」「목록의 눈금선이 위치에 따라 굵어지거나 사라진다」「화면에 끼워 넣은 오래된 장표 컨트롤만 작다」——모두 「DPI에 강해야 할」 WPF 앱에서 실제로 일어납니다.
전날 기사 「WinForms의 고DPI 대응」에서는 Windows의 DPI 스케일링 구조와 DPI 인식 모드(Unaware / System Aware / Per-Monitor V2), 그리고 96 DPI에 직접 쓰던 시대의 WinForms를 어떻게 살릴지를 정리했습니다. 이 기사는 그 WPF 판입니다. DPI 가상화나 DPI 인식 모드의 일반론은 WinForms 기사에 맡기고, 여기서는 「처음부터 DPI 대응이 되어 있어야 할 WPF에서, 왜·어디에 문제가 남는지」라는 WPF 고유 이야기에 집중합니다. 증상 분리, Per-Monitor DPI 대응 선언 방법(.NET Framework 4.6.2 / .NET 각각), 가는 선과 비트맵의 번짐 대책, WindowsFormsHost 혼재의 함정, 그리고 어디까지 할지 판단표까지, 실무에서 쓰는 순서로 나열합니다.
이 기사에서 쓰는 용어
DPI 인식 모드의 일반론은 WinForms 기사에 맡기되, 본문에서 따로 설명하지 않고 쓰는 말만 먼저 잡아 둡니다.
| 용어 | 의미 |
|---|---|
| DIP | Device Independent Pixel(디바이스 독립 단위). 1/96인치를 1로 두는 WPF 레이아웃 단위입니다. XAML의 Width="120"에서 120이 이것입니다 |
| HWND | Windows가 창 한 장마다 할당하는 식별자(윈도우 핸들). WPF는 최상위 창에만 HWND를 갖고, 안의 버튼이나 글자는 WPF가 자체적으로 그립니다. HWND를 끌어들이는 요소만 WPF 그리기의 「밖」이 됩니다(7장) |
| GDI | Windows 전통 2D 그리기 API. 픽셀 단위로 그리는 구조이며, Per-Monitor DPI 스케일링 지원은 공식 표에서 「없음」으로 되어 있습니다1 |
| 서브픽셀 배치 | 요소의 경계가 물리 픽셀의 정수 위치에 걸리지 않은 상태. 어중간한 위치의 에지는 안티앨리어싱(경계를 중간색으로 흐리게 하는 처리)으로 그려져 번져 보입니다(5.1절) |
| DPI 가상화 | Unaware / System Aware 앱에 대해, OS가 창의 그리기 결과를 비트맵으로 확대·축소하는 구제 조치. 레이아웃은 무너지지 않는 대신 전체가 번집니다1 |
| 매니페스트 | 실행 파일에 넣는 XML. WPF에서 DPI 인식 모드를 선언하는 유일한 입구입니다(4장) |
1. 먼저 결론
먼저, 이 기사 전체의 골격이 되는 3가지입니다.
- WPF는 「시작 시점의 DPI」에는 처음부터 올바르게 따라갑니다. 레이아웃 단위가 DIP이고, 그리기 계통이 시스템 DPI에 맞춰 자동 스케일하므로 기본값이 System DPI Aware입니다. WinForms처럼 AutoScaleMode를 설정·점검할 필요는 없습니다. 2
- 그래도 남는 문제는 4계통으로 나뉩니다. (a) DPI가 다른 모니터로 옮겼을 때의 전체 번짐, (b) 비트맵 이미지·아이콘의 흐림, (c) 가는 선·눈금선의 번짐, (d) WindowsFormsHost / WebBrowser 등 혼재 콘텐츠. 원인도 고치는 방법도 다르므로, 먼저 3장의 표로 분리하세요.
- WPF 쪽 작업은 「선언 1줄 + 나머지 점검」입니다. 레이아웃 추종은 프레임워크가 하므로, 진짜 수정 대상은 픽셀을 직접 다루는 코드와 비트맵 자산, 혼재 콘텐츠뿐입니다(8장).
4계통 각각의 결론은 다음과 같습니다.
| 계통 | 결론 | 상세 |
|---|---|---|
| (a) 전체 번짐 | 근본 대책은 Per-Monitor DPI 대응입니다. .NET Framework 4.6.2 이후 WPF는 이를 프레임워크로 지원하며, 매니페스트에서 선언하면 창의 재스케일링 자체는 자동으로 이루어집니다34. .NET(Core 3.1〜.NET 8)의 WPF도 기본값은 System Aware 그대로이며, WinForms의 ApplicationHighDpiMode / SetHighDpiMode에 해당하는 장치는 WPF에 없습니다 |
4장 |
| (b) 비트맵 흐림 | Path / Geometry 등 벡터 자산을 1순위로 둡니다. 비트맵밖에 없는 자산은 여러 해상도를 준비해 DPI로 바꿉니다. WPF 기본 보간(Linear)은 정수가 아닌 배율에서 아이콘처럼 작은 이미지를 가장 흐리게 만듭니다5 | 5.3절 |
| (c) 가는 선 번짐 | DPI보다 앞선 서브픽셀 배치 문제입니다. 루트 요소의 UseLayoutRounding="True"가 첫 조치이고, SnapsToDevicePixels는 그릴 때 스냅하는 다른 도구입니다. 둘 다 기본값은 해제입니다67 |
5.1절 |
| (d) 혼재 콘텐츠 | Per-Monitor 대응의 상한을 정하는 요소입니다. 특히 ElementHost / HwndSource에 「올라간」 WPF의 Per-Monitor 동작은 공식적으로 지원 외라고 명시되어 있습니다4 | 7장 |
참고로 DPI 인식 모드의 일반론(DPI 가상화, System Aware와 Per-Monitor V2의 차이, exe 속성에서 사용자 쪽 덮어쓰기)과 혼재 DPI 테스트 환경을 만드는 방법은 WinForms 기사의 2〜3장·7장과 같으므로 이 기사에서는 반복하지 않습니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 24건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. WPF는 왜 「DPI에 강한가」 ── DIP와 System DPI Aware
WinForms 고DPI 문제의 뿌리는, 좌표도 크기도 물리 픽셀에 직접 쓰고, 96 DPI 전제 레이아웃을 실행 시점에 다시 환산하는 장치(AutoScale)를 나중에 붙였다는 점에 있었습니다. WPF는 출발점이 다릅니다. XAML에 쓰는 Width="120"의 120은 물리 픽셀이 아니라, 1/96인치를 1로 두는 디바이스 독립 단위(DIP)입니다. 레이아웃은 전부 이 단위로 계산되고, 그릴 때 시스템 DPI에 맞는 배율(150%면 1.5배)로 변환됩니다. 글자도 벡터 폰트로 그려지므로, 확대해도 비트맵식 열화가 없습니다. 이 구조 때문에 WPF 앱은 아무 것도 선언하지 않아도 System DPI Aware로 동작합니다. 2
WinForms와의 차이를 표로 정리합니다.
| WinForms | WPF | |
|---|---|---|
| 좌표·크기의 단위 | 물리 픽셀 | DIP(1/96인치) |
| 아무 것도 선언하지 않는 경우 | Unaware(OS의 비트맵 확대로 흐려짐) | System Aware(주 모니터에서는 또렷함) |
| 시작 시점 DPI 추종 | AutoScaleMode로 폼 단위 재계산. 설정과 점검이 필요 | 그리기 계통이 자동 스케일. 설정 불필요 |
| 고DPI에서 깨지는 전형 | 고정 좌표 레이아웃의 겹침·잘림 | 레이아웃은 깨지지 않고, 번짐·흐림으로 나타남 |
| 디자이너에서 비롯된 사고 | AutoScaleDimensions가 바뀌는 일(정석 함정) | 원칙적으로 없음(XAML은 DIP 그대로 저장됨) |
이 표대로, WinForms에서 공수의 중심이었던 「레이아웃이 깨지는 것을 고친다」「디자이너 사고를 막는다」는 일이 WPF에서는 거의 생기지 않습니다. 「WPF는 DPI에 강하다」는 올바른 인식입니다.
다만 자동인 범위는 System Aware까지입니다. System Aware는 「로그인 시점의 주 모니터 DPI에 맞춘다」는 모드이며, 모니터마다 DPI가 다른 환경으로의 추종(Per-Monitor)은 포함하지 않습니다. 또한 DIP로 확대되는 것은 어디까지나 WPF가 스스로 그리는 것(텍스트, 도형, 컨트롤)이고, 비트맵 이미지는 확대하면 흐려지며, HWND를 끌어들이는 혼재 콘텐츠는 WPF 스케일링의 바깥에 있습니다. 「DPI에 강해야 하는데」라는 상담은, 거의 이 나머지에서 일어납니다.
3. 그래도 생기는 문제 ── 증상에서 원인을 분리한다
상담을 받았을 때, 당사가 먼저 쓰는 분리표입니다. WPF에서는 증상과 원인의 대응이 WinForms보다 더 깔끔하게 나뉘므로, 이 표만으로 원인이 거의 특정됩니다.
| 증상 | 원인 | 대처 |
|---|---|---|
| DPI가 다른 모니터로 옮기면 전체가 균일하게 번진다. 원래 모니터로 되돌리면 낫는다 | System Aware 그대로. OS가 창 전체를 비트맵 확대하고 있다 | 4장(Per-Monitor화) |
| 스케일링 설정을 바꾼 직후나, 고DPI 클라이언트에서 RDP 연결하면 번진다 | 동일(System DPI는 로그인 시점에 고정되므로) | 4장 |
| 글자는 또렷한데 아이콘·이미지만 흐리거나 작다 | 비트맵 자산이 보간 확대되고 있다 | 5.3절 |
| 1px 눈금선·테두리가 위치에 따라 굵기가 다르다, 옅게 번진다 | 서브픽셀 배치와 안티앨리어싱 | 5.1절 |
| 100%(96 DPI) 모니터에서 작은 글자가 번져 보인다 | 기본 텍스트 정형(Ideal)의 안티앨리어싱 | 5.2절 |
| 직접 만드는 이미지(WriteableBitmap, RenderTargetBitmap 등)가 흐리다 | 픽셀 크기를 96 DPI 전제로 계산하고 있다 | 6장 |
| 창 위치·마우스 좌표·스크린샷 좌표가 어긋난다 | DIP와 물리 픽셀의 혼동 | 6장 |
| WindowsFormsHost / WebBrowser / 일부 서드파티 컨트롤의 안만 작거나 거칠거나 무너진다 | 혼재 콘텐츠(WPF 스케일링 대상 밖) | 7장 |
1행의 「전체가 균일하게 번진다」를 보완합니다. 이는 WinForms 기사 2장에서 쓴 DPI 가상화(OS의 비트맵 확대)가 System Aware 앱에 대해 작동하는 상태이며, 앱이 고장난 것이 아닙니다. System Aware 앱은 「로그인 시점의 주 모니터 DPI」로 그리고, 그 외 DPI의 모니터에서는 OS가 확대·축소해 맞춥니다. 레이아웃은 무너지지 않는 대신 번진다는, OS의 구제 조치입니다. 2 고친다는 것은 이 구제를 끊고 「모니터마다의 DPI에 스스로 따라가겠습니다」라고 선언하는 것, 즉 Per-Monitor화입니다.
반대로 3행 이후 증상은 System Aware 그대로도 고칠 수 있습니다. 「멀티 모니터 운용은 없지만, 150% 화면에서 아이콘과 눈금선이 지저분하다」는 상담이라면, 4장을 건너뛰고 5장부터 착수해도 됩니다.
3.1 직접 증상을 재현·확인하는 절차
이 기사에는 화면 캡처를 실지 않습니다. 번짐·흐림은 정지 화면으로 보여주는 것보다, 직접 실기에서 재현하는 편이 확실히 알 수 있습니다. 다음 절차로 위 표의 증상을 직접 확인하세요. 혼재 DPI 테스트 환경 자체를 만드는 방법은 WinForms 기사 7장에 있지만, 모니터 한 장이라도 여기까지는 확인할 수 있습니다.
- 확대율을 바꾼다. 시작 > 설정 > 시스템 > 디스플레이 > 「확대/축소 및 레이아웃」> 「텍스트, 앱 및 기타 항목의 크기 변경」에서 100% / 125% / 150%를 전환합니다. 8 효과가 가장 크게 나오는 것은 125%와 150%입니다(정수가 아닌 배율이기 때문이라는 이야기는 4.1절입니다).
- 앱을 다시 시작한다. WPF는 기본값이 System Aware이므로, 실행 중인 앱은 새 확대율을 따라가지 않습니다. 변경 후에는 반드시 다시 시작하세요. 그래도 안 되면 로그아웃·로그인까지 합니다(System DPI는 로그인 시점에 고정되기 때문입니다).
- 돋보기로 세부를 본다. Windows 로고 키 +
+(플러스)로 돋보기가 시작됩니다. 9 400% 정도까지 올리고, 다음 세 곳을 차례로 봅니다.- 눈금선·테두리 ── 선 한쪽 또는 양쪽에 옅은 회색이 1px 삐져나와 있으면 5.1절의 서브픽셀 배치입니다. 같은
1지정 선인데 행마다 진하기나 굵기가 다르게 보이는 것이 결정적인 신호입니다. - 아이콘·이미지 ── 윤곽이 이중으로 번지고, 원래 1px여야 할 선이 2px분의 중간색이 되어 있으면 5.3절의 비트맵 보간 확대입니다. 같은 화면의 글자가 또렷한 것이 분리의 결정적 단서입니다(글자는 벡터라서 흐려지지 않습니다).
- 화면 전체 ── 글자도 눈금선도 아이콘도 똑같이 흐리면, 개별 그리기 문제가 아니라 OS의 DPI 가상화입니다. 이 경우는 4장으로 갑니다.
- 눈금선·테두리 ── 선 한쪽 또는 양쪽에 옅은 회색이 1px 삐져나와 있으면 5.1절의 서브픽셀 배치입니다. 같은
- 모니터 사이를 이동한다(2대 이상인 경우). 확대율이 다른 모니터로 창을 드래그합니다. 옮긴 쪽에서 번지고, 원래로 되돌리면 낫다면 표의 1행입니다. 반대로, 이동해도 보이는 방식이 바뀌지 않으면 (a)는 일어나지 않은 것입니다.
- 실행 중에 확대율을 바꾼다. 앱을 켠 채로 1.의 설정을 바꿉니다. System Aware 앱은 그 자리에서 번집니다.
이 1〜5는 Per-Monitor화한 뒤의 점검 절차로도 그대로 쓸 수 있습니다. 공식 권장 테스트 항목도 모니터 사이 이동·다른 DPI에서 시작·실행 중 확대율 변경·주 모니터를 바꿔 로그아웃/로그인, 네 가지입니다. 1
4. 멀티 모니터 번짐 ── Per-Monitor DPI 대응
4.1 System Aware의 한계
System DPI는 로그인 시점에 주 모니터 DPI로 고정됩니다. 150% 노트북 내장 디스플레이가 주 모니터이면 WPF는 모든 창을 1.5배로 그리고, 100% 외부 모니터로 옮기면 OS가 2/3로 축소 표시합니다. 이 OS 스케일링은 정수가 아닌 배율일 때 특히 흐려 보입니다. 2 200%와 100%의 2배 차이보다, 125%와 150%처럼 어중간한 조합 쪽이 겉보기 열화가 크다는 것은 검증해 보면 체감됩니다.
즉 System Aware WPF에서 곤란한 경우는 DPI가 다른 모니터를 오가는 사용법과, 로그인 뒤에 스케일링 설정이 바뀌는 상황(도킹 스테이션 탈착으로 주 모니터가 바뀜, RDP에서 클라이언트 쪽 DPI가 들어옴 등)에 한정됩니다. 전원이 단일 모니터 고정 데스크톱에서 쓰는 사내 앱이라면 System Aware 그대로 실용상 끝납니다. 이 판단은 8장의 표로 돌아옵니다.
4.2 .NET Framework 4.6.2 이후의 선언 방법
WPF의 Per-Monitor DPI 대응은 .NET Framework 4.6.2에서 들어갔습니다. 3 그 이전에는 OS에 Per-Monitor를 선언해도 WPF 쪽이 DPI 변경을 따라가지 않아, 창의 재스케일링을 전부 직접 써야 했습니다(Windows 8.1 시대 샘플이 네이티브 헬퍼 DLL을 쓰는 큰 구조였던 것은 이 때문입니다). 4.6.2 이후는 WPF가 WM_DPICHANGED를 처리하고, 창 크기 조정·재레이아웃·다시 그리기까지 자동으로 합니다.
요건은 두 가지, Windows 10 Anniversary Update(1607) 이후 OS와 .NET Framework 4.6.2 이후를 타깃으로 한 빌드입니다. 4 선언은 애플리케이션 매니페스트에 씁니다.
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings>
<!-- .NET Framework 4.6.2〜4.7.2 는 PerMonitor 단독으로 선언한다(개발자 가이드와 동일).
WPF의 PerMonitorV2 지원은 .NET Framework 4.8 이후이므로,
4.8+ / .NET 에서는 「PerMonitorV2, PerMonitor」처럼 V2를 앞에 둔다 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
>PerMonitor</dpiAwareness>
<!-- dpiAwareness를 인식하지 않는 오래된 OS용(System Aware로 폴백) -->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
이 2단 구성에는 의미가 있습니다. dpiAwareness 요소는 Windows 10 1607 이후에서 인식되고, 쉼표로 나열한 값 중 먼저 인식된 것이 쓰입니다. 10 dpiAwareness 요소 자체를 모르는 오래된 OS에서는 dpiAware 선언(System Aware)으로 폴백하는 구조입니다.
여기서 PerMonitor와 PerMonitorV2를 타깃 프레임워크로 가려 쓰는 점에 주의하세요. WPF가 PerMonitorV2(와 Mixed-Mode DPI)를 정식 지원하는 것은 .NET Framework 4.8 이후이며11, 4.6.2 개발자 가이드 샘플도 PerMonitor 단독 선언입니다. 4 4.6.2〜4.7.2 타깃에서 V2를 앞에 쓰면, Windows 10 1703 이후 OS는 프레임워크가 대응하지 않은 V2 모드를 골라 버리고, 특히 WindowsFormsHost 등 혼재 콘텐츠 쪽에서 예상 밖 동작이 됩니다. 4.8 이후(그리고 .NET의 WPF)로 올린 뒤에 PerMonitorV2, PerMonitor처럼 V2를 앞에 두고, 타이틀 바·스크롤바 등 비클라이언트 영역의 스케일링을 OS에 맡기는 것(WinForms 기사 3장 참조)이 당사 추천입니다.
한 가지, 타깃 프레임워크의 함정이 있습니다. 실행 환경의 .NET Framework가 4.6.2 이후여도, 프로젝트 타깃이 4.6.1 이전이면 Per-Monitor 추종은 기본값으로 꺼져 있습니다. 이 경우는 app.config의 AppContext 스위치로 명시적으로 켭니다. 4
<configuration>
<runtime>
<!-- 이중 부정에 주의: 「DPI 변경 시 스케일하지 않는다」를 false로 한다=활성화 -->
<AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
</runtime>
</configuration>
「매니페스트를 썼는데 안 된다」는 문의의 원인은, 경험상 거의 이 타깃 프레임워크이거나, 매니페스트가 빌드에 들어가지 않은 경우(프로젝트 설정이 기본 매니페스트 그대로)입니다.
4.3 .NET (Core 3.1〜.NET 8)에서의 선언 방법
.NET 위의 WPF도 매니페스트 없는 기본값은 System Aware 그대로입니다. WinForms가 프로젝트 파일의 ApplicationHighDpiMode나 Application.SetHighDpiMode라는 전용 입구를 갖는 반면, WPF에는 코드나 프로젝트 설정에서 DPI 인식 모드를 바꾸는 공식 장치가 없습니다. 선언 방법은 .NET Framework와 같습니다.
매니페스트 추가 절차도 화면 캡처 없이 쓸 수 있으므로, 메뉴 이름을 그대로 나열합니다.
- 솔루션 탐색기에서 프로젝트를 오른쪽 클릭하고, 「추가」>「새 항목」(바로 가기는 Ctrl+Shift+A)을 고릅니다.
- 목록에서 「애플리케이션 매니페스트 파일」 템플릿을 고르고, 기본 이름
app.manifest그대로 추가합니다. - 추가된
app.manifest를 열고, 4.2절과 같은dpiAwareness선언을<asmv3:application>요소로 씁니다. 템플릿에는requestedExecutionLevel(UAC 요구 수준) 등이 처음부터 들어 있으므로, 그것들은 지우지 말고 추가하세요. - 프로젝트 파일의
<PropertyGroup>에<ApplicationManifest>app.manifest</ApplicationManifest>를 써서 참조시킵니다(SDK 스타일 프로젝트에서는app.manifest라는 이름으로 추가하면 자동 인식되는 경우도 있지만, 명시해 두는 편이 확실합니다).
.NET Framework 프로젝트의 경우는 4. 대신, 프로젝트 속성 >「애플리케이션」탭 >「매니페스트」 드롭다운에서 추가한 매니페스트를 고릅니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
</Project>
런타임이 새것이므로 4.2절의 AppContext 스위치는 필요 없습니다. 「.NET으로 옮기면 고DPI도 자동으로 해결된다」가 되지 않는다는 점만 주의하세요. 이전으로 편해지는 것은 WinForms 쪽 이야기이며(WinForms 기사 4.1절), WPF는 .NET Framework 4.6.2 시점에 지금과 같은 수준에 도달해 있습니다.
선언 뒤에 할 일이 남는 것은 WinForms와 같지만, 내용이 다릅니다. WinForms에서는 레이아웃이 깨지는 문제를 고치는 일이 핵심이었습니다. WPF에서는 레이아웃을 프레임워크가 따라가 주므로, 남는 것은 전체 화면 표시 점검과 비트맵 자산의 DPI 전환(5.3절), 픽셀을 직접 다루는 코드의 추종(6장), 혼재 콘텐츠 확인(7장)입니다. 화면 수가 같다면 Per-Monitor화 총공수는 WinForms보다 한 단계 적게 끝나는 것이 통례입니다.
5. 그리기의 번짐·흐림 ── 가는 선·글자·비트맵
이 장의 내용은 Per-Monitor화와 독립입니다. System Aware 그대로도 먹히므로, 멀티 모니터 운용이 없는 앱에도 적용할 가치가 있습니다.
5.1 가는 선·눈금선의 번짐 ── UseLayoutRounding과 SnapsToDevicePixels
DIP로 레이아웃한다는 것은 요소의 경계가 물리 픽셀의 정수 위치에 걸린다는 보장이 없다는 뜻입니다. 125%라면 1 DIP = 1.25px이므로, 폭 1 DIP 눈금선은 물리 픽셀 1.25개분에 해당하고, 픽셀 중간에 떨어진 에지는 안티앨리어싱으로 반투명하게 그려집니다. 결과가 「선이 번진다」「같은 1px 지정인데 행마다 굵기가 다르게 보인다」입니다. 96 DPI에서도 Grid의 별표 크기(*) 나눗셈이나 가운데 정렬 여백 계산으로 0.5px에 걸리면 같은 일이 납니다. 이는 WPF 서브픽셀 그리기의 사양이며, DPI 문제라기보다 「DPI가 올라가면 드러나기 쉬워지는」 문제입니다. 6
첫 조치는 레이아웃 반올림입니다. 루트 요소에 한 줄을 더합니다.
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
UseLayoutRounding="True">
UseLayoutRounding은 레이아웃 경로에서 정수가 아닌 픽셀 값을 반올림하는 장치이며, 기본값은 해제, 루트에 설정하면 비주얼 트리 전체로 전파됩니다. 6 비슷한 이름의 SnapsToDevicePixels는 역할이 다르며, 레이아웃이 아니라 그릴 때 에지를 픽셀 경계에 스냅합니다. 이쪽도 기본값은 false이고, 설정은 서브트리로 상속됩니다. 96 DPI를 넘는 환경에서 가는 선 주변의 안티앨리어싱 번짐을 줄이는 용도가 공식 문서에도 나와 있습니다. 7
| UseLayoutRounding | SnapsToDevicePixels | |
|---|---|---|
| 작동 시점 | 레이아웃 경로(Measure / Arrange 결과를 정수 픽셀로 반올림) | 그릴 때(에지를 픽셀 경계에 스냅) |
| 기본값 | false | false |
| 적용 범위 | 루트에 설정하면 자손으로 전파 | 마찬가지로 자손에 상속 |
| 주로 먹는 곳 | 레이아웃 전반. 요소 크기·위치의 0.5px 어긋남, 그에서 비롯한 선·경계 번짐 | Border·가는 선 등 개별 요소에 남은 번짐 |
| 쓰는 법의 기준 | 먼저 이것을 루트 Window에. 신규 앱은 모든 창 공통 스타일로 | UseLayoutRounding으로 남은 곳에 핀포인트로 |
헤매면 「루트에 UseLayoutRounding="True", 그래도 남는 곳에 SnapsToDevicePixels="True"」 순서입니다. 반올림 부작용으로, 별표 크기로 등분하던 열 폭이 1px 단위로 불균등해질 수 있지만, 업무 앱 화면에서 문제가 된 경험은 거의 없습니다. 참고로 DrawingContext로 직접 그리는 코드의 번짐에는 GuidelineSet이라는 저수준 수단도 있지만, 먼저 그리기 좌표를 반올림하는 것만으로 대부분은 해결됩니다.
5.2 글자의 번짐 ── TextFormattingMode
「WPF는 글자가 옅고 번진다」는 예전부터의 불만은, DPI라기보다 텍스트 정형 모드 이야기입니다. WPF 텍스트 정형에는 폰트 본래의 이상 메트릭으로 배치하는 Ideal(기본값)과, GDI 호환 메트릭으로 배치하는 Display 두 모드가 있습니다. 12 Ideal은 글자 간격이 아름답고 스케일링에도 순하지만, 96 DPI(100%)에서 작은 글자를 그리면 안티앨리어싱으로 번져 보일 때가 있습니다. TextOptions.TextFormattingMode="Display"를 루트에 설정하면 WinForms적인 또렷함이 됩니다.
다만 고DPI 환경에서는 이야기가 반대로 됩니다. Display는 96 DPI 픽셀 격자에 맞추는 정형이므로, 스케일링 아래에서는 오히려 품질이 떨어질 수 있습니다. 100% 환경 이용자가 많으면 Display, 125% 이상이 주류가 된 지금의 표준 환경에서는 기본 Ideal 그대로가 실무적인 타협입니다. 100%일 때만 Display로 바꾸는 구현도 가능하지만, 그 정도까지 할 가치가 있는 화면(텍스트 에디터형 화면)은 한정됩니다.
5.3 이미지·아이콘의 흐림 ── 벡터 우선과 여러 해상도
WPF는 Image에 지정한 비트맵도 DIP로 배치하고 DPI에 따라 확대합니다. 16×16px 아이콘은 150% 환경에서 24×24px로 보간 확대되고, 기본 보간 알고리즘(Linear)에서는 분명히 흐려집니다. 5 「글자는 예쁜데 아이콘만 흐릿하다」는 WPF 앱의 겉모습은 거의 전부 이것입니다.
대책은 우선순위로 세 가지입니다.
- 벡터 자산으로 만든다(1순위).
Path/Geometry/DrawingImage로 가진 아이콘은 어떤 DPI에서도 또렷이 그려지고, 전환 코드도 필요 없습니다. 디자인 도구나 SVG에서 XAML로 변환하는 도구도 충실합니다. 신규 앱의 아이콘은 처음부터 벡터로 가진다는 규약을 둘 가치가 있습니다. Segoe MDL2 Assets 같은 아이콘 폰트도 같은 효과입니다. - 여러 해상도 비트맵을 DPI로 바꾼다. 사진이나 스크린샷처럼 벡터화할 수 없는 자산은 96 / 120 / 144 / 192 DPI용(16 / 20 / 24 / 32px 등)을 준비하고, 현재 DPI에 따라 고릅니다. 공식 개발자 가이드도 흐림 대책으로 DPI별 자산 전환을 권장합니다. 2
RenderOptions.BitmapScalingMode로 보간을 고른다. 자산을 늘릴 수 없을 때의 완화책입니다. 200%처럼 정수 배율이면NearestNeighbor로 점 그대로 확대하는 편이 또렷하고, 큰 이미지 축소에는HighQuality(Fant)가 맞습니다. 5
2의 전환은 Per-Monitor 대응이 되어 있으면 OnDpiChanged(.NET Framework 4.6.2 이후에서 사용 가능)로 합니다.
public partial class MainWindow : Window
{
protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
{
base.OnDpiChanged(oldDpi, newDpi);
// 모니터 사이 이동이나 스케일링 변경마다 호출된다
AppIcon.Source = IconAssets.SelectFor(newDpi.DpiScaleX);
}
}
public static class IconAssets
{
// 16 / 24 / 32 px로 준비한 자산에서 확대율에 맞는 것을 고른다
public static BitmapImage SelectFor(double scale) => scale switch
{
<= 1.0 => Load("icon16.png"),
<= 1.5 => Load("icon24.png"),
_ => Load("icon32.png"),
};
private static BitmapImage Load(string name) =>
new(new Uri($"pack://application:,,,/Assets/{name}"));
}
System Aware 그대로라면 DPI는 시작 뒤에 바뀌지 않으므로, 시작 시(Loaded)에 VisualTreeHelper.GetDpi(this)로 한 번 고르면 끝입니다. 전환 코드를 쓰기 전에 「애초에 이 자산은 벡터로 못 하는가」를 매번 다시 묻는 것이, 결국 유지보수가 가장 쉽습니다.
6. 픽셀을 다루는 코드와 DPI 변경 추종
WPF 자동 스케일링의 우산 밖으로 나가는 것이 물리 픽셀을 스스로 세는 코드입니다. 전형은 다음 세 가지이며, 모두 「개발기(100%)에서는 완벽하고, 고객 쪽 150%에서만 흐리거나 어긋난다」는 싫은 형태로 나타납니다.
첫 번째는 WriteableBitmap / RenderTargetBitmap처럼 픽셀 버퍼를 직접 만드는 코드입니다. 픽셀 수를 DIP 크기와 같은 값으로 만들면, 150% 환경에서는 1.5배로 보간 확대되어 번집니다. 현재 DPI는 VisualTreeHelper.GetDpi가 반환하는 DpiScale 구조체로 얻을 수 있으므로, 픽셀 수는 물리 픽셀로 잡고, DPI는 버퍼에 올바르게 지정합니다.
// imageHost: 그리기 결과를 표시하는 요소(ActualWidth/Height는 DIP)
DpiScale dpi = VisualTreeHelper.GetDpi(imageHost);
int pixelWidth = (int)Math.Ceiling(imageHost.ActualWidth * dpi.DpiScaleX);
int pixelHeight = (int)Math.Ceiling(imageHost.ActualHeight * dpi.DpiScaleY);
// 96,96 고정으로 만들지 않고, 실제 DPI를 넘긴다(1 물리 픽셀 = 1 버퍼 픽셀이 된다)
var bitmap = new WriteableBitmap(
pixelWidth, pixelHeight,
dpi.PixelsPerInchX, dpi.PixelsPerInchY,
PixelFormats.Bgra32, null);
두 번째는 스크린 좌표나 Win32 상호 운용입니다. PointToScreen이 반환하는 좌표, WM_MOUSEMOVE나 훅으로 받는 좌표, MoveWindow에 넘기는 좌표는 모두 물리 픽셀이며, XAML 쪽 DIP와 섞으면 125% 환경에서 1.25배 어긋납니다. 변환은 CompositionTarget의 행렬을 씁니다.
var source = PresentationSource.FromVisual(this);
if (source?.CompositionTarget is { } target)
{
// DIP → 물리 픽셀
Point device = target.TransformToDevice.Transform(new Point(x, y));
// 물리 픽셀 → DIP
Point dip = target.TransformFromDevice.Transform(devicePoint);
}
세 번째는 캐시입니다. DPI를 반영해 만든 비트맵·레이아웃 값·포맷된 텍스트를 캐시하는 경우, Per-Monitor화하면 모니터 사이 이동마다 낡아집니다. 5.3절과 같이 OnDpiChanged(또는 창의 DpiChanged 이벤트)에서 다시 만드세요. 여기에서도 System Aware 그대로라면 DPI는 시작 시점에 고정이므로 추종 코드는 필요 없습니다. 「Per-Monitor화 비용=픽셀을 다루는 코드의 수」로 보고 견적하면 실태와 잘 맞습니다.
참고로 이런 재생성 처리를 비동기로 할 때의 UI 스레드 취급은 「WPF/WinForms의 async와 UI 스레드」에서 정리한 대로입니다.
7. 혼재 콘텐츠의 함정 ── WindowsFormsHost·WebBrowser·서드파티
WPF 자동 스케일링이 미치는 것은 WPF가 스스로 그리는 것뿐입니다. HWND를 끌어들이는 요소는 WPF 그리기의 「밖」에 있으므로, DPI 취급이 한 단계 복잡해집니다. Windows 공식 자료에도 「WPF에 올린 다른 프레임워크, 다른 프레임워크에 올린 WPF는 자동으로는 스케일하지 않는다」고 명시되어 있습니다. 1
| 혼재의 형태 | 무엇이 일어나는가 | 현실적인 취급 |
|---|---|---|
| WindowsFormsHost(WPF에 WinForms를 올린다) | DIP와 물리 픽셀 두 좌표계를 호스트가 변환하지만, 안의 스케일링은 WinForms 컨트롤이 대응할 수 있는 범위까지13 | 안의 WinForms 쪽 DPI 대응(AutoScaleMode 등)을 WinForms 기사 기준으로 점검. Per-Monitor 추종은 기대하지 않는다 |
| WebBrowser 컨트롤(IE 엔진) | 별도 HWND의 네이티브 콘텐츠. 줌 비율이 앱 스케일링과 맞지 않을 수 있다 | 레거시 취급. WebView2로 이전을 검토할 때의 재료에 넣는다 |
| ElementHost / HwndSource(WinForms나 Win32에 WPF를 올린다) | Per-Monitor 시나리오는 공식 지원 외4 | 호스트 쪽 앱의 DPI 인식 모드에 맞춰 System Aware까지로 설계한다 |
| 서드파티 컨트롤 | 제품마다 대응 수준이 제각각. Per-Monitor 미대응 제품은 그 화면의 상한이 된다 | 벤더의 대응 상황·대응 버전을 파악한 뒤에 Per-Monitor화를 판단한다 |
실무에서 가장 많은 것은 1행입니다. 「WPF로 이전했지만, 장표 미리보기나 그래프만 오래된 WinForms / ActiveX 컨트롤을 WindowsFormsHost로 올리고 있다」는 구성의 앱을 Per-Monitor화하면, WPF 부분은 완벽하게 따라가는데 호스트 안만 이동 후 DPI를 따라가지 않고 작은 채·거친 채가 됩니다. Win32 수준에는 혼재 DPI를 호스트하는 장치(SetThreadDpiHostingBehavior)도 있지만, WPF에서 가볍게 쓸 수 있는 것이 아니며, 당사에서는 「혼재 부분이 Per-Monitor 대응의 상한을 정한다」고 보고, 먼저 혼재 콘텐츠 목록을 만드는 것부터 시작합니다. 혼재를 없애는 방향의 판단 재료는 「WinForms / WPF / WinUI를 어떻게 고를까」와 「ActiveX/OCX를 유지할지, 랩할지, 바꿀지」에 정리해 두었습니다.
8. 어디까지 할 것인가 ── 판단표와 단계적 진행
WPF의 경우 선택지는 실질 두 가지입니다(WinForms처럼 「아무 것도 안 함=Unaware」는, 선언하지 않아도 System Aware이므로 존재하지 않습니다).
| System Aware 그대로 | Per-Monitor 대응 | |
|---|---|---|
| 보이는 방식 | 주 모니터에서는 또렷함. DPI가 다른 모니터·스케일링 변경 후·RDP에서는 번짐 | 모든 모니터에서 또렷함 |
| 선언 작업 | 불필요(기본값) | 매니페스트 추가만(4장) |
| 선언 뒤 남은 작업 | ── | 전체 화면 표시 점검+비트맵 자산 DPI 전환(5.3절)+픽셀 계열 코드의 DpiChanged 대응(6장)+혼재 콘텐츠 확인(7장) |
| 상한을 쥐는 요소 | ── | WindowsFormsHost·WebBrowser·서드파티 컨트롤 |
| 맞는 경우 | 단일 모니터·고정 데스크톱 중심의 사내 앱. 혼재 콘텐츠가 많은 앱 | 노트북+외부 모니터 혼재 이용, 장수명 주력 앱, 고객에게 배포하는 제품 |
판단 축은 WinForms 기사 7장과 같습니다(앱 수명·이용 환경·수정 예산). 다만 WPF는 Per-Monitor화의 비용 상한이 작다는 점이 다릅니다. 선언은 파일 하나, 레이아웃은 프레임워크가 따라가고, 남은 작업은 픽셀 계열 코드와 자산 수에 비례합니다. 혼재 콘텐츠가 적은 앱이라면 Per-Monitor까지 가는 비용 대비 효과는 WinForms보다 분명히 높습니다. 당사가 실제 안건에서 추천하는 진행은 다음 3단계입니다.
- System Aware 그대로의 품질 개선: 루트의
UseLayoutRounding, 아이콘의 벡터화·여러 해상도화,WriteableBitmap계열 DPI 수정. 멀티 모니터 번짐 외에는 여기서 전부 해소하고, Per-Monitor화하지 않기로 한 경우에도 그대로 자산이 됩니다. - Per-Monitor 선언과 점검: 매니페스트를 추가하고, 혼재 DPI 환경에서 전체 화면을 돌려 문제 지점을 가려냅니다. 여기서 나오는 문제는 5〜7장 중 어딘가로 분류될 것입니다.
- DPI 변경 추종 구현:
OnDpiChanged로 자산 전환·캐시 재생성. 혼재 콘텐츠는 이 단계에서 「어디까지 허용할지」를 정합니다.
테스트 환경은 WinForms 기사 7장에 적은 구성(스케일링이 다른 모니터 2대, 주 모니터 교체+재로그인, 실행 중 스케일링 변경, 고DPI 클라이언트에서의 RDP)을 그대로 쓸 수 있습니다. 1 WPF에서 중점적으로 볼 것은 정수가 아닌 배율(125% / 150%)에서의 눈금선·아이콘·직접 그리기와, 모니터를 가로질러 창을 드래그했을 때의 거동입니다. 이용 환경 분포(사용자 몇 %가 멀티 모니터인지)를 먼저 파악해 두면 투자 판단이 흔들리지 않습니다. 이 관점은 「Windows 앱 UX 설계」에서도 썼습니다.
9. 정리
WPF는 DIP와 자동 스케일링으로 처음부터 System DPI Aware이며, WinForms의 핵심 과제였던 「레이아웃이 깨지는 문제와의 싸움」은 거의 없습니다. 그래도 남는 문제는 4계통──멀티 모니터 번짐(Per-Monitor 미대응), 비트맵 흐림, 가는 선 번짐, 혼재 콘텐츠──이고, 각각 대처가 자리 잡혀 있습니다.
- 멀티 모니터 번짐은 .NET Framework 4.6.2 이후 / .NET의 WPF라면 매니페스트 선언만으로 Per-Monitor 추종이 자동으로 손에 들어옵니다
- 가는 선·눈금선은 루트의
UseLayoutRounding="True"가 첫 조치, 남은 곳에SnapsToDevicePixels - 아이콘은 벡터 자산을 1순위, 비트맵은 여러 해상도+DPI 전환
WriteableBitmap이나 스크린 좌표처럼 픽셀을 다루는 코드만 진짜 수정 대상입니다. 그 수가 Per-Monitor화 공수를 정합니다- WindowsFormsHost / WebBrowser / 서드파티가 대응의 상한을 쥡니다. 먼저 파악하세요
「WPF이니 괜찮을 것」에서 멈춰 있던 앱도, 이 정리로 보면 고칠 곳이 손에 꼽을 정도인 경우가 많습니다. 손안의 앱을 어디까지 고칠 수 있는지, 혼재 콘텐츠를 포함한 현황 조사나 대응 수준 판단이 막히면 도움을 드릴 수 있습니다.
관련 기사
- WinForms의 고DPI 대응 ── 4K 모니터에서 흐려지거나 레이아웃이 깨지는 원인과 현실적인 대처
- WinForms/WPF/WinUI 고르는 법 - 실무 판단표
- WPF/WinForms의 async와 UI 스레드를 한 장으로 정리
- Windows 앱 UX 설계 - 이용 환경별 우선순위
관련 상담 영역
合同会社小村ソフト에서는 WPF / WinForms 앱의 고DPI 대응(현황 조사, Per-Monitor화 가능 여부 판단, 혼재 콘텐츠 파악과 수정), PC 교체·4K 모니터 도입에 따른 표시 불량의 원인 조사, UI 쇄신 상담을 다룹니다.
참고 링크
-
Microsoft Learn, High DPI Desktop Application Development on Windows. UI 프레임워크별 Per-Monitor 대응 표, WPF에 올린 다른 프레임워크·다른 프레임워크에 올린 WPF가 자동으로는 스케일하지 않는 것, 혼재 DPI 환경에서의 테스트 관점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Developing a Per-Monitor DPI-Aware WPF Application. WPF가 기본값으로 System DPI Aware인 것, DIP에 의한 자동 스케일링 구조, DPI가 다른 모니터로 이동하면 OS가 스케일링하고 정수가 아닌 배율에서 특히 흐려지는 것, DPI별 비트맵 자산 전환의 생각법에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, What’s new in .NET Framework. .NET Framework 4.6.2의 WPF에서 Per-Monitor DPI awareness가 켜진 것, Switch.System.Windows.DoNotScaleForDpiChanges 스위치, GitHub상의 개발자 가이드에 대해. ↩ ↩2
-
GitHub (microsoft/WPF-Samples), Per Monitor DPI Developer Guide. Windows 10 Anniversary Update와 .NET Framework 4.6.2 이후라는 요건, 매니페스트의 dpiAwareness / dpiAware 기술, 4.6.2보다 오래된 타깃용 AppContextSwitchOverrides, HwndSource / ElementHost에 WPF를 올리는 구성이 Per-Monitor에서 지원 외인 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, BitmapScalingMode Enum. 기본값(Unspecified)이 Linear인 것, HighQuality(Fant)·NearestNeighbor 각 보간 알고리즘의 특성에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Layout - WPF. DIP에 의한 레이아웃과 서브픽셀 그리기로 에지가 흐려지는 구조, 레이아웃 반올림(UseLayoutRounding)이 기본값으로 해제인 것, 루트 요소에 설정하면 비주얼 트리로 전파되는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, UIElement.SnapsToDevicePixels Property. 기본값이 false인 것, 루트에 설정하면 서브트리로 상속되는 것, 96 DPI를 넘는 환경에서 가는 선 근처 안티앨리어싱에 의한 시각적 아티팩트를 줄일 수 있는 것에 대해. ↩ ↩2
-
Microsoft 지원, Windows에서 화면 해상도와 레이아웃 변경. 「시작 > 설정 > 시스템 > 디스플레이」의 「확대/축소 및 레이아웃」에서 「텍스트, 앱 및 기타 항목의 크기 변경」으로 확대율을 고르는 절차에 대해. ↩
-
Microsoft 지원, 돋보기를 사용하여 화면의 항목을 보기 쉽게 만들기. Windows 로고 키 + 더하기 기호로 돋보기를 시작하고, 화면 일부를 확대 표시할 수 있는 것에 대해. ↩
-
Microsoft Learn, Setting the default DPI awareness for a process. dpiAwareness 요소(Windows 10 1607 이후)가 dpiAware보다 우선되는 것, 쉼표로 나열한 값 중 먼저 인식된 것이 쓰이는 폴백 동작에 대해. ↩
-
Microsoft Learn, What’s new in .NET Framework. .NET Framework 4.8에서 WPF에 Per-Monitor V2 DPI Awareness와 Mixed-Mode DPI 스케일링 지원이 추가된 것, 호스트된 HWND / WinForms 상호 운용 개선, 활성화에 필요한 AppContext 스위치에 대해. ↩
-
Microsoft Learn, TextFormattingMode Enum. 텍스트 정형의 Ideal(이상 메트릭)과 Display(GDI 호환 메트릭) 두 모드에 대해. ↩
-
Microsoft Learn, Layout Considerations for the WindowsFormsHost Element. WindowsFormsHost가 DIP와 물리 픽셀 두 좌표계를 변환하는 것, 스케일링은 안의 Windows Forms 컨트롤이 대응할 수 있는 범위까지인 것에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
WinForms의 고DPI 대응 ── 4K 모니터에서 흐려지거나 레이아웃이 깨지는 원인과 현실적인 대처
4K 모니터에서 WinForms 앱이 흐려지거나 레이아웃이 깨지는 원인을 DPI 가상화와 DPI 인식 모드(System Aware / Per-Monitor V2)로 정리합니다. .NET과 .NET Framework의 설정 방법, AutoScale...
업무 앱의 날짜·시각과 타임존 ── DateTime의 함정부터 UTC 저장 원칙, 테스트 설계까지
서버를 옮겼더니 시각이 9시간 어긋난다──날짜·시각 사고의 원인을 DateTime의 Kind와 암묵 변환부터 정리합니다. DateTimeOffset과의 구분, UTC 저장 원칙, TimeZoneInfo와 서머타임, TimeProvider를 이용한...
Windows 프린터 드라이버 제공 종료 ── 업무 앱의 장표·라벨 인쇄는 어떻게 대비할 것인가
Microsoft는 v3/v4 프린터 드라이버의 제공 종료를 단계적으로 진행하고 있으며, 2026년 7월부터는 IPP 클래스 드라이버가 우선됩니다. Windows protected print mode에서 무엇이 사라지는지, 업무 앱의 장표·라벨 ...
Windows 앱의 작업 트레이 상주와 토스트 알림 ── NotifyIcon의 함정과 AppNotification 고르는 법
업무용 Windows 앱의 작업 트레이 상주와 토스트 알림 구현을 정리합니다. NotifyIcon의 올바른 사용법, Explorer 재시작 시 재등록, 토스트 API 3종의 선정 판단표, 알림이 도착하지 않는 경우까지 설명합니다.
WinForms/WPF 앱의 다국어화 ── resx·satellite assembly·culture 전환의 실무
Windows 데스크톱 앱의 다국어화를 정리합니다. CurrentCulture와 CurrentUICulture의 차이, resx와 satellite assembly의 구조, WPF에서 현실적인 방식 선택, 런타임 언어 전환, 서식·RTL까지 설명...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- WPF는 DPI 대응이 필요 없다고 들었는데, 왜 번집니까?
- WPF는 레이아웃 단위가 1/96인치의 디바이스 독립 단위(DIP)이고, 기본값으로 System DPI Aware로 동작하므로 시작 시점의 주 모니터 DPI에는 올바르게 따라갑니다. 다만 System DPI는 로그인 시점에 고정되므로, DPI가 다른 모니터로 창을 옮기면 OS가 창 전체를 비트맵으로 확대·축소해 맞추고, 전체가 균일하게 번집니다. 특히 125%와 150%처럼 정수가 아닌 배율 조합에서 열화가 두드러집니다. 근본 대책은 매니페스트에서 Per-Monitor DPI 대응을 선언하는 것이며, .NET Framework 4.6.2 이후의 WPF라면 창의 재스케일링은 자동으로 이루어집니다.
- WPF에서 1px 눈금선이 번지거나 굵기가 들쭉날쭉한 이유는 무엇입니까?
- DIP로 레이아웃하므로, 요소의 경계가 물리 픽셀의 정수 위치에 걸린다는 보장이 없기 때문입니다. 125% 환경에서는 1 DIP = 1.25px가 되고, 픽셀 중간에 떨어진 에지는 안티앨리어싱으로 반투명하게 그려져 번집니다. 첫 조치는 루트 Window에 UseLayoutRounding="True"를 설정하는 것으로, 레이아웃 경로에서 정수가 아닌 픽셀 값이 반올림되어 비주얼 트리 전체로 전파됩니다. 그래도 남는 곳에는 그릴 때 에지를 픽셀 경계에 스냅하는 SnapsToDevicePixels="True"를 핀포인트로 적용합니다. 둘 다 기본값은 해제입니다.
- WPF에서 글자는 선명한데 아이콘만 흐려지는 이유는 무엇입니까?
- 글자는 벡터 폰트로 그려지므로 어떤 DPI에서도 또렷하지만, 비트맵 이미지는 DIP로 배치된 뒤 DPI에 따라 보간 확대되기 때문입니다. 16×16px 아이콘은 150% 환경에서 24×24px로 확대되고, 기본 보간(Linear)에서 흐려집니다. 대책 우선순위는 (1) Path/Geometry/DrawingImage나 아이콘 폰트 등 벡터 자산으로 만들기, (2) 여러 해상도 비트맵을 준비해 VisualTreeHelper.GetDpi나 OnDpiChanged로 DPI에 따라 바꾸기, (3) RenderOptions.BitmapScalingMode로 보간 알고리즘을 조정하기입니다.
- WPF의 Per-Monitor 대응은 어떻게 설정합니까?
- 애플리케이션 매니페스트에 dpiAwareness 요소를 써서 선언합니다. WinForms의 ApplicationHighDpiMode 같은 프로젝트 설정이나 코드에서 전환하는 수단은 WPF에 없습니다. 요건은 Windows 10 1607 이후 OS와 .NET Framework 4.6.2 이후 타깃이며, 선언하면 WM_DPICHANGED 처리부터 창의 재스케일링까지 프레임워크가 자동으로 합니다. WPF의 PerMonitorV2 지원은 .NET Framework 4.8 이후이므로, 4.6.2〜4.7.2에서는 PerMonitor만으로 선언합니다. 타깃이 4.6.1 이전이면 추종이 무효이므로 AppContext 스위치로 켜야 합니다. 참고로 ElementHost나 HwndSource에 올라간 WPF의 Per-Monitor 동작은 공식적으로 지원 대상이 아닙니다.