user2371524
user2371524

Reputation:

win32: How to calculate control sizes for a consistent look across windows versions / themes?

Working on a simple GUI library, I'm starting with the backend and having some problems right now calculating the preferred sizes of controls. I'm comparing my results with those of Windows.Forms.

Right now, I'm using values from Design Specifications and Guidelines - Visual Design Layout (like Buttons and TextBoxes being 14 "Dialog Logical Units" high) for calculating the pixel sizes in the implementation, while keeping everything default with Windows Forms. I created these simple demo implementations:

Windows Forms (demo.cs):

using System.Drawing;
using System.Windows.Forms;

namespace W32CtlTest
{
    public class Demo : Form
    {
        private FlowLayoutPanel panel;
        private Button button;
        private TextBox textBox;

        public Demo() : base()
        {
            Text = "winforms";
            panel = new FlowLayoutPanel();

            button = new Button();
            button.Text = "test";
            button.Click += (sender, args) =>
            {
                Close();
            };
            panel.Controls.Add(button);

            textBox = new TextBox();
            panel.Controls.Add(textBox);

            Controls.Add(panel);
        }

        protected override Size DefaultSize
        {
            get
            {
                return new Size(240,100);
            }
        }

        public static void Main(string[] argv)
        {
            if (argv.Length < 1 || argv[0] != "-s")
            {
                Application.EnableVisualStyles();
            }
            Application.Run(new Demo());
        }
    }
}

compile with C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /out:demo.exe /lib:C:\Windows\Microsoft.NET\Framework\v4.0.30319 /reference:System.Windows.Forms.dll,System.Drawing.dll demo.cs

Win32 API (demo.c):

#include <string.h>

#include <windows.h>
#include <commctrl.h>

static HINSTANCE instance;
static HWND mainWindow;
static HWND button;
static HWND textBox;

#define WC_mainWindow L"W32CtlTestDemo"
#define CID_button 0x101

static NONCLIENTMETRICSW ncm;
static HFONT messageFont;
static TEXTMETRICW messageFontMetrics;
static int buttonWidth;
static int buttonHeight;
static int textBoxWidth;
static int textBoxHeight;

/* hack to enable visual styles without relying on manifest
 * found at http://stackoverflow.com/a/10444161
 * modified for unicode-only code */
static int enableVisualStyles(void)
{
    wchar_t dir[MAX_PATH];
    ULONG_PTR ulpActivationCookie = 0;
    ACTCTXW actCtx =
    {
        sizeof(actCtx),
        ACTCTX_FLAG_RESOURCE_NAME_VALID
            | ACTCTX_FLAG_SET_PROCESS_DEFAULT
            | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID,
        L"shell32.dll", 0, 0, dir, (LPWSTR)124,
        0, 0
    };
    UINT cch = GetSystemDirectoryW(dir, sizeof(dir) / sizeof(*dir));
    if (cch >= sizeof(dir) / sizeof(*dir)) { return 0; }
    dir[cch] = L'\0';
    ActivateActCtx(CreateActCtxW(&actCtx), &ulpActivationCookie);
    return (int) ulpActivationCookie;
}

static void init(void)
{
    INITCOMMONCONTROLSEX icx;
    icx.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icx.dwICC = ICC_WIN95_CLASSES;
    InitCommonControlsEx(&icx);
    ncm.cbSize = sizeof(ncm);
    SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0);
    messageFont = CreateFontIndirectW(&ncm.lfStatusFont);
    HDC dc = GetDC(0);
    SelectObject(dc, (HGDIOBJ) messageFont);
    GetTextMetricsW(dc, &messageFontMetrics);
    SIZE sampleSize;
    GetTextExtentExPointW(dc,
            L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
            52, 0, 0, 0, &sampleSize);
    ReleaseDC(0, dc);
    buttonWidth = MulDiv(sampleSize.cx, 50, 4 * 52);
    buttonHeight = MulDiv(messageFontMetrics.tmHeight, 14, 8);
    textBoxWidth = 100;
    textBoxHeight = MulDiv(messageFontMetrics.tmHeight, 14, 8);
    instance = GetModuleHandleW(0);
}

