Problems with per-monitor DPI aware MFC MDI app - c++

I'm working on making an MFC app display properly on multiple monitor environments with different DPI scaling. There is one issue I cannot explain which occurs when the primary and secondary monitors are running with different DPIs and the app is on the secondary monitor.
If the primary monitor has 100% DPI scaling (96) and the secondary monitor has the same 100% DPI scaling, all is fine.
If the primary monitor has 100% DPI scaling (96) and the secondary monitor has 125% scaling (120 DPI) or 150% scaling (144 DPI) or any other higher value, when child windows are maximized, part of the child window system bar is visible, as seen here:
125% scaling:
150% scaling:
If you look carefully, its 7 pixels for 125% and 14 for 150%. Given that the system bar is 29 pixels at 100% scaling, 36 and 125%, and 43 at 150%, those 7 and 14 pixels is the height difference between the bar size at 125% and 150% respectively, compared to the 100% baseline.
Therefore, it appears that the position and size of the bar is computed by the system as it was run on the primary monitor.
When you maximize the child window, there is a series of Windows messages that are sent to the window: WM_GETMINMAXINFO > WM_WINDOWPOSCHANGING > WM_GETMINMAXINFO > WM_NCCALSIZE > WM_WINDOWSPOSCHANGED > WM_MOVE > WM_SIZE. WM_GETMINMAXINFO is sent when the size or position of the window is about to change so that an app can override, for instance, the window's default maximized size and position. There is a note about this:
For systems with multiple monitors, the ptMaxSize and ptMaxPosition
members describe the maximized size and position of the window on the
primary monitor, even if the window ultimately maximizes onto a
secondary monitor. In that case, the window manager adjusts these
values to compensate for differences between the primary monitor and
the monitor that displays the window. Thus, if the user leaves
ptMaxSize untouched, a window on a monitor larger than the primary
monitor maximizes to the size of the larger monitor.
There is an article by Raymond Chan, explaining this: How does the window manager adjust ptMaxSize and ptMaxPosition for multiple monitors?.
So the ptMaxSize should be filled with the dimensions of the primary monitor. My primary monitor is 2560x1440 pixels and the size of the secondary monitor is 1920x1200. However, the value of the size I get here is 1757x1023 and 1761x1027 (at consecutive calls). This is neither the size of the primary nor the secondary monitor.
I tried to do a dirty trick and handled the WM_NCCALCSIZE message and set the position (left, top) at 0 (relative to the parent).
void CMyMDIChildWnd::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp)
{
CMDIChildWnd::OnNcCalcSize(bCalcValidRects, lpncsp);
if (condition)
{
lpncsp->rgrc[0].left = 0;
lpncsp->rgrc[0].top = 0;
}
}
Works fine as long as the child window has the focus. If I click on another window and it loses the focus, then the bar is redrawn and shows up at the previous position. This trick is only saying where the client area starts so when the non-client is redrawn I get back to the original problem.
My question is what could the root of this problem be and how can I try to fix it?

I came across possibly the same phenomenon, albeit from a different route, and my solution was very similar to yours, with a little extra, maybe this might help you?
void CFixedFrame::OnNcCalcSize ( BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp )
{
MDIBASEWND::OnNcCalcSize ( bCalcValidRects, lpncsp ) ;
if ( bCalcValidRects )
{
RECT& rcNew = lpncsp->rgrc[0];
RECT& rcOld = lpncsp->rgrc[1];
RECT& rcClient = lpncsp->rgrc[2];
// My problem arose because of the Aero bug (hardwired border widths)
// And also problems with Theming
const CNonClientMetrics ncm;
rcNew.top = ncm.iCaptionHeight + Aero related stuff not relevant to your problem
}
}
Where CNonClientMetrics is...
class CNonClientMetrics : public NONCLIENTMETRICS
{
public:
CNonClientMetrics ( )
{
cbSize = sizeof ( NONCLIENTMETRICS ) - sizeof ( this->iPaddedBorderWidth ) ;
SystemParametersInfo ( SPI_GETNONCLIENTMETRICS, sizeof ( NONCLIENTMETRICS ), this, 0 ) ;
}
} ;
[In a DPI-Aware way] This allowed me to get rid of that annoying border of blue (with buttons). As I understand it, you can no longer disable the DWM that draws that section.
I can't now find my original references, but in my notes for this problem, it only occurs for MFC's MDI Frame Windows.
Still, this link might also be useful?

Related

Qt - cross platform behaviour

