How do you scale the title bar on a DPI aware win application?
Asked Answered
C

4

19

I am making my app dpi-aware per monitor by setting <dpiAware>True/PM</dpiAware> in the manifest file. I can verify with process explorer that this is indeed working or by calling GetProcessDpiAwareness.

This is all working fine and I can scale anything in the client area fine in my code. However, my only problem is that if I drag my app from a system-dpi monitor to a non-system dpi monitor, the title bar and any system menu would either become too big or too small. This isn't the case for most built-in apps (e.g. calc, edge browser, etc..) so there must be away to scale it properly. Does anyone how the devs at MS did this?

The screenshot below should explain my problem better. Also notice, that the padding between the close, min, and max button is different when it's scaled (96dpi).

Screenshot

Sample app I'm attaching a very simple app that is per-monitor dpi aware.

Contexture answered 3/8, 2015 at 7:34 Comment(11)
What does your per-monitor code looks like? Handler for WM_DPICHANGED?Syndesis
Is your app a universal application? Both calculator and Edge areCalmas
@Calmas It isn't a universal app. This is an old app - just wanted to make it scale properly on all my monitors. Do you think windows would scale the title bar if it is a universal app?Contexture
@David Heffernan Yes I'm handling WM_DPICHANGED and there's nothing fancy happening there just scaling fonts and button sizes.Contexture
It usually helps to show code rather than describe it. How about an MCVE?Syndesis
The app is a fork of a popular open source project and it's a bit hard to get a working snippet of the code. Anyway, part of the main idea about making it dpi-aware was taken from here. At the bottom, there is a zip file that you can download that contains the compiled binaries. The compiled app in the zip file has the same exact problem I'm having.Contexture
Just to reiterate, I'm not having troubles scaling the content. It's the non-client area that I'm having trouble getting windows to scale it. @Calmas just quickly created a universal app and can confirm it scales nicely and is per-monitor dpi aware as well. Only thing to find out now is whether I can replicate that behavior in a non-universal app.Contexture
OK. If you can't show an MCVE then I'm voting to close as off topic.Syndesis
Hmm.. ok. I'm not sure how you can call it off-topic but here's an off-the-shelf scaffold with the dpi-awareness code that I'm using. The code that sets the dpi-awareness is in wWinMain (not in manifest). WndProc handles WM_DPICHANGED as well but doesn't do anything. You can verify the dpi-awareness of the app using process explorer (the app runs its own verifications too)Contexture
Questions and answers on stackoverflow should be self-contained. Don't link to off-site content, when that content is relevant. Include the relevant part in your question instead.Horrible
ok done. afaik, the sample app is pretty much standard but I hope people would find that relevant.Contexture
F
7

Does anyone how the devs at MS did this?

This has a pretty disappointing answer. Using Alin Constantin's WinCheat and inspecting the top-level window of Calculator, I see a window size of 320x576, and a client size that is also 320x576.

In other words, Microsoft entirely avoids the problem by suppressing the non-client area of the window, putting everything in the client area instead. Making this work well for you may involve custom drawing of the title bar.

Something worth noting is that Calculator and e.g. Windows Explorer don't use the same colour for the title bars. Calculator doing custom drawing of the title bar would explain that perfectly.