static LRESULT CALLBACK wproc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg)
    {
    case WM_CREATE:
        button = CreateWindowExW(0, L"Button", L"test",
                WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                2, 2, buttonWidth, buttonHeight,
                w, (HMENU)CID_button, instance, 0);
        SendMessageW(button, WM_SETFONT, (WPARAM)messageFont, 0);

        textBox = CreateWindowExW(WS_EX_CLIENTEDGE, L"Edit", L"",
                WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,
                6 + buttonWidth, 2, textBoxWidth, textBoxHeight,
                w, 0, instance, 0);
        SendMessageW(textBox, WM_SETFONT, (WPARAM)messageFont, 0);

        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    case WM_COMMAND:
        switch (LOWORD(wp))
        {
        case CID_button:
            DestroyWindow(w);
            break;
        }
        break;

    }

    return DefWindowProcW(w, msg, wp, lp);
}

int main(int argc, char **argv)
{
    if (argc < 2 || strcmp(argv[1], "-s"))
    {
        enableVisualStyles();
    }

    init();

    WNDCLASSEXW wc;
    memset(&wc, 0, sizeof(wc));
    wc.cbSize = sizeof(wc);
    wc.hInstance = instance;
    wc.lpszClassName = WC_mainWindow;
    wc.lpfnWndProc = wproc;
    wc.hbrBackground = (HBRUSH) COLOR_WINDOW;
    wc.hCursor = LoadCursorA(0, IDC_ARROW);
    RegisterClassExW(&wc);

    mainWindow = CreateWindowExW(0, WC_mainWindow, L"winapi",
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 240, 100,
            0, 0, instance, 0);
    ShowWindow(mainWindow, SW_SHOWNORMAL);

    MSG msg;
    while (GetMessageW(&msg, 0, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    return (int)msg.wParam;
}

compile with gcc -odemo.exe -O2 demo.c -lgdi32 -lcomctl32


The test code is also available on github


It looks like this on windows 10, with visual styles enabled in the upper row and disabled in the lower row:

control appearance on windows 10

One thing I soon found out is that Windows.Forms doesn't use the message font (as I had expected) but instead uses the DEFAULT_GUI_FONT Although that's not the right thing to do, I changed my win32 code accordingly so I can compare the results better:

control appearance on windows 10 with DEFAULT_GUI_FONT

For completeness, here is what it looks like on windows 7 without visual styles:

control appearance on windows 7 with DEFAULT_GUI_FONT

Now my questions are:

  1. Is it correct to use the message font? So, Windows.Forms definitely got this one "wrong"?

  2. Obviously Windows.Forms uses the 14 DLU height for Buttons, but some smaller height for TextBoxes. This contradicts the Design Specifications. So is Windows.Forms wrong here as well? Or should TextBoxes in fact be smaller, so the text doesn't look like it's "hanging from the ceiling"? I think this does look better the way Windows.Forms does it.

  3. Comparing visual styles enabled/disabled, I find that without visual styles, I get the same height for my button and my text box, but with visual styles enabled on windows 10, the text box is actually higher. Is there something like "theme specific metrics" and if so, how can I use that to correct my calculations?

Upvotes: 9

Views: 1668

Answers (1)

user2371524
user2371524

Reputation:

This is only a partial answer I'm adding here for reference:

Indeed, using the DEFAULT_GUI_FONT is wrong according to this blog entry by Raymond Chen. So, no need to trust to do "the right thing".

The Design Specifications indicate that Edit Controls should be the same height as Buttons (14 DLU). To convert these to pixel sizes, the Dialog Base Units (DBU) are needed, and while GetDialogBaseUnits() only returns them for the system font, there are MSDN articles describing how to calculate them for other fonts.

1 vertical DBU corresponds to 8 DLU, so an Edit control will be 6 DLU higher than the text it contains. This doesn't look so nice, because the Edit control doesn't center the text vertically but instead aligns it at the top. avoids this by calculating a smaller size for an Edit control. The drawback is that an Edit control will not align nicely next to a Button.

I found a kind of "hacky" solution to that problem by shrinking the client area of the Edit control in an overridden window proc. The following code compares the results (and contains controls using the system font for completeness):

#include <stdlib.h>
#include <string.h>

#include <windows.h>
#include <commctrl.h>

typedef struct PaddedControl
{
    WNDPROC baseWndProc;
    int vshrink;
} PaddedControl;

static HINSTANCE instance;
static HWND mainWindow;
static HWND buttonSF;
static HWND textBoxSF;
static HWND buttonMF;
static HWND textBoxMF;
static HWND buttonMFC;
static HWND textBoxMFC;
static PaddedControl textBoxMFCPadded;

#define WC_mainWindow L"W32CtlTestDemo"

static NONCLIENTMETRICSW ncm;
static HFONT messageFont;
static TEXTMETRICW messageFontMetrics;
static int controlHeightSF;
static int controlHeightMF;
static int buttonWidthSF;
static int buttonWidthMF;

/* hack to enable visual styles without relying on manifest
 * found at http://stackoverflow.com/a/10444161
 * modified for unicode-only code */
static int enableVisualStyles(void)
{
    wchar_t dir[MAX_PATH];
    ULONG_PTR ulpActivationCookie = 0;
    ACTCTXW actCtx =
    {
        sizeof(actCtx),
        ACTCTX_FLAG_RESOURCE_NAME_VALID
            | ACTCTX_FLAG_SET_PROCESS_DEFAULT
            | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID,
        L"shell32.dll", 0, 0, dir, (LPWSTR)124,
        0, 0
    };
    UINT cch = GetSystemDirectoryW(dir, sizeof(dir) / sizeof(*dir));
    if (cch >= sizeof(dir) / sizeof(*dir)) { return 0; }
    dir[cch] = L'\0';
    ActivateActCtx(CreateActCtxW(&actCtx), &ulpActivationCookie);
    return (int) ulpActivationCookie;
}

static void init(void)
{
    INITCOMMONCONTROLSEX icx;
    icx.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icx.dwICC = ICC_WIN95_CLASSES;
    InitCommonControlsEx(&icx);
    ncm.cbSize = sizeof(ncm);
    SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0);
    messageFont = CreateFontIndirectW(&ncm.lfStatusFont);

    LONG sysDbu = GetDialogBaseUnits();
    HDC dc = GetDC(0);
    SelectObject(dc, (HGDIOBJ) messageFont);
    GetTextMetricsW(dc, &messageFontMetrics);
    SIZE sampleSize;
    GetTextExtentExPointW(dc,
            L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
            52, 0, 0, 0, &sampleSize);
    ReleaseDC(0, dc);
    controlHeightSF = MulDiv(HIWORD(sysDbu), 14, 8);
    controlHeightMF = MulDiv(messageFontMetrics.tmHeight, 14, 8);
    buttonWidthSF = MulDiv(LOWORD(sysDbu), 50, 4);
    buttonWidthMF = MulDiv(sampleSize.cx, 50, 4 * 52);
    instance = GetModuleHandleW(0);
}

static LRESULT CALLBACK paddedControlProc(
        HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
    PaddedControl *self = (PaddedControl *)GetPropW(w, L"paddedControl");
    WNDCLASSEXW wc;

    switch (msg)
    {
    case WM_ERASEBKGND:
        wc.cbSize = sizeof(wc);
        GetClassInfoExW(0, L"Edit", &wc);
        RECT cr;
        GetClientRect(w, &cr);
        cr.top -= self->vshrink;
        cr.bottom += self->vshrink;
        HDC dc = GetDC(w);
        FillRect(dc, &cr, wc.hbrBackground);
        ReleaseDC(w, dc);
        return 1;

    case WM_NCCALCSIZE:
        if (!wp) break;
        LRESULT result = CallWindowProcW(self->baseWndProc, w, msg, wp, lp);
        NCCALCSIZE_PARAMS *p = (NCCALCSIZE_PARAMS *)lp;
        int height = p->rgrc[0].bottom - p->rgrc[0].top;
        self->vshrink = 0;
        if (height > messageFontMetrics.tmHeight + 3)
        {
            self->vshrink = (height - messageFontMetrics.tmHeight - 3) / 2;
            p->rgrc[0].top += self->vshrink;
            p->rgrc[0].bottom -= self->vshrink;
        }
        return result;
    }

    return CallWindowProcW(self->baseWndProc, w, msg, wp, lp);
}

static LRESULT CALLBACK wproc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg)
    {
    case WM_CREATE:
        buttonSF = CreateWindowExW(0, L"Button", L"sysfont",
                WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                4, 4, buttonWidthSF, controlHeightSF,
                w, 0, instance, 0);

        buttonMF = CreateWindowExW(0, L"Button", L"msgfont",
                WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                4, 8 + controlHeightSF, buttonWidthMF, controlHeightMF,
                w, 0, instance, 0);
        SendMessageW(buttonMF, WM_SETFONT, (WPARAM)messageFont, 0);

        buttonMFC = CreateWindowExW(0, L"Button", L"msgfont adj",
                WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                4, 12 + controlHeightSF + controlHeightMF,
                buttonWidthMF, controlHeightMF,
                w, 0, instance, 0);
        SendMessageW(buttonMFC, WM_SETFONT, (WPARAM)messageFont, 0);

        textBoxSF = CreateWindowExW(WS_EX_CLIENTEDGE, L"Edit", L"abcdefgh",
                WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,
                8 + buttonWidthSF, 4, 100, controlHeightSF,
                w, 0, instance, 0);

        textBoxMF = CreateWindowExW(WS_EX_CLIENTEDGE, L"Edit", L"abcdefgh",
                WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,
                8 + buttonWidthMF, 8 + controlHeightSF,
                100, controlHeightMF,
                w, 0, instance, 0);
        SendMessageW(textBoxMF, WM_SETFONT, (WPARAM)messageFont, 0);

        textBoxMFC = CreateWindowExW(WS_EX_CLIENTEDGE, L"Edit", L"abcdefgh",
                WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,
                8 + buttonWidthMF, 12 + controlHeightSF + controlHeightMF,
                100, controlHeightMF,
                w, 0, instance, 0);
        memset(&textBoxMFCPadded, 0, sizeof(PaddedControl));
        textBoxMFCPadded.baseWndProc = (WNDPROC)SetWindowLongPtr(
                textBoxMFC, GWLP_WNDPROC, (LONG_PTR)paddedControlProc);
        SetPropW(textBoxMFC, L"paddedControl", &textBoxMFCPadded);
        SetWindowPos(textBoxMFC, 0, 0, 0, 0, 0,
                SWP_NOOWNERZORDER|SWP_NOSIZE|SWP_NOMOVE|SWP_FRAMECHANGED);
        SendMessageW(textBoxMFC, WM_SETFONT, (WPARAM)messageFont, 0);

        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    }

    return DefWindowProcW(w, msg, wp, lp);
}

int main(int argc, char **argv)
{
    if (argc < 2 || strcmp(argv[1], "-s"))
    {
        enableVisualStyles();
    }

    init();

    WNDCLASSEXW wc;
    memset(&wc, 0, sizeof(wc));
    wc.cbSize = sizeof(wc);
    wc.hInstance = instance;
    wc.lpszClassName = WC_mainWindow;
    wc.lpfnWndProc = wproc;
    wc.hbrBackground = (HBRUSH) COLOR_WINDOW;
    wc.hCursor = LoadCursorA(0, IDC_ARROW);
    RegisterClassExW(&wc);

    mainWindow = CreateWindowExW(0, WC_mainWindow, L"fontdemo",
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 240, 180,
            0, 0, instance, 0);
    ShowWindow(mainWindow, SW_SHOWNORMAL);

    MSG msg;
    while (GetMessageW(&msg, 0, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    return (int)msg.wParam;
}

The last row of controls using this hack is the best I could achieve so far:

screenshots in win10 and win7

As you can see, a problem that still persists is that the heights of Button and Edit controls look different with the visual styles theme of Windows 10. So I'd still be happy to see a better answer to this question.

Upvotes: 4

Related Questions