DPI 및 장치 독립적 픽셀
Windows 그래픽으로 효과적으로 프로그래밍하려면 다음 두 가지 관련 개념을 이해해야 합니다.
- DPI(인치당 도트 수)
- 장치 독립적 픽셀(DIP)
DPI부터 시작하겠습니다. 그러려면 입력 체계로 잠시 우회해야 합니다. 입력 체계에서 활자의 크기는 포인트라는 단위로 측정됩니다. 1포인트는 1/72인치입니다.
- 1pt = 1/72inch
참고
이것은 포인트에 대한 데스크톱 게시 정의입니다. 역사적으로, 포인트의 정확한 측정은 다양합니다.
예를 들어 12포인트 글꼴은 1/6인치(12/72) 텍스트 줄 내에 맞게 설계되었습니다. 물론 글꼴의 모든 문자가 정확히 1/6인치 높이임을 의미하지는 않습니다. 실제로 일부 문자는 1/6인치보다 클 수 있습니다. 예를 들어 많은 글꼴에서 문자 Å는 글꼴의 명목 높이보다 큽니다. 글꼴을 올바르게 표시하려면 텍스트 사이에 약간의 추가 공간이 필요합니다. 이 공간을 선행이라고 합니다.
다음 그림은 72포인트 글꼴을 보여줍니다. 실선은 텍스트 주위에 1인치 높이의 경계 상자를 나타냅니다. 파선은 기준선이라고 합니다. 글꼴에 있는 대부분의 문자는 기준선에 얹혀 있습니다. 글꼴 높이는 기준선 위 부분(상승)과 기준선 아래 부분(하강)을 포함합니다. 여기에 표시된 글꼴에서 상승은 56포인트이고 하강은 16포인트입니다.
그러나 컴퓨터 디스플레이의 경우 픽셀 크기가 모두 동일하지 않기 때문에 텍스트 크기를 측정하는 데 문제가 있습니다. 픽셀의 크기는 디스플레이 해상도와 모니터의 물리적 크기라는 두 가지 요인에 따라 달라집니다. 따라서 물리적 인치는 유용한 척도가 아닙니다. 물리적 인치와 픽셀 사이에는 고정된 관계가 없기 때문입니다. 대신 글꼴은 논리 단위로 측정됩니다. 72포인트 글꼴은 1 논리 인치 높이로 정의됩니다. 그런 다음, 논리 인치가 픽셀로 변환됩니다. 다년간 Windows는 다음 변환을 사용했습니다. 1 논리 인치는 96픽셀과 같습니다. 이 배율 인수를 사용하면 72포인트 글꼴이 96픽셀 높이로 렌더링됩니다. 12포인트 글꼴의 높이는 16픽셀입니다.
- 12포인트 = 12/72 논리 인치 = 1/6 논리 인치 = 96/6픽셀 = 16픽셀
이 배율 인수는 96DPI(인치당 도트 수)로 설명됩니다. 도트라는 용어는 물리적인 잉크 점이 종이에 찍히는 인쇄에서 유래되었습니다. 컴퓨터 디스플레이의 경우 논리 인치당 96픽셀이라고 하는 것이 더 정확할 수 있지만 DPI라는 용어가 고착되었습니다.
실제 픽셀 크기는 다양하기 때문에 한 모니터에서 읽을 수 있는 텍스트가 다른 모니터에서는 너무 작을 수 있습니다. 또한 사람들마다 선호도가 다릅니다. 어떤 사람들은 더 큰 텍스트를 선호합니다. 이러한 이유로 Windows에서는 사용자가 DPI 설정을 변경할 수 있습니다. 예를 들어 사용자가 디스플레이를 144DPI로 설정하면 72포인트 글꼴의 높이는 144픽셀입니다. 표준 DPI 설정은 100%(96 DPI), 125%(120 DPI) 및 150%(144 DPI)입니다. 사용자 지정 설정을 적용할 수도 있습니다. Windows 7부터 DPI는 사용자별 설정입니다.
DWM 배율 조정
프로그램에 DPI를 고려하지 않으면 높은 DPI 설정에서 다음과 같은 결함이 나타날 수 있습니다.
- UI 요소 잘림
- 잘못된 레이아웃
- 픽셀화된 비트맵 및 아이콘
- 잘못된 마우스 좌표(적중 테스트, 끌어서 놓기 등에 영향을 줄 수 있음)
이전 프로그램이 높은 DPI 설정에서 작동하도록 하기 위해 DWM은 유용한 대체를 구현합니다. 프로그램이 DPI 인식으로 표시되지 않으면 DWM은 DPI 설정에 맞게 전체 UI 배율을 조정합니다. 예를 들어, 144 DPI에서 UI는 150%로 배율이 조정됩니다(텍스트, 그래픽, 컨트롤 및 창 크기 포함). 프로그램이 500 × 500 창을 생성하면 창은 실제로 750 × 750 픽셀로 나타나고 창의 내용은 그에 따라 배율이 조정됩니다.
이 동작은 이전 프로그램이 높은 DPI 설정에서 "작동"한다는 것을 의미합니다. 그러나 배율을 조정하면 다소 흐릿한 모양이 나타나기도 합니다. 창을 그린 후에 배율 조정이 적용되기 때문입니다.
DPI 인식 애플리케이션
DWM 배율 조정을 방지하기 위해 프로그램은 스스로를 DPI 인식으로 표시할 수 있습니다. 그러면 자동 DPI 조정을 수행하지 않도록 DWM에 알립니다. 모든 새로운 애플리케이션은 DPI를 인식하도록 설계되어야 합니다. DPI 인식은 더 높은 DPI 설정에서 UI의 모양을 개선하기 때문입니다.
프로그램은 애플리케이션 매니페스트를 통해 스스로 DPI 인식을 선언합니다. 매니페스트는 DLL 또는 애플리케이션을 설명하는 단순한 XML 파일입니다. 매니페스트는 일반적으로 실행 파일에 포함되지만 별도의 파일로 제공할 수도 있습니다. 매니페스트에는 DLL 종속성, 요청된 권한 수준 및 프로그램이 어떤 Windows 버전에 맞게 설계되었는지와 같은 정보가 포함됩니다.
프로그램이 DPI를 인식함을 선언하려면 매니페스트에 다음 정보를 포함합니다.
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
여기에 표시된 목록은 부분 매니페스트일 뿐이지만 Visual Studio 링커에서 매니페스트의 나머지 부분이 자동으로 생성됩니다. 프로젝트에 부분 매니페스트를 포함하려면 Visual Studio에서 다음 단계를 수행합니다.
- 프로젝트 메뉴에서 속성을 클릭합니다.
- 왼쪽 창에서 구성 속성을 확장하고, 매니페스트 도구를 확장한 다음, 입력 및 출력을 클릭합니다.
- 추가 매니페스트 파일 텍스트 상자에 매니페스트 파일의 이름을 입력한 다음, 확인을 클릭합니다.
프로그램을 DPI 인식으로 표시하면 애플리케이션 창의 크기를 조정하지 말라고 DWM에 알리는 것입니다. 이제 500 × 500 창을 만들면 사용자의 DPI 설정에 관계없이 창은 500 × 500 픽셀을 차지합니다.
GDI 및 DPI
GDI 그리기는 픽셀 단위로 측정됩니다. 즉, 프로그램이 DPI 인식으로 표시되고 GDI에 200 × 100 사각형을 그리도록 요청하면 결과 사각형은 화면에서 너비가 200픽셀, 높이는 100픽셀이 됩니다. 단, GDI 글꼴 크기는 현재 DPI 설정으로 조정됩니다. 즉, 72포인트 글꼴을 만들면 글꼴 크기가 96 DPI에서는 96픽셀이지만 144 DPI에서는 144픽셀이 됩니다. 다음은 GDI를 사용하여 144 DPI로 렌더링된 72포인트 글꼴입니다.
애플리케이션이 DPI를 인식하고 그리기에 GDI를 사용하는 경우 모든 그리기 좌표를 DPI와 일치하도록 배율을 조정합니다.
Direct2D 및 DPI
Direct2D는 DPI 설정과 일치하도록 자동으로 배율 조정을 수행합니다. Direct2D에서 좌표는 DIP(장치 독립적 픽셀)라는 단위로 측정됩니다. DIP는 논리 인치의 1/96으로 정의됩니다. Direct2D에서 모든 그리기 작업은 DIP에 지정된 다음, 현재 DPI 설정에 맞게 배율이 조정됩니다.
DPI 설정 | DIP 크기 |
---|---|
96 | 1픽셀 |
120 | 1.25픽셀 |
144 | 1.5픽셀 |
예를 들어 사용자의 DPI 설정이 144 DPI이고 Direct2D에 200 × 100 사각형을 그리도록 요청하면 사각형은 300 × 150 물리적 픽셀이 됩니다. 또한 DirectWrite는 포인트가 아닌 DIP로 글꼴 크기를 측정합니다. 12포인트 글꼴을 만들려면 16 DIP(12포인트 = 1/6 논리 인치 = 96/6 DIP)를 지정합니다. 텍스트가 화면에 그려지면 Direct2D는 DIP를 물리적 픽셀로 변환합니다. 이 시스템의 이점은 측정 단위가 현재 DPI 설정에 관계없이 텍스트와 그리기 모두에 대해 일관성이 있다는 것입니다.
주의 사항: 마우스 및 창 좌표는 여전히 DIP가 아닌 물리적 픽셀로 제공됩니다. 예를 들어 WM_LBUTTONDOWN 메시지를 처리하는 경우 마우스 누름 위치는 물리적 픽셀로 제공됩니다. 해당 위치에 점을 그리려면 픽셀 좌표를 DIP로 변환해야 합니다.
물리적 픽셀을 DIP로 변환
DPI의 기본 값은 96으로 설정된 로 USER_DEFAULT_SCREEN_DPI
정의됩니다. 배율 인수를 확인하려면 DPI 값을 사용하고 를 로 USER_DEFAULT_SCREEN_DPI
나눕니다.
물리적 픽셀에서 DIP로의 변환에는 다음 수식을 사용합니다.
DIPs = pixels / (DPI / USER_DEFAULT_SCREEN_DPI)
DPI 설정을 얻으려면 GetDpiForWindow 함수를 호출합니다. DPI는 부동 소수점 값으로 반환됩니다. 두 축의 배율 인수를 계산합니다.
float g_DPIScale = 1.0f;
void InitializeDPIScale(HWND hwnd)
{
float dpi = GetDpiForWindow(hwnd);
g_DPIScale = dpi / USER_DEFAULT_SCREEN_DPI;
}
template <typename T>
float PixelsToDipsX(T x)
{
return static_cast<float>(x) / g_DPIScale;
}
template <typename T>
float PixelsToDips(T y)
{
return static_cast<float>(y) / g_DPIScale;
}
Direct2D를 사용하지 않는 경우 DPI 설정을 얻는 다른 방법은 다음과 같습니다.
void InitializeDPIScale(HWND hwnd)
{
HDC hdc = GetDC(hwnd);
g_DPIScaleX = (float)GetDeviceCaps(hdc, LOGPIXELSX) / USER_DEFAULT_SCREEN_DPI;
g_DPIScaleY = (float)GetDeviceCaps(hdc, LOGPIXELSY) / USER_DEFAULT_SCREEN_DPI;
ReleaseDC(hwnd, hdc);
}
참고
데스크톱 앱의 경우 GetDpiForWindow를 사용하고, UWP(유니버설 Windows 플랫폼) 앱의 경우 DisplayInformation::LogicalDpi를 사용하는 것이 좋습니다. 권장하지는 않지만 SetProcessDpiAwarenessContext를 사용하여 프로그래밍 방식으로 기본 DPI 인식을 설정할 수 있습니다. 프로세스에서 창(HWND)이 만들어지고 나면 DPI 인식 모드 변경이 더 이상 지원되지 않습니다. 프로세스 기본 DPI 인식 모드를 프로그래밍 방식으로 설정하는 경우에는 HWND가 생성되기 전에 해당 API를 호출해야 합니다. 자세한 내용은 프로세스에 대한 기본 DPI 인식 설정을 참조하세요.
렌더링 대상 크기 조정
창 크기가 변경되면 그에 맞게 렌더링 대상의 크기를 조정해야 합니다. 대부분의 경우 레이아웃도 업데이트하고 창을 다시 칠해야 합니다. 다음 코드는 이러한 단계를 보여줍니다.
void MainWindow::Resize()
{
if (pRenderTarget != NULL)
{
RECT rc;
GetClientRect(m_hwnd, &rc);
D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);
pRenderTarget->Resize(size);
CalculateLayout();
InvalidateRect(m_hwnd, NULL, FALSE);
}
}
GetClientRect 함수는 클라이언트 영역의 새 크기를 물리적 픽셀(DIP 아님)로 구합니다. ID2D1HwndRenderTarget::Resize 메서드는 렌더링 대상의 크기를 업데이트합니다(픽셀 단위로도 지정됨). InvalidateRect 함수는 전체 클라이언트 영역을 창의 업데이트 영역에 추가하여 다시 칠하기를 적용합니다. (모듈 1의 창 그리기 참조)
창이 커지거나 축소되면 일반적으로 그리는 개체의 위치를 다시 계산해야 합니다. 예를 들어 원 프로그램에서 반경 및 중심점을 업데이트해야 합니다.
void MainWindow::CalculateLayout()
{
if (pRenderTarget != NULL)
{
D2D1_SIZE_F size = pRenderTarget->GetSize();
const float x = size.width / 2;
const float y = size.height / 2;
const float radius = min(x, y);
ellipse = D2D1::Ellipse(D2D1::Point2F(x, y), radius, radius);
}
}
ID2D1RenderTarget::GetSize 메서드는 렌더링 대상의 크기를 레이아웃 계산에 적합한 단위인 DIP(픽셀 아님)로 반환합니다. 물리적 픽셀 단위의 크기를 반환하는 ID2D1RenderTarget::GetPixelSize와 밀접하게 관련된 메서드가 있습니다. HWND 렌더링 대상의 경우 이 값은 GetClientRect에서 반환된 크기와 일치합니다. 단, 그리기는 픽셀이 아닌 DIP 단위로 수행됩니다.