0
votes

I have a Windows application which I want to look good at high DPI monitors. The application is using DEFAULT_GUI_FONT in lots of places, and the font created this way doesn't scale correctly.

Is there any simple way to fix this problem with not too much pain?

3
Stop using DEFAULT_GUI_FONT. You're out of luck with any bitmap font; sorry.andlabs
@andlabs, DEFAULT_GUI_FONT uses Tahoma as default font, which is a vector one. And when I use one monitor and set DPI-Awareness for my application, while changing DPI at the monitor via Windows Resolution Settings the font got with DEFAULT_GUI_FONT changes it's size.deserg
That's not true. It's a bitmap font. blogs.msdn.microsoft.com/oldnewthing/20050707-00/?p=35013 Really, give it up already.David Heffernan
It may be Tahoma, but it's a bitmap version of Tahoma for compatibility reasons, and so you'll get the pixellation no matter what (even if at a high DPI the font is scaled up to try to reduce the amount of pixellation). It's a shame there's no real good "one true system font" on Windows, just a lot of dead ends like this one. RbMm's answer below is the most correct, but adopting it depends on what your code currently does. (I still have a suspicion Microsoft just wants us to hardcode Segoe UI 9 with no plans of ever changing the preferred font again...)andlabs
@DavidHeffernan It's not a bitmap font, and it's not Tahoma either. And it's not "dead". Both Raymond Chen and MSDN are wrong here. It's "Microsoft Sans Serif", not to be confused with "MS Sans Serif" (the latter is a bitmap font). It's also NOT a registry mapped pseudo-font like "MS Shell Dlg". It has also been the de-facto default font in C# since at least .NET 2.0. This is the font all classic Windows dialogs have been using since at least Vista, like File Properties or Folder Options.dialer

3 Answers

4
votes

you need get NONCLIENTMETRICS by SystemParametersInfo(SPI_GETNONCLIENTMETRICS,) and then use it LOGFONT data, for create self font. or you can query for SystemParametersInfo(SPI_GETICONTITLELOGFONT) and use it

1
votes

The recommended fonts for different purposes can be obtained from the NONCLIENTMETRICS structure.

For automatically DPI-scaled fonts (Windows 10 1607+, must be per-monitor DPI-aware):

// Your window's handle
HWND window;

// Get the DPI for which your window should scale to
UINT dpi = GetDpiForWindow(window);

// Obtain the recommended fonts, which are already correctly scaled for the current DPI
NONCLIENTMETRICSW non_client_metrics;

if (!SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(non_client_metrics), &non_client_metrics, 0, dpi)
{
    // Error handling
}

// Create an appropriate font(s)
HFONT message_font = CreateFontIndirectW(&non_client_metrics.lfMessageFont);

if (!message_font)
{
    // Error handling
}

For older Windows versions you can use the system-wide DPI and scale the font manually (Windows 7+, must be system DPI-aware):

// Your window's handle
HWND window;

// Obtain the recommended fonts, which are already correctly scaled for the current DPI
NONCLIENTMETRICSW non_client_metrics;

if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(non_client_metrics), &non_client_metrics, 0)
{
    // Error handling
}

// Get the system-wide DPI
HDC hdc = GetDC(nullptr);

if (!hdc)
{
    // Error handling
}

UINT dpi = GetDeviceCaps(hdc, LOGPIXELSY);

ReleaseDC(nullptr, hdc);

// Scale the font(s)
constexpr UINT font_size = 12;

non_client_metrics.lfMessageFont.lfHeight = -((font_size * dpi) / 72);

// Create the appropriate font(s)
HFONT message_font = CreateFontIndirectW(&non_client_metrics.lfMessageFont);

if (!message_font)
{
    // Error handling
}

NONCLIENTMETRICS has also many other fonts in it. Make sure to choose the right one for your purpose.

You should set the DPI-awareness level in your application manifest as described here for best compatibility.

0
votes

WinForms in the .NET framework internally converts the DEFAULT_GUI_FONT (which is in fact used to get the default font for WinForms Forms and Controls in most situations) by scaling its height from pixels (which is the unit GDI fonts use natively) to Points (which is preferred by GDI+). Drawing text using points implies that the physical size of the rendered text depends on the monitor DPI setting.

System.Drawing.Font.SizeInPoints:

float emHeightInPoints;
                    
IntPtr screenDC = UnsafeNativeMethods.GetDC(NativeMethods.NullHandleRef);

try {
    using( Graphics graphics = Graphics.FromHdcInternal(screenDC)){
        float pixelsPerPoint      = (float) (graphics.DpiY / 72.0);
        float lineSpacingInPixels = this.GetHeight(graphics);
        float emHeightInPixels    = lineSpacingInPixels * FontFamily.GetEmHeight(Style)  / FontFamily.GetLineSpacing(Style);
        
        emHeightInPoints    = emHeightInPixels / pixelsPerPoint;
    }
}
finally {
    UnsafeNativeMethods.ReleaseDC(NativeMethods.NullHandleRef, new HandleRef(null, screenDC));
}

return emHeightInPoints;

Obviously you cannot use this directly as it's C#. But besides that, this article suggests that you should scale pixel dimensions assuming a 96 dpi design, and use GetDpiForWindow to determine the actual DPI. Note that the "72" in the formula above has nothing to do with the monitor DPI setting, it comes from the fact that .NET likes to use fonts specified in points rather than pixels (otherwise just scale the LOGFONT's height by DPIy/96).

This site suggests something similar, but with GetDpiForMonitor.

I cannot say for sure whether the general approach of manually scaling the font size according to some DPI-dependent factor is a robust and future-proof for scaling fonts (it seems to be the way to go about scaling non-font GUI elements though). However, since .NET basically also just calculates some magic factor based on some sort of DPI value, it's probably a pretty good guess.

Also, you'll want to cache that HFONT. HFONT - LOGFONT conversions are not negligible.


See also (references):

WinForms gets its default using GetStockObject(DEFAULT_GUI_FONT) (there are a few exceptions though, mostly obsolete):

IntPtr handle = UnsafeNativeMethods.GetStockObject(NativeMethods.DEFAULT_GUI_FONT);        
try {
    Font fontInWorldUnits = null;

    // SECREVIEW : We know that we got the handle from the stock object,
    //           : so this is always safe.
    //
    IntSecurity.ObjectFromWin32Handle.Assert();
    try {
        fontInWorldUnits = Font.FromHfont(handle);
    }
    finally {
        CodeAccessPermission.RevertAssert();
    }

    try{
        defaultFont = FontInPoints(fontInWorldUnits);
    }
    finally{
        fontInWorldUnits.Dispose();
    }
}
catch (ArgumentException) {
}

https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/SystemFonts.cs,355

The HFONT is converted to GDI+, and then the GDI+ font retrieved this way is transformed using FontInPoints:

private static Font FontInPoints(Font font) {
    return new Font(font.FontFamily, font.SizeInPoints, font.Style, GraphicsUnit.Point, font.GdiCharSet, font.GdiVerticalFont);
}

https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/SystemFonts.cs,452

The content of the SizeInPoints getter is already listed above.

https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Advanced/Font.cs,992