Free answered 3/8, 2015 at 21:48 Comment(10)
Thanks. That's exactly what I was worried about though. I actually don't get the reasoning behind this. If they fixed this the proper way in win10, everyone would've benefited from it.Contexture
I did more research on this and it seems that UWP apps will not have this problem. This is because of the new ApplicationViewTitleBar class in Windows 10. Like @hvd said, in a UWP app there is no non-client area. The tile bar is drawn in the client area but will still function as it should without extra coding. I wish we could get the same behavior out of the box or at least call some apis to achieve the same thing in a native app though.Contexture
I don't think this explains it entirely. For example cmd.exe or the Win-R dialog have different sizes for window and client size but MAGICALLY redraw their window chrome, window menus, etc.Wadewadell
@Wadewadell For non-DPI-aware applications, Windows performs scaling of the complete window, including non-client area. I cannot check right now (not on a Win10 system) whether either of the windows you describe are created by a DPI-aware application, but my first guess is that they are not.Free
@hvd "cmd.exe" is unaware but spawns "conhost.exe" which shows up as per-monitor aware in procexp. It scales perfectly (on 100% and 200% DPI). The run-dialog also scales perfectly, but run an invalid program from it and it brings up an unscaled message box. Press the Find button and it'll bring up a perfectly scaled open file dialog. WTF. I realize cmd and run-dialog are probably very special windows, but the behaviour isn't easily explained by rendering everything on my own in the client area.Wadewadell
@jabe is right -- conhost is the only app I've seen that actually does its per-monitor DPI scaling 100% correctly and with a regular non-client area. Even the console options dialog scales correctly.Repugnant
@Wadewadell I just noticed the exact same thing about the Run dialog. I think that's the most interesting example, considering it's just a normal old Win32 dialog that is launched by explorer.exe. Looking at it with Spy++, there is absolutely nothing unusual. It's not a weirdo app like Calculator, and it's not even using a proprietary control like DirectUIHWND---all bog standard stuff. The fact that it displays an unscaled message box isn't unusual, that's the fault of the MessageBox API that hasn't been recoded to handle per-monitor DPI. More interesting is the Browse dialog is properly scaled...Damal
Trying to decide whether I should bounty this question or write a new one of my own. hvd, I like your answer, and I think it's correct as far as it goes, but it doesn't go quite far enough. As pointed out in other comments (see in particular mine above), the bog-standard Win32 Run dialog gets this right. As far as I can tell with Spy++, it is not owner-drawing its non-client area, and I very much doubt that the shell team has subclassed the Browse dialog that Run launches to owner-draw its title bar. So there must be some reasonable way of doing it. They can't have been total idiots.Damal
@CodyGray I would like to see a better answer myself, and I think if you bounty this question, there's a reasonable chance of not only not getting that, but also the system awarding the bounty to me when the whole point for the bounty was to get something better than my answer. I think creating a new question is reasonable. Include a link to this question and explain in your question that for your question, you're specifically looking to avoid owner-drawing title bars, and I think it shouldn't be mistakenly closed as a duplicate.Free
Done. Turned out to be ridiculously long, oh well. I wanted to give some impression of all the research I've done, even if only for my own reference later.Damal
R
10

The Windows 10 Anniversary Update (v1607) has added a new API you must call to enable DPI scaling of the non-client areas: EnableNonClientDpiScaling. This function should be called, when WM_NCCREATE is received. The message is sent to the window's procedure callback during window creation.

Example:

case WM_NCCREATE:
{
    if (!EnableNonClientDpiScaling(hWnd))
    {
        // Error handling
        return FALSE;
    }

    return DefWindowProcW(...);
}

If the application's DPI-awareness context is DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, then calling EnableNonClientDpiScaling should be omitted, as it won't have any effect, although the function will still return successfully.

From the documentation:

Non-client scaling for top-level windows is not enabled by default. You must call this API to enable it for each individual top-level window for which you wish to have the non-client area scale automatically. Once you do, there is no way to disable it. Enabling non-client scaling means that all the areas drawn by the system for the window will automatically scale in response to DPI changes on the window. That includes areas like the caption bar, the scrollbars, and the menu bar. You want to call EnableNonClientDpiScaling when you want the operating system to be responsible for rendering these areas automatically at the correct size based on the API of the monitor.

See this blog post for additional information about DPI scaling changes in Windows 10 AU.

Repugnant answered 14/9, 2016 at 22:57 Comment(2)
You don't need to call that EnableNonClientDpiScaling. Just add proper <dpiAwarness> : #31782267Weathersby
Not sure if this is true. I have <dpiAwareness>PerMonitorV2</dpiAwareness> in my manifest. When I call GetAwarenessFromDpiAwarenessContext it returns DPI_AWARENESS_PER_MONITOR_AWARE. EnableNonClientDpiScaling is required in WM_NCCREATE in this case, though it doesn't really work properly. Things only seem to be correctly scaled when I nudge the window to resize, having dragged it from one monitor to the other, higher DPI monitor. Without the call Windows makes no attempt to scale my non-client areas. All very strange.Toddler
F
7

Does anyone how the devs at MS did this?

This has a pretty disappointing answer. Using Alin Constantin's WinCheat and inspecting the top-level window of Calculator, I see a window size of 320x576, and a client size that is also 320x576.

In other words, Microsoft entirely avoids the problem by suppressing the non-client area of the window, putting everything in the client area instead. Making this work well for you may involve custom drawing of the title bar.

Something worth noting is that Calculator and e.g. Windows Explorer don't use the same colour for the title bars. Calculator doing custom drawing of the title bar would explain that perfectly.