I am trying to deploy a cross platform Qt application written in c++. It works fine on my Ubuntu Linux but when I run it on Windows the application's main window's position gets set on the very top left point of the screen with the upper frame (that holds the minimize, maximize, close buttons) missing.
That is it until i resize the main window (in this case making the width smaller from the right). When this happens the upper frame and the control buttons appear as in the visualization I provided.
Note: I've removed all widgets on the app so they do not аppear as a distraction.
Note2: It appears the maximize button is disabled, which is not the case inside Ubuntu. I have not set any window flags.
How do i visualize the upper frame at the very start of the application without the need to resize the window. I understand its an OS specific behaviour. Setting the main window's geometry with a starting point with a higher y value does NOT help. It still pops at the very top left of the screen.
try to use QWidget::move to set the Window position after setGeometry.
If the widget is a window, the position is that of the widget on the
desktop, including its frame.
You ask a question about cross-platform UI code, and then you don't show the full code. Please show the full code.
The one line of code you do show does something in the wrong way: if you want to maximize a window, call the appropriate function, instead of setting its absolute size that you think shows the window maximized. Windows, their decorations and their placement are very very platform specific, and you should prefer their cross-platform abstractions over trying to do them yourself.
Concretely: the window positioning handles the decorations (title bar) differently on Windows and on Ubuntu. There is absolutely nothing you can do about it except not position your windows absolutely like this.
In the MainWindow constructor at the end this->setGeometry(0, 0, 1336, 600);
That's the problem. setGeometry deals with the geometry of the client area. This is well documented. You should use move to change the position of the widget frame. To set the size of the frame requires the knowledge of the frame's incremental width and height:
bool setWidgetFrameGeometry(QWidget *w, const QRect &r) {
auto frame = w->frameGeometry().size();
auto client = w->size();
auto delta = frame - client;
auto maxDelta = 128;
if (delta.width() > 0 && delta.width() < maxDelta
&& delta.height() > 0 && delta.height() < maxDelta) {
w->move(r.topLeft());
w->resize(r.size() - delta);
return true;
}
return false;
}
The call may need to be deferred to when the event loop had a chance to run:
auto setter = [this]{ return setWidgetFrameGeometry(this, {0,0,1336,600}); };
if (!setter())
QTimer::singleShot(0, this, setter);

High DPI scaling, mouse hooks and WindowFromPoint

I have a process that sets a SetWindowsHookEx(WH_MOUSE_LL, , ,) right click hook. My process is set to DPI system aware on Window 10.0.10586 at 150% scaling on both monitors. This was set by calling SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE).
My problem scenario is e.g. Office 2007 is DPI unaware, so let's place MS Word on the right quarter of the screen. Right click just above the task bar in the bottom right and the mouse hook sends coordinates of 1279, 675 - scaled to Word. Then I right click on visual studio (DPI aware), nearly three quarters across the screen and the mouse hook sends me coordinates of e.g. 1279, 1008 from Visual Studio. So if I clicked higher up the screen I would potentially get the same 1279, 675.
My process is trying to determine which window is at the point by calling the WindowFromPoint API, but this will clearly fail in this scenario as two applications "share" the same point.
Is it possible to force the mouse hook to always send raw physical coordinates instead of those scaled to the DPI unaware application? and if so, how? Alternately, is there some other way of determining the hWnd or processID from the mouse hooks?
Microsoft fixed it in 10.0.14393.
You should have nothing in your client's network now with a lower build number unless they are on LTSB 10.0.10240.
This is the solution: DPI aware screen capture
Because the process is DPI aware, calling GetCursorPos() in the mouse hook callback handler always fetches raw physical coordinates instead of logical coordinates scaled to the application. Simply discard the coordinates passed to the mouse callback.
Added 30/09/2016
It's worth nothing that whilst GetMessagePos seems a likely, candidate it only returns the correct coordinates if the process is not dpi virtualised.
e.g.
VOID MessagePump()
{
MSG messageGet = { 0 };
DWORD dwPos;
POINTS p;
while (GetMessage(&messageGet,NULL,0,0))
{
dwPos = GetMessagePos();
p = MAKEPOINTS( dwPos );
TranslateMessage( &messageGet );
DispatchMessage( &messageGet );
}
}
The Mouse callback handler gets called during the GetMessage() call, but this does not fetch correct physical coordinates where DPI virtualisation is active for the process. e.g. physical x = 1909, y = 1072 comes back as 1091, 612 with 175% scaling, which though arithmetically correct, is not what was required.

SetWindowPos with scaled-up dialog on Laptop

