DPI 和裝置獨立圖元
若要有效地使用 Windows 圖形進行程式設計,您必須瞭解兩個相關的概念:
- 每吋點數 (DPI)
- 與裝置無關的圖元 (DIP) 。
讓我們從 DPI 開始。 這需要簡短的印刷樣式。 在印刷樣式中,類型的大小是以稱為 點的單位來測量。 一點等於 1/72 英吋。
- 1 pt = 1/72 英吋
注意
這是點的桌面發佈定義。 在過去,點的確切量值已有所不同。
例如,12 點字型的設計目的是要符合 1/6 吋 (12/72) 行文字。 顯然,這並不表示字型中的每個字元都是 1/6 吋高。 事實上,某些字元可能高於 1/6」。 例如,在許多字型中,Å 字元的高度高於字型的標稱高度。 若要正確顯示,字型在文字之間需要一些額外的空格。 此空間稱為 前置。
下圖顯示 72 點字型。 實線會在文字周圍顯示 1 英吋高周框方塊。 虛線稱為 基準。 字型中的大部分字元會停留在基準上。 字型的高度包括基準上方的部分 () 和基準下方的部分 (下降) 。 在此顯示的字型中,ascent 為 56 點,而下降為 16 點。
不過,當電腦顯示器發生問題時,測量文字大小有問題,因為圖元大小不相同。 圖元的大小取決於兩個因素:顯示器解析度和監視器的實體大小。 因此,實體英吋不是有用的量值,因為實體英吋與圖元之間沒有固定關聯性。 相反地,字型會以 邏輯 單元來測量。 72 點字型定義為一個邏輯英吋高。 邏輯英吋接著會轉換成圖元。 Windows 已使用下列轉換:一個邏輯英吋等於 96 圖元。 使用此縮放比例,72 點字型會呈現為 96 圖元高。 12 點字型高度為 16 圖元。
- 12 點 = 12/72 邏輯英吋 = 1/6 邏輯英吋 = 96/6 圖元 = 16 圖元
此縮放比例描述為每英吋 96 個點, (DPI) 。 點一詞衍生自列印,其中將實際筆跡點放在紙張上。 針對電腦顯示器,每個邏輯英吋的 96 圖元會更精確,但 DPI 一詞停滯。
因為實際的圖元大小不同,所以一個監視器上可讀取的文字可能在另一個監視器上太小。 此外,人們有不同的喜好設定,有些人偏好較大的文字。 基於這個理由,Windows 可讓使用者變更 DPI 設定。 例如,如果使用者將顯示器設定為 144 DPI,則 72 點字型高度為 144 圖元。 標準 DPI 設定為 100% (96 DPI) 、125% (120 DPI) ,以及 150% (144 DPI) 。 使用者也可以套用自訂設定。 從 Windows 7 開始,DPI 是每個使用者設定。
DWM 調整
如果程式未考慮 DPI,在高 DPI 設定中可能會顯示下列瑕疵:
- 裁剪的 UI 元素。
- 版面配置不正確。
- 圖元點陣圖和圖示。
- 不正確的滑鼠座標,可能會影響點擊測試、拖放等等。
為了確保較舊的程式可在高 DPI 設定下運作,DWM 會實作有用的後援。 如果程式未標示為 DPI 感知,DWM 會調整整個 UI 以符合 DPI 設定。 例如,在 144 DPI 時,UI 會縮放 150%,包括文字、圖形、控制項和視窗大小。 如果程式建立 500 × 500 個視窗,則視窗實際上會顯示為 750 × 750 圖元,並據以調整視窗的內容。
此行為表示較舊的程式在高 DPI 設定中「只運作」。 不過,縮放也會產生一些模糊的外觀,因為縮放會在繪製視窗之後套用。
DPI 感知應用程式
為了避免 DWM 縮放,程式可以將本身標示為 DPI 感知。 這會告訴 DWM 不要執行任何自動 DPI 縮放比例。 所有新的應用程式都應該設計為 DPI 感知,因為 DPI 感知可改善 UI 在較高 DPI 設定的外觀。
程式會透過其應用程式資訊清單宣告本身 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 圖元,但 144 圖元為 144 DPI。 以下是使用 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 的基底值定義為 USER_DEFAULT_SCREEN_DPI
設定為 96。 若要判斷縮放比例,請採用 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 PixelsToDipsY(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;針對 通用 Windows 平臺 (UWP) app,請使用DisplayInformation::LogicalDpi。 雖然我們不建議這麼做,但可以使用 SetProcessDpiAwarenessCoNtext以程式設計方式設定預設 DPI 感知。 一旦視窗 (在程式中建立 HWND) ,就不再支援變更 DPI 感知模式。 如果您要以程式設計方式設定進程預設 DPI 感知模式,則必須先呼叫對應的 API,才能建立任何 HWND。 如需詳細資訊,請參閱 設定進程的預設 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 執行,而不是圖元。