Free answered 3/8, 2015 at 21:48 Comment(10)
Thanks. That's exactly what I was worried about though. I actually don't get the reasoning behind this. If they fixed this the proper way in win10, everyone would've benefited from it.Contexture
I did more research on this and it seems that UWP apps will not have this problem. This is because of the new ApplicationViewTitleBar class in Windows 10. Like @hvd said, in a UWP app there is no non-client area. The tile bar is drawn in the client area but will still function as it should without extra coding. I wish we could get the same behavior out of the box or at least call some apis to achieve the same thing in a native app though.Contexture
I don't think this explains it entirely. For example cmd.exe or the Win-R dialog have different sizes for window and client size but MAGICALLY redraw their window chrome, window menus, etc.Wadewadell
@Wadewadell For non-DPI-aware applications, Windows performs scaling of the complete window, including non-client area. I cannot check right now (not on a Win10 system) whether either of the windows you describe are created by a DPI-aware application, but my first guess is that they are not.Free
@hvd "cmd.exe" is unaware but spawns "conhost.exe" which shows up as per-monitor aware in procexp. It scales perfectly (on 100% and 200% DPI). The run-dialog also scales perfectly, but run an invalid program from it and it brings up an unscaled message box. Press the Find button and it'll bring up a perfectly scaled open file dialog. WTF. I realize cmd and run-dialog are probably very special windows, but the behaviour isn't easily explained by rendering everything on my own in the client area.Wadewadell
@jabe is right -- conhost is the only app I've seen that actually does its per-monitor DPI scaling 100% correctly and with a regular non-client area. Even the console options dialog scales correctly.Repugnant
@Wadewadell I just noticed the exact same thing about the Run dialog. I think that's the most interesting example, considering it's just a normal old Win32 dialog that is launched by explorer.exe. Looking at it with Spy++, there is absolutely nothing unusual. It's not a weirdo app like Calculator, and it's not even using a proprietary control like DirectUIHWND---all bog standard stuff. The fact that it displays an unscaled message box isn't unusual, that's the fault of the MessageBox API that hasn't been recoded to handle per-monitor DPI. More interesting is the Browse dialog is properly scaled...Damal
Trying to decide whether I should bounty this question or write a new one of my own. hvd, I like your answer, and I think it's correct as far as it goes, but it doesn't go quite far enough. As pointed out in other comments (see in particular mine above), the bog-standard Win32 Run dialog gets this right. As far as I can tell with Spy++, it is not owner-drawing its non-client area, and I very much doubt that the shell team has subclassed the Browse dialog that Run launches to owner-draw its title bar. So there must be some reasonable way of doing it. They can't have been total idiots.Damal
@CodyGray I would like to see a better answer myself, and I think if you bounty this question, there's a reasonable chance of not only not getting that, but also the system awarding the bounty to me when the whole point for the bounty was to get something better than my answer. I think creating a new question is reasonable. Include a link to this question and explain in your question that for your question, you're specifically looking to avoid owner-drawing title bars, and I think it shouldn't be mistakenly closed as a duplicate.Free
Done. Turned out to be ridiculously long, oh well. I wanted to give some impression of all the research I've done, even if only for my own reference later.Damal
W
2

UPDATE:

It is enough to add new <dpiAwarness> declaration to manifest to solve all this mess. Example is here.

Remnants of former investigations (obsolete):

More investigations on the subject.

System setup: two monitors, one is 96 dpi another one is 267 dpi (Microsoft Surface 4).

Testing window is moved to secondary 96 dpi monitor:

Here is rendering (wrong, IMO) with <dpiAware>true/pm</dpiAware> in manifest:

enter image description here

Note huge size of caption bar and wrong sizes of window icons.

And here is correct rendering using <dpiAware>true</dpiAware>

enter image description here

And I suspect that MSDN documentation is plainly misleading about values of PROCESS_DPI_AWARENESS. I do not see any differences in messages and styles between <dpiAware>true</dpiAware> and <dpiAware>true/pm</dpiAware>. The later one just makes caption larger. In both case application receives WM_DPICHANGED message while moving between monitors with different DPI.

Sigh.

Weathersby answered 2/8, 2016 at 19:52 Comment(2)
As was mentioned, on the Anniversary Update (and newer versions of Windows 10) you can get the non-client area to DPI scale by calling EnableNonClientDpiScaling from WM_NCCREATE. Check out this sample: github.com/Microsoft/Windows-classic-samples/tree/master/…Kristofer
In the Windows 10 Creators Update there will be a new DPI awareness context DPI_AWARENESS_PER_MONITOR_AWARE_V2 which will enable non-client scaling by default for a top-level window.Kristofer
S
0

The documentation says:

Note that the non-client area of a per monitor–DPI aware application is not scaled by Windows, and will appear proportionately smaller on a high DPI display.

The Microsoft apps that you link to deal with this by removing the non-client area and making the client area cover the entire window.

Syndesis answered 8/10, 2015 at 16:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.