Below is a very old function that has worked on numerous computers, never seen a bug, but now this laptop is experiencing problems. According to the tester, "Seems to be fine until I reboot and don’t have another monitor plugged in." It seems Windows 10 scales dialog content in some situations making the actual size differ from the designed size.
void ResizeComponent::SetWidth( int width /*= DEF_WIDTH*/ )
{
CRect rect;
this->GetWindowRect(rect);
this->SetWindowPos(NULL, 0,0, width, rect.Height(), /*resize only*/SWP_NOZORDER|SWP_NOMOVE);
}
Okay, usage info:
const static int WIDTH_PANEL4 = 585;
SetWidth(WIDTH_PANEL4);
According to a screenshot from that laptop, in one example the window is 581 wide, and when I run it on a development computer, it's also 581 wide. BUT: The laptop dialog is scaled larger, and so 581 is no longer the correct size.
I don't know how to deal correctly with this situation.
Because dialogs are laid out in "dialog units", I do not use hard pixel counts in my source. I base my dynamic size/position calculations based on the rendered size of the dialog and/or its controls. If your customer changes the system text size (Control Panel/Display Settings of 100% 125% 150% etc.), then you will definitely see issues if you code "hard 100% rendering" pixel values.
I am guessing that your laptop may be doing this type of "translation" when rendering with monitors that do not match the "native resolution" of the built-in laptop monitor.
Here is an example where I reposition OK/Cancel buttons based on the rendered positions (i.e. after chainback call to CDialog::OnInitDialog)
BOOL CSetupDlg::OnInitDialog()
{
CDialog::OnInitDialog();
if (m_bShowCancel)
{
// show/enable Cancel button and re-position the OK/CANCEL buttons (default is OK button is centered and cancel is hidden/disabled)
CWnd *pWndOK = GetDlgItem(IDOK);
CWnd *pWndCancel = GetDlgItem(IDCANCEL);
if (pWndOK->GetSafeHwnd() && pWndCancel->GetSafeHwnd())
{
CRect rOKOriginal;
pWndOK->GetWindowRect(&rOKOriginal);
this->ScreenToClient(rOKOriginal);
// move Cancel button to the immediate right of the centered OK button
pWndCancel->SetWindowPos(NULL, rOKOriginal.right, rOKOriginal.top, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER | SWP_SHOWWINDOW);
pWndCancel->EnableWindow(true);
// pWndCancel->ShowWindow(SW_SHOW);
// move OK button to the immediate left of its original/centered position
pWndOK->SetWindowPos(NULL, rOKOriginal.left - rOKOriginal.Width(), rOKOriginal.top, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER);
}
}
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
There are several possibilities:
The dialog is specified in "dialog units" for a font that's different than the font the code is actually using.
The border style changes between Windows versions weren't accounted for in the design of the dialog, and thus you're losing a few pixels.
The screen is high-DPI and the program isn't properly accounting for high-DPI, or it is but it hasn't told the OS that it knows how to (via manifest or SetProcessDPIAware or SetProcessDPIAwareness).
There isn't enough information in the question to know for sure the exact problem. I suspect #3, based on the fact that the behavior changes depending upon whether an external monitor is connected and on the fact that Windows 10 introduced more nuanced concepts of DPI awareness, like PROCESS_PER_MONITOR_DPI_AWARE.

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

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).
Sample app I'm attaching a very simple app that is per-monitor dpi aware.
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.
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.
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:
Note huge size of caption bar and wrong sizes of window icons.
And here is correct rendering using <dpiAware>true</dpiAware>
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.
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.

Detect Split Screen mode in windows 8

How to detect split screen mode in windows 8. I have a wim32 desktop application(written in MFC) and i need to provide some functionality in case of split screen mode
FYI - In split screen mode both desktop and metro mode come side by side
From your comments, the reason you're getting the screen size is because that's what you're asking for. Passing SM_CXSCREEN and SM_CYSCREEN to GetSystemMetrics() will return, as the name suggests, the width and height of the primary display.
There are a number of solutions, each with their pro's and con's, the simplest of which is probably:
RECT rcDesktop;
BOOL ok = GetWindowRect(GetDesktopWindow(), &rcDesktop);
This will return the size of the desktop window of the primary monitor. If you wanted just the "useable" area (taking into account the taskbar):
RECT rc;
BOOL ok = SystemParametersInfo(SPI_GETWORKAREA, 0, &rc, 0);
In the case of having a Modern-UI app docked to the side of the screen, both of those should return what you want, depending on whether you want to cover the taskbar with your program or not.
Note that those examples will only return information for the primary monitor on multi-monitor systems. You can get information about a specific monitor, such as the monitor that your current window is located on, by doing the following:
MONITORINFO mon_info;
mon_info.cbSize = sizeof(MONITORINFO);
BOOL ok = GetMonitorInfo(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &mon_info);
The MONITORINFO structure contains the size (and position - don't assume it's 0,0) of the requested monitor, including the work area:
Caveat: I'm not at home on my Windows8 system, so I can't check that all of these will return the correct information, but in theory checking the work area should do what you want, unless you specifically want your program to be full-screen.