DPI und geräteunabhängige Pixel
Um effektiv mit Windows-Grafiken zu programmieren, müssen Sie zwei verwandte Konzepte verstehen:
- Dots per Inch (DPI)
- Geräteunabhängige Pixel (DIPs).
Beginnen wir mit DPI. Dies erfordert einen kurzen Umweg in die Typografie. In der Typografie wird die Größe des Typs in Einheiten gemessen, die als Punkte bezeichnet werden. Ein Punkt entspricht 1/72 Zoll.
- 1 Pt = 1/72 Zoll
Hinweis
Dies ist die Desktopveröffentlichungsdefinition von Punkt. Historisch hat sich das genaue Maß eines Punkts verändert.
Beispielsweise ist eine 12-Punkt-Schriftart so konzipiert, dass sie in eine 1/6"-Textzeile (12/72) passt. Das bedeutet natürlich nicht, dass jedes Zeichen in der Schriftart genau 1/6 Zoll groß ist. Tatsächlich können einige Zeichen größer als 1/6" sein. Beispielsweise ist in vielen Schriftarten das Zeichen Å höher als die Nominalhöhe der Schriftart. Zur korrekten Anzeige benötigt die Schriftart zusätzlichen Platz zwischen dem Text. Dieser Bereich wird als führend bezeichnet.
Die folgende Abbildung zeigt eine 72-Punkt-Schriftart. Die einfarbigen Linien zeigen ein um den Text grenzendes 1"-Feld an. Die gestrichelte Linie wird als Baseline bezeichnet. Die meisten Zeichen in einer Schriftart ruhen auf der Baseline. Die Höhe der Schriftart umfasst den Teil über der Basislinie (den Aufstieg) und den Teil unterhalb der Grundlinie (den Abstieg). In der hier gezeigten Schriftart beträgt der Aufstieg 56 Punkte und der Abstieg 16 Punkte.
Wenn es um ein Computerdisplay geht, ist die Messung der Textgröße jedoch problematisch, da Pixel nicht alle dieselbe Größe haben. Die Größe eines Pixels hängt von zwei Faktoren ab: der Bildschirmauflösung und der physischen Größe des Monitors. Daher sind physische Zoll kein nützliches Maß, da es keine feste Beziehung zwischen physischen Zoll und Pixeln gibt. Stattdessen werden Schriftarten in logischen Einheiten gemessen. Eine 72-Punkt-Schriftart ist definiert, um einen logischen Zoll groß zu sein. Logische Zoll werden dann in Pixel konvertiert. Seit vielen Jahren verwendete Windows die folgende Konvertierung: Ein logischer Zoll entspricht 96 Pixel. Mit diesem Skalierungsfaktor wird eine 72-Punkt-Schriftart als 96 Pixel hoch gerendert. Eine 12-Punkt-Schriftart ist 16 Pixel hoch.
- 12 Punkte = 12/72 logischer Zoll = 1/6 logischer Zoll = 96/6 Pixel = 16 Pixel
Dieser Skalierungsfaktor wird als 96 Dots per Inch (DPI) beschrieben. Der Begriff Punkte leitet sich vom Druck ab, bei dem physische Tuschepunkte auf Papier gesetzt werden. Für Computeranzeigen wäre es genauer, 96 Pixel pro logischem Zoll zu sagen, aber der Begriff DPI ist nicht mehr vorhanden.
Da die tatsächlichen Pixelgrößen variieren, ist Text, der auf einem Monitor lesbar ist, auf einem anderen Monitor möglicherweise zu klein. Außerdem haben Menschen unterschiedliche Vorlieben – einige Menschen bevorzugen größeren Text. Aus diesem Grund ermöglicht Windows dem Benutzer, die DPI-Einstellung zu ändern. Wenn der Benutzer beispielsweise die Anzeige auf 144 DPI festlegt, ist eine 72-Punkt-Schriftart 144 Pixel hoch. Die Standard-DPI-Einstellungen sind 100% (96 DPI), 125% (120 DPI) und 150% (144 DPI). Der Benutzer kann auch eine benutzerdefinierte Einstellung anwenden. Ab Windows 7 ist DPI eine Benutzereinstellung.
DWM-Skalierung
Wenn ein Programm dpi nicht berücksichtigt, können die folgenden Fehler bei den Einstellungen mit hoher DPI-Auflösung auftreten:
- Beschneidte UI-Elemente.
- Falsches Layout.
- Pixelierte Bitmaps und Symbole.
- Falsche Mauskoordinaten, die sich auf Treffertests, Ziehen und Ablegen usw. auswirken können.
Um sicherzustellen, dass ältere Programme mit hohen DPI-Einstellungen funktionieren, implementiert die DWM ein nützliches Fallback. Wenn ein Programm nicht als DPI-fähig markiert ist, skaliert der DWM die gesamte Benutzeroberfläche so, dass es der DPI-Einstellung entspricht. Beispielsweise wird die Benutzeroberfläche bei 144 DPI um 150 % skaliert, einschließlich Text, Grafiken, Steuerelementen und Fenstergrößen. Wenn das Programm ein Fenster mit 500 × 500 erstellt, wird das Fenster tatsächlich als 750 × 750 Pixel angezeigt, und der Inhalt des Fensters wird entsprechend skaliert.
Dieses Verhalten bedeutet, dass ältere Programme bei Einstellungen mit hoher DPI-Auflösung "nur funktionieren". Die Skalierung führt jedoch auch zu einem etwas verschwommenen Erscheinungsbild, da die Skalierung nach dem Zeichnen des Fensters angewendet wird.
DPI-fähige Anwendungen
Um eine DWM-Skalierung zu vermeiden, kann sich ein Programm selbst als DPI-fähig markieren. Dadurch wird der DWM aufgefordert, keine automatische DPI-Skalierung durchzuführen. Alle neuen Anwendungen sollten so konzipiert werden, dass sie DPI-fähig sind, da die DPI-Wahrnehmung die Darstellung der Benutzeroberfläche bei höheren DPI-Einstellungen verbessert.
Ein Programm deklariert sich über sein Anwendungsmanifest für DPI-fähig. Ein Manifest ist einfach eine XML-Datei, die eine DLL oder Anwendung beschreibt. Das Manifest wird in der Regel in die ausführbare Datei eingebettet, kann jedoch als separate Datei bereitgestellt werden. Ein Manifest enthält Informationen wie DLL-Abhängigkeiten, die angeforderte Berechtigungsstufe und die Version von Windows, für die das Programm entworfen wurde.
Um zu deklarieren, dass Ihr Programm DPI-fähig ist, fügen Sie die folgenden Informationen in das Manifest ein.
<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>
Die hier gezeigte Auflistung ist nur ein partielles Manifest, aber der Visual Studio-Linker generiert den Rest des Manifests automatisch für Sie. Führen Sie die folgenden Schritte in Visual Studio aus, um ein partielles Manifest in Ihr Projekt aufzunehmen.
- Klicken Sie im Menü Projekt auf Eigenschaft.
- Erweitern Sie im linken Bereich Konfigurationseigenschaften, erweitern Sie Manifesttool, und klicken Sie dann auf Eingabe und Ausgabe.
- Geben Sie im Textfeld Zusätzliche Manifestdateien den Namen der Manifestdatei ein, und klicken Sie dann auf OK.
Indem Sie Ihr Programm als DPI-fähig markieren, weisen Sie den DWM an, ihr Anwendungsfenster nicht zu skalieren. Wenn Sie nun ein Fenster mit 500 × 500 erstellen, belegt das Fenster unabhängig von der DPI-Einstellung des Benutzers 500 × 500 Pixel.
GDI und DPI
Die GDI-Zeichnung wird in Pixeln gemessen. Das bedeutet, wenn Ihr Programm als DPI-fähig markiert ist und Sie GDI bitten, ein Rechteck mit 200 × 100 zu zeichnen, ist das resultierende Rechteck 200 Pixel breit und 100 Pixel hoch auf dem Bildschirm. Die GDI-Schriftgrößen werden jedoch auf die aktuelle DPI-Einstellung skaliert. Anders ausgedrückt: Wenn Sie eine Schriftart mit 72 Punkten erstellen, beträgt die Schriftgröße 96 Pixel bei 96 DPI, aber 144 Pixel bei 144 DPI. Hier ist eine 72-Punkt-Schriftart, die mit GDI mit 144 DPI gerendert wird.
Wenn Ihre Anwendung DPI-fähig ist und Sie GDI zum Zeichnen verwenden, skalieren Sie alle Zeichnungskoordinaten so, dass sie dem DPI-Wert entsprechen.
Direct2D und DPI
Direct2D führt die Skalierung automatisch an die DPI-Einstellung aus. In Direct2D werden Koordinaten in Einheiten gemessen, die als geräteunabhängige Pixel (DIPs) bezeichnet werden. Ein DIP wird als 1/96 eines logischen Zolls definiert. In Direct2D werden alle Zeichnungsvorgänge in DIPs angegeben und dann auf die aktuelle DPI-Einstellung skaliert.
DPI-Einstellung | DIP-Größe |
---|---|
96 | 1 Pixel |
120 | 1,25 Pixel |
144 | 1,5 Pixel |
Wenn die DPI-Einstellung des Benutzers beispielsweise 144 DPI beträgt und Sie Direct2D bitten, ein Rechteck mit 200 × 100 zu zeichnen, ist das Rechteck 300 × 150 physische Pixel. Darüber hinaus misst DirectWrite schriftgrößen in DIPs anstelle von Punkten. Um eine 12-Punkt-Schriftart zu erstellen, geben Sie 16 DIPs (12 Punkte = 1/6 logischer Zoll = 96/6 DIPs) an. Wenn der Text auf dem Bildschirm gezeichnet wird, konvertiert Direct2D die DIPs in physische Pixel. Der Vorteil dieses Systems besteht darin, dass die Maßeinheiten sowohl für Text als auch für Zeichnung konsistent sind, unabhängig von der aktuellen DPI-Einstellung.
Vorsicht: Maus- und Fensterkoordinaten werden weiterhin in physischen Pixeln angegeben, nicht in DIPs. Wenn Sie beispielsweise die WM_LBUTTONDOWN-Nachricht verarbeiten, wird die Maus nach unten in physischen Pixeln angegeben. Um einen Punkt an dieser Position zu zeichnen, müssen Sie die Pixelkoordinaten in DIPs konvertieren.
Konvertieren physischer Pixel in DIPs
Der Basiswert von DPI wird als USER_DEFAULT_SCREEN_DPI
definiert, der auf 96 festgelegt ist. Um den Skalierungsfaktor zu bestimmen, nehmen Sie den DPI-Wert, und dividieren Sie durch USER_DEFAULT_SCREEN_DPI
.
Die Konvertierung von physischen Pixeln in DIPs verwendet die folgende Formel.
DIPs = pixels / (DPI / USER_DEFAULT_SCREEN_DPI)
Um die DPI-Einstellung abzurufen, rufen Sie die GetDpiForWindow-Funktion auf. Der DPI-Wert wird als Gleitkommawert zurückgegeben. Berechnen Sie den Skalierungsfaktor für beide Achsen.
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;
}
Hier ist eine alternative Möglichkeit zum Abrufen der DPI-Einstellung, wenn Sie Direct2D nicht verwenden:
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);
}
Hinweis
Wir empfehlen, dass Sie für eine Desktop-App GetDpiForWindow verwenden. und für eine Universelle Windows-Plattform-App (UWP) verwenden Sie DisplayInformation::LogicalDpi. Obwohl dies nicht empfohlen wird, ist es möglich, die standardmäßige DPI-Erkennung programmgesteuert mithilfe von SetProcessDpiAwarenessContext festzulegen. Sobald ein Fenster (ein HWND) in Ihrem Prozess erstellt wurde, wird das Ändern des DPI-Sensibilisierungsmodus nicht mehr unterstützt. Wenn Sie den Prozessstandard-DPI-Erkennungsmodus programmgesteuert festlegen, müssen Sie die entsprechende API aufrufen, bevor HWNDs erstellt wurden. Weitere Informationen finden Sie unter Festlegen der Standard-DPI-Erkennung für einen Prozess.
Ändern der Größe des Renderziels
Wenn sich die Größe des Fensters ändert, müssen Sie die Größe des Renderziels entsprechend ändern. In den meisten Fällen müssen Sie auch das Layout aktualisieren und das Fenster neu streichen. Der folgende Code zeigt diese Schritte.
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);
}
}
Die GetClientRect-Funktion ruft die neue Größe des Clientbereichs in physischen Pixeln (keine DIPs) ab. Die ID2D1HwndRenderTarget::Resize-Methode aktualisiert die Größe des Renderziels, die ebenfalls in Pixel angegeben ist. Die InvalidateRect-Funktion erzwingt eine Neubemalung, indem der gesamte Clientbereich zum Updatebereich des Fensters hinzugefügt wird. (Siehe Zeichnen des Fensters in Modul 1.)
Wenn das Fenster vergrößert oder verkleinert wird, müssen Sie in der Regel die Position der objekte neu berechnen, die Sie zeichnen. Beispielsweise müssen im Kreisprogramm der Radius und der Mittelpunkt aktualisiert werden:
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);
}
}
Die ID2D1RenderTarget::GetSize-Methode gibt die Größe des Renderziels in DIPs (keine Pixel) zurück, die die geeignete Einheit zum Berechnen des Layouts ist. Es gibt eine eng verwandte Methode, ID2D1RenderTarget::GetPixelSize, die die Größe in physischen Pixeln zurückgibt. Bei einem HWND-Renderziel entspricht dieser Wert der von GetClientRect zurückgegebenen Größe. Denken Sie jedoch daran, dass das Zeichnen in DIPs und nicht in Pixeln ausgeführt wird.