diff --git a/Samples/WindowPlacement/README.md b/Samples/WindowPlacement/README.md new file mode 100644 index 00000000..0d15b8fc --- /dev/null +++ b/Samples/WindowPlacement/README.md @@ -0,0 +1,858 @@ + +# Remembering Window Positions with PlacementEx + +This README is paired with the header files in this directory: + +- [PlacementEx.h](PlacementEx.h) +- [MonitorData.h](MonitorData.h) +- [MiscUser32.h](MiscUser32.h) +- [CurrentMonitorTopology.h](CurrentMonitorTopology.h) +- [VirtualDesktopIds.h](VirtualDesktopIds.h) + +These header files simplify storing & restoring window positions for apps. For +example, storing the position when a window closes and using it to pick an +initial position when the app launches. In this way, they are an extension of +[`SetWindowPlacement`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowplacement) +and +[`GetWindowPlacement`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowplacement). + +The intended audience of this file is developers who are using or modifying +these headers in apps or frameworks. + +These header files use only public APIs that have been stable since before +Windows 10 (and in most cases long before). For example: + + - [CreateWindow](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa) + - [SetWindowPos](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos) + - [GetMonitorInfo](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa) + - [GetWindowPlacement](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowplacement) + - [SetWindowPlacement](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowplacement) + - [GetDpiForWindow](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdpiforwindow) + - [IsWindowArranged](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-iswindowarranged) + - [GetDpiForMonitor](https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-getdpiformonitor) + - [DwmGetWindowAttribute](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmgetwindowattribute) + - [IVirtualDesktopManager](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ivirtualdesktopmanager) + +## Overview + +Most users expect apps to launch where they last closed. For example, apps +closed on secondary monitors should relaunch on that monitor (and not on the +primary). Or if maximized, apps should relaunch maximized (not at their restore +position). + +Most apps today attempt to do this. The +[`SetWindowPlacement`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowplacement) +and +[`GetWindowPlacement`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowplacement) +APIs assist with this. But even when using these APIs correctly, sometimes apps +launch in unexpected places, especially for users with monitors that have +different DPI scales, or whose machines reboot or dock/undock frequently. + +This happened because restore was initially (~Win3.1) fairly simple and grew in +complexity over many decades. **Multiple monitors**, **DPI**, **Arrangement**, +and **Virtual Desktops** in particular make positioning top-level windows more +complicated, which in turn makes storing window positions (and using them later) +more complicated. This README, and [PlacementEx.h](PlacementEx.h), introduce +fresh guidance for storing window positions. + +## Background + +### Screen Coordinates + +Windows (HWNDs) parented to the Desktop window are called [**top-level windows**](https://learn.microsoft.com/en-us/windows/win32/winmsg/about-windows#parent-or-owner-window-handle). +These windows can move freely between all monitors. (As opposed to **child windows**, +which move relative to their parent). + +The coordinate space of the Desktop window, the one top-level windows move within, +is called **Screen Coordinates**. + +There is always a monitor connected to the system whose top-left corner is 0,0 +in screen coordinates. This is the primary monitor. + +Monitors to the left of the primary have negative X values, and ones above the +primary have negative Y values. + +### Monitors (`HMONITOR`s) + +APIs like [`MonitorFromPoint`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfrompoint) and +[`MonitorFromRect`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromrect) +return a handle to a **monitor**, an `HMONITOR`. This handle can be used with APIs +like [`GetMonitorInfo`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa) +and [`GetDpiForMonitor`](https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-getdpiformonitor) +to read critical information about a monitor: + +- **Monitor Rect**. This is the position and size (resolution) of the monitor. +- **Work Area**. This is a subset of the monitor rect that isn't covered by the + taskbar (or other docked toolbars, like Voice Access). +- **DPI**. This is the scale for content on the monitor. See **DPI**, below. +- **Display Name**. This is a string uniquely identifying the monitor. This is + available in `MONITORINFOEX` as `szDevice`. + +See [MonitorData.h](MonitorData.h), which defines helpers to query the monitor +data. + +### DPI + +Since Vista, Windows supports custom **DPIs** (Dots Per Inch) and scale factors, +so that apps scale properly on high-resolution displays. In 8.1, this was +extended to allow each monitor to have its own DPI, and this has improved a few +times since. Windows scales 'DPI-unaware' apps by stretching them, while +'DPI-aware' apps/windows must scale themselves. This is typically done via a +**scale factor**, which is `(DPI) / 96`; a logical DPI of 144 would have a scale +factor of 144 / 96 = 150%. + +**Logical** units refer to units that have been scaled by this scale factor, +whereas **physical** units refer to units that have not. For example, 12 logical +pixels on a 150% scale monitor correspond to 18 physical pixels. Screen +coordinates are in physical pixels. + +Today, there are 3 types of [DPI awareness](https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows). A window can be any one of these: + + - **DPI unaware**. The window renders itself at the Windows default of 96 DPI (scale factor 100%). Windows stretches this window bitmap to the actual DPI of the monitor. + + - **"System DPI" aware**. Previously called 'Aware'. The window receives the + primary monitor's DPI at the time the process was launched, and must scale + its UI appropriately. Windows stretches this window bitmap if the window + moves to a monitor with a different DPI or if the primary monitor's DPI + changes. + + - **"Per-Monitor DPI" aware**. The window is expected to handle + [`WM_DPICHANGED`](https://learn.microsoft.com/en-us/windows/win32/hidpi/wm-dpichanged), + which tells the window when its DPI scale should change (e.g. when moved + between monitors). This provides the window a `RECT` that must be used to + resize the window to the new DPI. + +Consider a console window with some number of lines of text, or a browser window +with some number of paragraphs or pictures. If the window is Per-Monitor DPI +aware, and changes from 200% to 100% scaling without changing its window size +(but scales its *content* correctly), the content visible in the window would +double/quadruple. For example, a window that is 640 logical pixels wide would be +1280 physical pixels at 200% scaling, and should shrink down to 640 physical +pixels when moved to an 100% scale monitor. + +It is possible for a thread to change its awareness, using [`SetThreadDpiAwarenessContext`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setthreaddpiawarenesscontext). +When a window is created, it is 'stamped' with the thread awareness at the +time, and the thread will automatically switch back to that awareness when +dispatching messages to the window. This allows a thread to create two windows +with different awarenesses, which means the coordinates seen by the two windows +will be different. + +To know the DPI a window is currently scaling to: [`GetDpiForWindow`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdpiforwindow). + +>![WARNING] +> +>Do not use `MonitorFromWindow` followed by `GetDpiForMonitor` to determine a +window's current DPI. This can be wrong while a window is being dragged between +monitors of different DPIs, or briefly while monitors are changing (for example +after the primary monitor changes but before the window is moved to stay on the +same monitor it was on, which is now in a different location). + +## Window positioning concepts + +### Maximize, Minimize, and Restore + +The API `ShowWindow` can **Maximize**, **Minimize**, and **Restore** a window. + +**Maximized** windows fit the monitor. By default, this will moves the window's +resize borders *outside* the work area. Indeed, moving the cursor to the top of +the monitor and dragging will begin *moving* a Maximized window, not resizing it +(although this will typically *cause* a resize as the window unmaximizes and +begins moving). And moving the cursor to the top-right corner of the monitor +should put it over the Maximized window's close button. + +**Minimized** windows are normally off-screen and can be restored by clicking on +the Taskbar, or pressing Alt-Tab. On SKUs without a Shell (or a Taskbar), the +default Minimize position is the bottom-right of the monitor. + +If the window is not in a special state like Maximized/Minimized, the window is +'normal', or **Restored**. When a normal window becomes Maximized/Minimized, its +previous position is saved (as the normal position). Restoring a window in a +state like Maximized/Minimized moves it back to this normal position. + +The window styles `WS_MAXIMIZED` and `WS_MINIMIZED` are set when a window is in +these states. But using `SetWindowLongPtr` to set these styles directly will not +function correctly: it will not move the window to be maximized or cache its +normal/restore position. Use [`ShowWindow`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow) with something like `SW_MAXIMIZE`, +`SW_MINIMIZE`, or `SW_SHOWMINNOACTIVE` instead. + +To check if a window is Maximized: [`IsZoomed`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-iszoomed) + +To check if a window is Minimized: [`IsIconic`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isiconic) + +### Arranged (Snapped) + +**Arranged** (or **Snapped**) windows are similar to Maximized windows. They have a normal +position that is different from their 'real' position (`GetWindowRect`), and the +real position is 'fit' to the monitor's work area. + +While Maximized windows fill the entire work area, Arranged windows are aligned +with some number of edges of the work area. For example, left half, corners, +columns, etc. Dragging the Arranged window or double-clicking the title bar will +(like with Maximized windows) cause the window to restore to its normal size. + +To check if a window is Arranged: [`IsWindowArranged`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-iswindowarranged) + +Users can Arrange windows in many ways: + + - Drag a window and hit the edges of the monitor with the cursor + - Drag a window and drop it onto the Snap Flyout that appears at the top of the screen + - Hotkeys like Win+Left/Right + - Snap another window and choose this window from Snap Assist + - See [this MSDN page](https://support.microsoft.com/en-us/windows/snap-your-windows-885a9b1e-a983-a3b1-16cd-c531795e6241) for more info. + +However, `GetWindowPlacement` (detailed below) does not handle Arranged windows. +Also, unlike Maximized/Minimized, there is no `ShowWindow` command (or other +API) to Arrange a window. There is a workaround for this, but it is not trivial. +It's based on 2 factors: + + - Double-clicking on a window's top (or bottom) resize area will Arrange + the window. This aligns the top and bottom borders with the top/bottom of the + work area. + + - Apps can move themselves while Arranged. + + This is true of Maximized as well, though it is not generally advised. + + You can call `SetWindowPos` to move a Maximized, Minimized, or Arranged + window, but this will not change the window's state (styles) or normal + position. Doing this without consulting the monitor information can leave + the window in an unexpected position, possibly off-screen! + +This means that you can do something like this to Arrange a window +and set its position: + +```cpp +DefWindowProc(hwnd, WM_NCLBUTTONDBLCLK, HTTOP, 0); +SetWindowPos(hwnd, nullptr, x, y, cx, cy, SWP_NOZORDER | SWP_NOACTIVATE); +``` + +Be careful when moving an Arranged or Maximized window: + + - Arranged positions should fit a work area, aligning visibly with the monitor + edges. + + While you can move an Arranged or Maximized window into the center of the + screen, this will look and behave strangely! The user will see unexpected + window borders (ones for Max/Arranged windows), and moving the window will + 'restore' the window to a previous size, which could look unexpected. + + Normally, Arranged windows are perfectly aligned with 2 or 3 edges of the + work area. For example, left half, top-left corner, or top/bottom. + + - When moving an Arranged rect between monitors, the relative distance to + each edge of the monitor should be maintained. This is unlike the normal + rect, which generally should retain its logical size. + + - Some windows have invisible resize areas. + + Simply positioning the window (`SetWindowPos`) to align with the edges of + the monitor will leave visible gaps between the window and the monitor, + because part of the window is invisible/transparent. + + To query the visible bounds of a window (the extended frame bounds): + + ```cpp + RECT rcFrame; + HRESULT hr = DwmGetWindowAttribute(hwnd, + DWMWA_EXTENDED_FRAME_BOUNDS, &rcFrame, sizeof(rcFrame)); + ``` + + This value can be compared with `GetWindowRect` to know the size of the + window's invisible resize areas. + + Caveats: + - The window should not be Maximized/Minimized when getting the frame + bounds (the values are different in those states). + - Changing a window's DPI (moving it between monitors) can change the + size of the invisible area! Apps moving windows between monitors + should move the window to the target monitor before querying the + window's frame bounds. + - This API does not handle DPI virtualization. If an app or window is + virtualized for DPI (for example, a DPI unaware window), these values + may not match those from GetWindowRect. + +### Cascading + +If an app launches multiple windows, or if the user launches an app multiple +times, it is common to **cascade** the windows. This means moving each window +down and right a bit, leaving the previous window's title bar visible. If the +new position is now outside the work area, the window is moved to the top or +left side of the same monitor. + +The size of the 'nudge' is ideally the height of the window's caption bar. +This can be queried using system metrics: + +```cpp +UINT captionheight = + GetSystemMetricsForDpi(SM_CYCAPTION, dpi) + + (2 * GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi)); +``` + +Adding `2 * SM_CXSIZEFRAME` ensures the entire title bar from the previous +window is visible, since it includes the invisible resize area (which is part of +the title bar when not Maximized). + +> ![NOTE] +> +> The similar function [`GetSystemMetrics`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics) +returns values for the *process* DPI. If an app is Per-Monitor DPI aware, it +should use [`GetSystemMetricsForDpi`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetricsfordpi) +and the DPI of the window, [`GetDpiForWindow`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdpiforwindow). + +## Common factors when relaunching windows + +### `Get*` and `SetWindowPlacement` + +[GetWindowPlacement](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowplacement) and [SetWindowPlacement](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowplacement) are the main APIs intended to remember window placement between launches. They handle Maximized, Minimized, and Restored windows to an extent. However, they do not handle Arranged windows. + +`GetWindowPlacement` returns a [`WINDOWPLACEMENT`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-windowplacement), which contains: + + - `rcNormalPosition` is the window position (the normal, restore position if + Maximized, Minimized, or Arranged.) + - `showCmd` is a `ShowWindow` command, `SW_MAXIMIZE`, `SW_MINIMIZE`, or `SW_NORMAL`. + - `flags` can be `WPF_RESTORETOMAXIMIZED` (and others). + - **Note:** It is not recommended to use the other fields, like ptMinPosition. + +[`SetWindowPlacement`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowplacement) accepts this `WINDOWPLACEMENT` struct and +sets a window's normal position and Maximize state. Internally, this calls [`SetWindowPos`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos) +and [`ShowWindow`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow). + +When these APIs were created, prior to multiple monitor support, apps could +`GetWindowPlacement` on exit, store the `WINDOWPLACEMENT` to the registry, and +`SetWindowPlacement` when launching, and the app successfully launched where it +was when last closed in all cases. Today, doing this (and nothing else to +adjust the position) would lead to the app sometimes being the wrong size or +partially off-screen. + +> ![NOTE] +> The normal rect is in 'workspace coordinates', which is offset by the space +between the window's monitor's work area and monitor rect. These coordinates +match screen coordinates, even on secondary monitors, unless the taskbar is on +the top or left. + +### The `STARTUPINFO` + +There is a struct called [**`STARTUPINFO`**](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa), +which contains flags set by whoever launched the process, for example by the +taskbar or start menu. Apps can query these flags using [`GetStartupInfo`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getstartupinfow). A few `STARTUPINFO` flags relate to the position of the app's first window. + + - `STARTF_USESHOWWINDOW` is set if the caller requested that the window launch + `SW_MAXIMIZE` or `SW_MINIMIZE`. Note that apps can 'overrule' this if the app last + closed Maximized and is being launched with `SW_NORMAL`. + + For example, from cmd: + ```cmd + > start /min notepad + ``` + + From PowerShell: + ```powershell + > start -WindowStyle Minimized notepad + ``` + + - The `hStdOutput` can, for non-console apps, be an `HMONITOR`. This is the + 'Monitor Hint.' If launched from the Start Menu or Taskbar on a secondary + monitor, this `HMONITOR` will be the monitor the user clicked on to launch the + app. + + - Less used/useful are `STARTF_USEPOSITION` and `STARTF_USESIZE`. These specify the + position and size of the first window. + +### `CreateWindow` parameters + +[**`CreateWindow`**](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowa) +takes an: `x`, `y`, `nWidth`, `nHeight`, and styles. (And other parameters not +discussed here.) + +If the position and size parameters are ALL `CW_USEDEFAULT` (a special value), +the window will be in the default position: + +- On the primary, unless a monitor hint is set in `STARTUPINFO`. +- Cascading: at a position a bit down/right from the last window to get the default + position on that monitor, see above. +- A size picked from the size of the monitor (likely too large on very large + monitors). + +Note that if the size parameters are set, the window will *always* launch on the +primary and at the requested size (which is assumed scaled to the DPI of the +primary monitor). + +If a process is launched with the `STARTUPINFO` show command set, that is applied +to the first window the app makes visible (matching some criteria, like `WS_CAPTION`). + +Since these parameters must all be `CW_USEDEFAULT` for the magic behavior to +occur, when restoring window positions (including Maximize state, size, and +monitor), it is recommended to create the window hidden (without `WS_VISIBLE`) +and in the default position (`CW_USEDEFAULT`). Then, after the window is +created, the app should move the window to its correct position (which in some +cases requires moving multiple times), and only show the window when it is in +the final position. + +Providing a size or position to `CreateWindow` requires querying the monitor +information and pre-scaling the size to the DPI of the monitor (and adjusting +it to ensure the position is on screen). If Maximizing/Minimizing, this would +be translated to the styles field (`WS_MAXIMIZE`/`WS_MINIMIZE`). + +Apps can also position themselves within `WM_CREATE`. Apps can pick an initial +size here (and call `SetWindowPos`), but calling `ShowWindow` or +`SetWindowPlacement` to Maximize the window can cause problems! After +`WM_CREATE` returns, if the window has `WS_MAXIMIZE`/`WS_MINIMIZE` styles, it is +assumed that the window was created with these styles (and its current position +is the normal position). If the window has been Maximized already at this point, +restoring it will not move the window (its normal position was set to the +Maximize position). This is (much) worse if done for Minimize, since it could +cause the window to be stuck off screen. + +## Restarting windows after updates/crashes + +There are also several other factors to consider when apps *restart themselves* +(e.g. after updating themselves) or are restarted automatically (e.g. after a +system update causes a reboot). + +### Relaunching Minimized + +If an app is closed while the window is Maximized, it is ideal to launch +Maximized. But, if closed while Minimized, for example via Taskbar/Alt-Tab, the +window should typically launch to its restore position. (As a bonus, you can use +`WPF_RESTORETOMAXIMIZED` to launch Maximized if the window was Maximized prior +to Minimizing and closing.) + +BUT, what if the app restarts itself? If a window is Minimized while the app +restarts, the app should relaunch the window Minimized, 'staying Minimized' +over the app restart. + +Similarly, if the system reboots while the app is open and minimized, the window +should be Minimized after reboot. (In other words, if a machine reboots +overnight, all Minimized windows should not be restored from Minimize). + +As a general rule: when storing positions, remember the Minimize state. When +launching normally, this state should be ignored (the window should launch to +the normal position, or Maximized if WPF_RESTORETOMAXIMIZED). But when +*relaunching* after a reboot or crash, relaunch to Minimized if it was +previously Minimized. + +>![NOTE] +> +> A previous section described `STARTUPINFO`, which allows users to launch an +appwith an explicit show command, e.g. `SW_MINIMIZE`. If launched in this way, +like `start /min`, the app should launch Minimized regardless of the last close +position. + +### Virtual Desktops + +Win+Tab and Taskview button on the taskbar allow the user to create +multiple [**Virtual Desktops**](https://support.microsoft.com/en-us/windows/configure-multiple-desktops-in-windows-36f52e38-5b4a-557b-2ff9-e1a60c976434) (sometimes called **Desktops**). These are groups +of windows that the user can switch between. When a window is on a background +virtual desktop, it is cloaked (hidden). + +Apps generally do not need to worry about virtual desktops, including for most +scenarios related to restoring window positions. In a normal app launch, it is +always the case that the window should launch on the current virtual desktop +(even if closed while on a background desktop). + +But, when restarted (automatic app/sytstem update, etc), apps should ideally +'keep' their windows on the same virtual desktops. Consider an app like a +browser that creates many windows, and a user has organized them onto different +virtual desktops. If the system reboots and the app is restarted, it should +restore all window positions, *and* it should move each window to its previous +virtual desktop. + +This is done using the [`IVirtualDesktopManager`](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ivirtualdesktopmanager) APIs, which has these two functions: + - [`GetWindowDesktopId`](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ivirtualdesktopmanager-getwindowdesktopid) + - [`MoveWindowToDesktop`](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ivirtualdesktopmanager-movewindowtodesktop) + +When storing previous window positions, apps should remember the GUID for the +window's virtual desktop. This is used ONLY when the app is restarted, not in a +'normal launch'. This is similar to minimized; if closed while minimized and +launched normally, the app should start restored (or maximized). + +### Restarting after Windows Update with `RegisterApplicationRestart` + +If you search for 'Restart apps after signing in' from the start menu, you'll +find a user setting (which is off by default). If enabled, apps should [relaunch +themselves if opened while the system +reboots](https://blogs.windows.com/windowsdeveloper/2023/07/20/help-users-resume-your-app-seamlessly-after-a-windows-update/). +Your app should call [`RegisterApplicationRestart`](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrestart), which accepts a parameter +that is passed as a command line argument when the system relaunches your app +after a reboot. + +For example: + +```cpp +// Register your app for restart; you can control which types of restart with flags. +PCWSTR restartCmdLine = L"restart"; +DWORD flags = 0; +RegisterApplicationRestart(restartCmdLine, flags); + +// Detect a restart by checking for the argument +const bool isRestart = (wcsstr(cmdLine, restartCmdLine) != nullptr); +``` + +If your app receives this command line argument, you know your app is restarting. You should use this as a signal to reuse previous minimized state and virtual desktops. + +Note that when closing, apps should handle `WM_ENDSESSION` to know when they are +being closed in all cases. This message is sent prior to destroying the window +when the system is shutting down. + +## Example: putting it all together + +We now have the background to correctly relaunch windows in their previous +locations. + +We'll need this stored information to do so correctly: + +* the window's last **normal position** (a `RECT` in screen coordinates) +* potentially its last **Arranged position** (also in screen coordinates) + * If a window is Arranged, it has both a normal rect and an Arranged rect. (If + Max/Min/Normal, the Arranged rect is not set.) +* some information about the window's last monitor + * its **work area** + * its **DPI** + * its **device name** (`szDevice` from `MONITORINFOEX`) + +### Moving stored positions between monitors + +First, we need an algorithm for translating stored positions between monitors. +This is necessary if the stored position is from a monitor that no longer exists +or changed in some way. Given the stored information above and a new monitor +with its own work area & DPI, we can adjust our `RECT`s: + +* **The normal rect** + + 1. Adjust the rect to stay the same logical offset from its work area. + (Calculate the logical offset by calculating the offset in physical pixels + and then scaling by the DPI). + 2. Scale the rect's size from the previous DPI to the new DPI, keeping the same + logical size. + 3. If the rect is now larger than the work area of its new monitor, pick a + different size. Preferably, scale the original size based on the ratio + between the old work area to the new one. + 4. Crop the rect as needed to remain entirely within the work area. + +* **The Arrange rect** + - The Arranged rect should be stored in 'frame bounds' (without invisible + resize borders). The size of the invisible resize borders change when the + DPI changes, and this size does not scale linearly. To ensure Arranged + positions keep windows visibly aligned when their DPI changes while + Arranged, it is necessary to remove the invisible resize borders prior to + storing the rect, and add them back in when Arranging the window, after + the window is already on the monitor it is being Arranged on. + - The Arranged rect always is sized to stay the same relative distance from + each edge of the work area. + +Note that it is not necessary to handle the Maximize/Minimize positions when +moving the other RECTs to a different monitor. These positions are chosen +depending on the monitor a window is on when it is Maximized/Minimized, and apps +can handle messages like `WM_GETMINMAXINFO` to override these default positions. + +### Moving a window to a stored position + +Next, we need an algorithm for moving a window to a desired position on any +monitor. + +Let's go step by step. Given the same stored information plus a **show window +command** (`SW_*`): + +1. **Pick a monitor.** + + To pick the best monitor in all cases, it is recommended to store the device + name (`MONITORINFOEX`'s `szDevice`), in addition to the work area. In cases + like changing the primary monitor, this results in better behavior than + reusing the work area (since a user changing the primary monitor likely does + not think of it as a change in coordinate space). + + So: + + - If a monitor exists with the same name, use that monitor. + + - If that's not available, fall back to `MonitorFromRect` with + `MONITOR_DEFAULTTONEAREST`. + +2. **Adjust the normal rect** (and Arrange rect if Arranged) for this desired + monitor. + + - Each rect (normal/arranged) is transformed in a different way. + + - See previous section. + +3. **Disable painting & animations**, if needed. + + - In some cases, it will be necessary to move the window multiple times. To + avoid flickering, temporarily disable painting and animations. + + - Calling `WM_SETREDRAW` with `false` for `wParam` disables painting for the + window. While disabled, the window's contents on screen will not be + changed (though the window will not be hidden, if it is visible). When + re-enabling painting (`WM_SETREDRAW` `true`) at the end, the window will need + to be explicitly invalidated (repainted) and activated, since both are + skipped while `WM_SETREDRAW` false. + + - `SystemParametersInfo`'s `SPI_SETANIMATION` sets a global user setting for + animating window transitions (Max/Min/Arrange/Restore). If moving the + window multiple times, it is best to temporarily disable animations. This + ensures the window isn't shown animating from a position the user didn't + see the window at. + + ```c++ + DefWindowProc(hwnd, WM_SETREDRAW, 1, 0); + + ANIMATIONINFO animationInfo = { sizeof(ANIMATIONINFO) }; + SystemParametersInfo(SPI_GETANIMATION, 0, &animationInfo, 0); + bool needsAnimationReset = !!animationInfo.iMinAnimate; + if (needsAnimationReset) + { + animationInfo.iMinAnimate = false; + SystemParametersInfo(SPI_SETANIMATION, 0, &animationInfo, 0); + } + + // ... move window multiple times + + DefWindowProc(hwnd, WM_SETREDRAW, 1, 0); + SetActiveWindow(hwnd); + InvalidateRect(hwnd, nullptr, true); + + if (needsAnimationReset) + { + animationInfo.iMinAnimate = true; + SystemParametersInfo(SPI_SETANIMATION, 0, &animationInfo, 0); + } + ``` + +4. **Restore the window**. If a window is Maximized, Minimized, or Arranged, the + window needs to be restored (SW_RESTORE) before it is moved to the desired + normal position. + + - The normal position is defined as the last position the window had before + Maximizing/Minimizing/Arranging. + + - If Maximized and moving to another monitor (but staying Maximized), the + window must be restored first, moved to the other monitor, and then + Maximized. Moving only once would leave the normal position on the + previous monitor (restoring the window would move it to another monitor). + +5. **Move the window to the correct monitor**. If a window is changing monitors, it must be moved to that monitor prior to + maximizing/minimizing. + + - This is especially important when changing DPI, since moving a window + between monitors of different DPIs causes the window to change size + (via WM_DPICHANGED). + + - It is fine to simply SetWindowPos with the desired normal RECT, expecting + that the window may receive a WM_DPICHANGED with a size that is scaled + by some amount (from the window's current DPI to the new monitor's DPI). + The next time the window is moved it will already be at the right DPI and + not have the size scaled. + + - One 'gotcha' with the above is that if moving to a higher DPI, this + 'unwanted WM_DPICHANGED' RECT could potentially move the window onto some + other monitor! (If moving to the bottom right of a high DPI monitor and + we find that another monitor is to the right/below.) To account for this, + scale the temp RECT from the monitor DPI to the window DPI, ensuring that + when the system scales the RECT in the opposite direction, the final size + and position match the stored (and now modified) normal RECT. + +6. Once on the desired monitor, call `SetWindowPlacement` to set the desired + normal position and show state. + +7. If Arranging, the show command with SetWindowPlacement is SW_NORMAL. We need + to make the window arranged and move it to the Arranged position. + (SetWindowPlacement doesn't support Arranged.) + + ```c++ + DefWindowProc(hwnd, WM_NCLBUTTONDBLCLK, HTTOP, 0); + + // The Arranged RECT is stored in frame bounds (no invisible resize + // borders). It has already been 'moved' (fit) to the monitor we're moving + // to, staying aligned with the edges of the work area. + // Now that the window is on the monitor it is moving to, and Normal (not + // Maximized/Minimized), query the size of the invisible resize borders + // and expand the Arranged RECT by that size. + RECT rcWindow, rcFrame; + GetWindowRect(hwnd, &rcWindow); + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rcFrame, sizeof(rcFrame)); + arrangeRect->left -= rcFrame.left - rcWindow.left; + arrangeRect->top -= rcFrame.top - rcWindow.top; + arrangeRect->right += rcWindow.right - rcFrame.right; + arrangeRect->bottom += rcWindow.bottom - rcFrame.botto; + + SetWindowPos(hwnd, + nullptr, + arrangeRect.left, + arrangeRect.top, + arrangeRect.right - arrangeRect.left, + arrangeRect.bottom - arrangeRect.top, + SWP_NOZORDER | SWP_NOACTIVATE); + ``` + +### Remembering Window Positions + +Now we can stitch this all together so that your app will relaunch wherever it +was when last closed: + +1. The app defines some persisted data store, like a registry path. + + ```cpp + PCWSTR appRegKeyName = L"SOFTWARE\\UniqueNameForThisApp"; + PCWSTR lastCloseRegKeyName = L"LastClosePosition"; + ``` + +2. Before creating the window, check if another instance of the app is already + running. + + - Note: [`FindWindow`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-findwindowa) +returns the top window in z-order if multiple other instances are running. + + ```c++ + HWND hwndPrev = FindWindow(wndClassName, nullptr); + ``` + +3. Create the window without `VS_VISIBLE` and with `CW_USEDEFAULT` position and size. + + ```c++ + HWND hwnd = CreateWindowEx( + ... + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + ...); + + if (!hwnd) + { + // Handle failure. + } + ``` + +4. Set the initial position. + + - If there was a previous window position, start with that window's position. + - If that window is Minimized, use the restored position. + - Cascade the position, moving it down/right to keep both windows title bars visible. + - If no previous window, check the last close position. + - If no last close position, set some hard coded logical size reasonable for + the app to use by default. + - If the STARTUPINFO has relevant flags set, adjust the position picked above + as needed. + - Set the window position and show the window. + +3. Call `RegisterApplicationRestart`. + + - Do this after a successful launch. If the app is running while the system + shuts down for updates, it will then be relaunched after the system reboots. + + ```c++ + RegisterApplicationRestart(L"", 0); + ``` + +4. When the window gets `WM_CLOSE`, store the window position in the registry. + + ```c++ + // ...wndproc... + case WM_CLOSE: + PlacementEx::StorePlacementInRegistry( + hwnd, + appRegKeyName, + lastCloseRegKeyName); + break; + ``` + +## More scenarios + +### FullScreen windows + +All previous sections describe 4 possible window states, Max/Min/Arrange/Normal. +In these four states, all required information to capture a window's position is +known to the system (and exposed via APIs like `GetWindowPlacement` and `GetDpiForWindow`). + +FullScreen is another (separate) state: + + - A FullScreen window does not have the `WS_CAPTION` or `WS_THICKFRAME` styles. + (It has no title bar or resize borders). + - A FullScreen window fits the monitor rect, covering the taskbar (if one is + present on the monitor). + +Some uncommon knowledge about FullScreen windows: + + - They are NOT always above other apps (z-order), or covering all monitors. + - They can be Maximized or Minimized. (ex, maximize a browser window then F11 + twice. This enters FullScreen and exits back to Maximized). + - They can change monitors. (ex, unplug a FullScreen window's monitor, or + change the resolution of the monitor a FullScreen window is on). + +The main difference between FullScreen and Maximize is that the system does +not know all of the state for the window (only the app knows the window's restore +from FullScreen position). + +If an app becomes FullScreen, and is FullScreen when it is closed, it should NOT +store the FullScreen position (the one sized to the monitor). Launching to this +size, without being FullScreen (and without knowing the right restore position +upon exiting FullScreen) would lead to unexpected behavior. Instead, apps that +enter FullScreen must remember their position prior to becoming FullScreen. They +can use this as their "restore position" when exiting full screen, and they +should then store this as their "current position" when storing placement . + + ```c++ + case WM_DESTROY: + if (WI_IsFlagSet(placement.flags, PlacementFlags::FullScreen)) + { + placement.MoveToMonitor(hwnd); + } + else + { + PlacementEx::GetPlacement(hwnd, &placement); + } + + placement.StoreInRegistry(registryPath, lastCloseRegKeyName); + break; + ``` + + +### Handling monitor changes + +After an app creates a window, the monitors can change at any time! This means +that screen coordinates that are 'on-screen' (within the bounds of some monitor) +could become off-screen if that monitor is removed, or changes size. + +Most apps care about 2 "monitor" events: + +* `WM_DISPLAYCHANGE`: sent to all top-level windows when monitors change. +* `WM_SETTINGCHANGE` (for `SPI_SETWORKAREA`): sent when work area changes. This is important: if a new docked toolbar appears (e.g. Voice Access), only this event is fired, not `WM_DISPLAYCHANGE`. + +DANGER: If an app moves itself in response to WM_DISPLAYCHANGE, it may end up +off screen or in unexpected places! + + - A user has two monitors connected to a laptop, using two ports on the + laptop. The user unplugs one cable, then immediately unplugs the other. + + - It is possible that the first display change moves the window to the monitor + that is about to be removed. If this happens, the window may be moved twice. + + - It is possible that the window receives the first WM_DISPLAYCHANGE after + being moved once, but after that monitor it was moved to was removed. During + this time, the window moving itself would cause the system to NOT move the + window the second time. + + - The final state of the window becomes hard to define. If the user expects + the system's behavior (like moving the window back to a monitor it was on when + the monitor was unplugged), the window might end up on the wrong monitor. And + if the app only sizes itself, but does not check that it's position is on + screen, this could cause the window to end up off screen. + +It is important to handle failures when using HMONITOR APIs, even when using +MONITOR_DEFAULTTONEAREST. (The handle this call returns will never be null, but +it could become invalid before it is used.) + +```cpp +MONITORINFOEX mi { sizeof(mi) }; +if (!GetMonitorInfo(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), &mi)) +{ + // Return without moving the window. The monitors are changing. + return; +} +``` + +Note: Apps sometimes query the monitors very frequently, which can make these +'very rare' error cases difficult to handle, or expensive to compute. See +[CurrentMonitorTopology.h](CurrentMonitorTopology.h), which defines a cache +for the monitor data that addresses these issues. \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.cpp b/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.cpp new file mode 100644 index 00000000..9aa99b26 --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.cpp @@ -0,0 +1,325 @@ + +#define USE_VIRTUAL_DESKTOP_APIS +#include "../inc/User32Utils.h" +#include "resource.h" + +// A FullScreen window is sized to the monitor and doesn't have a caption +// bar or resize borders (in other words, no WS_CAPTION or WS_THICKFRAME). +// +// When exiting FullScreen, users expect the window to move back to its previous +// position, stored when entering FullScreen. This is similar to Maximize/Minimize, +// except apps must track this restore position manually; the system does not. +// +// This sample demonstrates entering/exiting FullScreen, using PlacementEx to +// implement this memory. PlacementEx remembers the window position (when +// entering) and uses that position to move the window at a later time (exiting). +PlacementEx fsPlacement; + +// The last close position is stored in the registry, using a string that is +// unique to this app. +PCWSTR registryPath = L"SOFTWARE\\Microsoft\\Win32Samples\\FullScreenSample"; +PCWSTR lastCloseRegKeyName = L"LastClosePosition"; + +// Called after creating the window (hidden). This picks a good starting +// position for the window and shows it. +// +// hwndPrev Another instance of this app, opened prior to this +// instance opening. If this is not null, this window will +// launch over the other instance, in the same position but +// cascaded (moved down/right a bit to keep both windows visible). +// +// isRestart If true, this is a restart (not a normal launch). The +// system restarted while this app was running, and we're +// being relaunched. +// This allows the window to launch minimized, or cloaked (on +// a background virtual desktop). +// +void SetInitialPosition(HWND hwnd, HWND hwndPrev, bool isRestart) +{ + PlacementParams pp({ 600, 400 }, registryPath, lastCloseRegKeyName); + + if (isRestart) + { + pp.SetIsRestart(); + } + else if (hwndPrev) + { + pp.SetPrevWindow(hwndPrev); + } + + fsPlacement = pp.PositionAndShow(hwnd); + + // Repaint now that fsPlacement is set (checked for when painting to pick + // background color). + InvalidateRect(hwnd, nullptr, true); +} + +// Called before destroying the window. This stores the current window placement +// in the registry, as the last close position. +// +// If closed while FullScreen, make sure our stored position is on the window's +// current monitor (but do not refresh it). We want to launch next time as +// FullScreen, with the restore position that we stored when entering FullScreen. +void SaveLastClosePosition(HWND hwnd) +{ + if (fsPlacement.IsFullScreen()) + { + fsPlacement.MoveToWindowMonitor(hwnd); + } + else if (!PlacementEx::GetPlacement(hwnd, &fsPlacement)) + { + return; + } + + fsPlacement.StoreInRegistry( + registryPath, + lastCloseRegKeyName); +} + +// Handle keyboard input. +void OnWmChar(HWND hwnd, WPARAM wParam) +{ + switch (wParam) + { + // Enter key toggles FullScreen. + case VK_RETURN: + fsPlacement.ToggleFullScreen(hwnd); + return; + + // Space toggles Maximize. + // This is not done while FullScreen. + case VK_SPACE: + if (!fsPlacement.IsFullScreen()) + { + ShowWindow(hwnd, IsZoomed(hwnd) ? SW_RESTORE : SW_MAXIMIZE); + } + return; + + // M key minimizes the window. + case 0x4D: // M key + case 0x6D: // m key + ShowWindow(hwnd, SW_MINIMIZE); + return; + + // C key moves the cursor to the center of the window. + case 0x43: // C key + case 0x63: // c key + { + RECT rc; + GetWindowRect(hwnd, &rc); + SetCursorPos(rc.left + ((RECTWIDTH(rc)) / 2), rc.top + ((RECTHEIGHT(rc)) / 2)); + return; + } + + // Escape key exits. + case VK_ESCAPE: + DestroyWindow(hwnd); + return; + } +} + +void OnWmPaint(HWND hwnd, HDC hdc) +{ + const COLORREF rgbMaize = RGB(255, 203, 5); + const COLORREF rgbBlue = RGB(0, 39, 76); + static HBRUSH hbrMaize = CreateSolidBrush(rgbMaize); + static HBRUSH hbrBlue = CreateSolidBrush(rgbBlue); + const UINT dpi = GetDpiForWindow(hwnd); + + // Create a font of size 30 (* DPI scale) and store it until DPI changes. + static HFONT hfont = nullptr; + static UINT dpiLast = 0; + if (dpiLast != dpi) + { + if (hfont) + { + DeleteObject(hfont); + } + + hfont = CreateFont(MulDiv(30, dpi, 96), + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + L"Courier New"); + + dpiLast = dpi; + } + SelectObject(hdc, hfont); + + // Background in one color, text in the other. + const bool fFullScreen = fsPlacement.IsFullScreen(); + HBRUSH hbrBackground = fFullScreen ? hbrMaize : hbrBlue; + COLORREF rgbText = fFullScreen ? rgbBlue : rgbMaize; + + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, rgbText); + + RECT rc; + GetClientRect(hwnd, &rc); + + // Draw background + FillRect(hdc, &rc, hbrBackground); + + const UINT nudge = MulDiv(30, dpi, 96); + + // Draw text + + rc.top += (3 * nudge); + rc.left += nudge; + + PCWSTR toggleFullTxt = fFullScreen ? + L"ENTER to exit Fullscreen" : L"ENTER to enter Fullscreen"; + DrawText(hdc, toggleFullTxt, (int)wcslen(toggleFullTxt), &rc, DT_LEFT); + + if (!fFullScreen) + { + rc.top += nudge; + PCWSTR toggleMaxTxt = IsZoomed(hwnd) ? + L"SPACE to restore from Maximize" : L"SPACE to Maximize"; + DrawText(hdc, toggleMaxTxt, (int)wcslen(toggleMaxTxt), &rc, DT_LEFT); + } + + rc.top += nudge; + PCWSTR minTxt = L"M to minimize"; + DrawText(hdc, minTxt, (int)wcslen(minTxt), &rc, DT_LEFT); + + rc.top += nudge; + PCWSTR escTxt = L"ESC to close"; + DrawText(hdc, escTxt, (int)wcslen(escTxt), &rc, DT_LEFT); + + rc.top += nudge; + PCWSTR cTxt = L"C to move cursor to center"; + DrawText(hdc, cTxt, (int)wcslen(cTxt), &rc, DT_LEFT); +} + +LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) + { + case WM_CHAR: + OnWmChar(hwnd, wParam); + break; + + case WM_SYSCOMMAND: + + // If the window is maximized, FullScreen, and someone is trying to + // restore the window (for example, Win+Down hotkey), exit FullScreen. + // (We do NOT want to remain FullScreen but restore from Maximize and + // move to the restore position...) + if ((wParam == SC_RESTORE) && + IsZoomed(hwnd) && + fsPlacement.IsFullScreen()) + { + fsPlacement.ExitFullScreen(hwnd); + return 0; + } + break; + + case WM_PAINT: + { + PAINTSTRUCT ps; + OnWmPaint(hwnd, BeginPaint(hwnd, &ps)); + EndPaint(hwnd, &ps); + break; + } + + case WM_DPICHANGED: + { + RECT* prc = (RECT*)lParam; + + SetWindowPos(hwnd, + nullptr, + prc->left, + prc->top, + prc->right - prc->left, + prc->bottom - prc->top, + SWP_NOZORDER | SWP_NOACTIVATE); + break; + } + + case WM_ENDSESSION: + case WM_DESTROY: + SaveLastClosePosition(hwnd); + PostQuitMessage(0); + break; + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +bool InitWindow(HINSTANCE hInst, bool isRestart) +{ + PCWSTR windowTitle = L"FullScreen Sample"; + PCWSTR wndClassName = L"FullScreenSampleWindow"; + + WNDCLASSEX wc = { sizeof(wc) }; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WndProc; + wc.hInstance = hInst; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.lpszClassName = wndClassName; + wc.hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDC_FULLSCREEN)); + + if (!RegisterClassEx(&wc)) + { + return false; + } + + HWND hwndPrev = FindWindow(wndClassName, nullptr); + + // Create the window with the default position and not visible. + HWND hwnd = CreateWindowEx( + 0, + wndClassName, + windowTitle, + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + nullptr, + nullptr, + hInst, + nullptr); + + if (!hwnd) + { + return false; + } + + SetInitialPosition(hwnd, hwndPrev, isRestart); + + return true; +} + +int wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR cmdLine, int) +{ + // If 'u' in the command line, run as DPI Unaware. Otherwise run as + // Per-Monitor DPI Aware. + SetThreadDpiAwarenessContext((wcsstr(cmdLine, L"u") != nullptr) ? + DPI_AWARENESS_CONTEXT_UNAWARE : + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + // Initialize COM, needed for virtual desktop APIs (USE_VIRTUAL_DESKTOP_APIS). + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + PCWSTR restartCmdLine = L"restart"; + const bool isRestart = (wcsstr(cmdLine, restartCmdLine) != nullptr); + RegisterApplicationRestart(restartCmdLine, 0); + + // Create the main window. + if (!InitWindow(hInst, isRestart)) + { + MessageBox(NULL, + L"Failed to create Main Window.", + L"ERROR", MB_ICONEXCLAMATION | MB_OK); + return 1; + } + + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return 0; +} diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.filters b/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.filters new file mode 100644 index 00000000..388ab2e2 --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.filters @@ -0,0 +1,37 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + + Source Files + + + Source Files + + + + + + + + + + \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.vcxproj b/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.vcxproj new file mode 100644 index 00000000..a534d9bc --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/FullScreenSample.vcxproj @@ -0,0 +1,134 @@ + + + + true + 15.0 + {04a94fc2-e929-4327-9e0a-f70ef4059773} + Win32Proj + FullScreenSample + 10.0.26100.0 + 10.0.17134.0 + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + Application + v143 + v142 + v141 + v140 + Unicode + + + true + true + + + false + true + false + + + + + + + + + + + + + + + + _CONSOLE;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + %(AdditionalOptions) /permissive- /bigobj + ..\inc;%(AdditionalIncludeDirectories) + stdcpp17 + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + Windows + false + gdi32.lib;shcore.lib;dwmapi.lib;advapi32.lib;%(AdditionalDependencies) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + Windows + true + true + false + gdi32.lib;shcore.lib;dwmapi.lib;advapi32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/PropertySheet.props b/Samples/WindowPlacement/cpp/FullScreenSample/PropertySheet.props new file mode 100644 index 00000000..b0c62269 --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/full-screen.ico b/Samples/WindowPlacement/cpp/FullScreenSample/full-screen.ico new file mode 100644 index 00000000..efff82cd Binary files /dev/null and b/Samples/WindowPlacement/cpp/FullScreenSample/full-screen.ico differ diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/packages.config b/Samples/WindowPlacement/cpp/FullScreenSample/packages.config new file mode 100644 index 00000000..bc4698db --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/resource.h b/Samples/WindowPlacement/cpp/FullScreenSample/resource.h new file mode 100644 index 00000000..b66e3f1d --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/resource.h @@ -0,0 +1,2 @@ + +#define IDC_FULLSCREEN 10102 diff --git a/Samples/WindowPlacement/cpp/FullScreenSample/resources.rc b/Samples/WindowPlacement/cpp/FullScreenSample/resources.rc new file mode 100644 index 00000000..aeb00af9 --- /dev/null +++ b/Samples/WindowPlacement/cpp/FullScreenSample/resources.rc @@ -0,0 +1,4 @@ + +#include "resource.h" + +IDC_FULLSCREEN ICON "full-screen.ico" diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/PropertySheet.props b/Samples/WindowPlacement/cpp/SaveRestoreSample/PropertySheet.props new file mode 100644 index 00000000..b0c62269 --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.cpp b/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.cpp new file mode 100644 index 00000000..6a641bf5 --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.cpp @@ -0,0 +1,420 @@ + +#define USE_VIRTUAL_DESKTOP_APIS +#define USE_WINDOW_ACTION_APIS +#include "User32Utils.h" +#include "resource.h" +#include "windowsx.h" + +// +// This sample app uses PlacementEx.h to implement 'remembering positions'. When +// the window closes, its position and other information (a PlacementEx) is +// stored in the registry. When launching, this stored position is used to pick +// an initial position. +// +// This information includes Minimized state and virtual desktop information. +// Typically, this information is unused (the app launches restored/Maximized +// and on the active desktop). But, if the app is restarted, like after a system +// reboot, we use that info to correctly relaunch the window Minimized and on +// its original desktop (if closed in that state). +// + +PCWSTR appRegKeyName = L"SOFTWARE\\Microsoft\\Win32Samples\\SaveRestoreSample"; +PCWSTR lastCloseRegKeyName = L"LastClosePosition"; +PCWSTR wndClassName = L"SaveRestoreSampleWindow"; +constexpr SIZE defaultSize = { 600, 400 }; +constexpr SIZE minSize = { 300, 200 }; + +// Monitor Hint +// +// When launched from UI (like the Taskbar, desktop icons, file explorer, etc), +// apps receive a 'monitor hint', the monitor of the UI the user interacted +// with, in their `STARTUPINFO`. By default, CreateWindow uses this hint to +// place the window, so by default PlacementEx also uses this hint to override +// any stored position (where the window was last closed). This is generally the +// correct behavior: users will see apps on the monitor they interacted with. +// However, some users prefer to hide taskbars on their non-primary displays and +// may be frustrated that apps always relaunch on their primary display (instead +// of where they last closed). +// +// If `true`: consumes the monitor hint & launches on that monitor. +// If `false`: ignores the monitor hint & launches on monitor it was last-closed on. +bool UseMonitorHint = true; +PCWSTR UseMonitorHintKeyName = L"UseMonitorHint"; +RECT rcUseMonitorHintTxt = {}; + +// Allow Partially Off-Screen +// +// Users can move and resize apps to any positions and dimensions, including +// hard-to-recover ones (e.g. extremely large and in the bottom-right corner). +// In most cases, users don't want apps to restart in those positions (e.g. the +// move was accidental or they forgot they did such an absurd move or they +// relaunch to recover a more sensible state). However, rare apps may +// deliberately want to be saved & relaunched partially off-screen. +// +// If `true`: sets the flag `PlacementFlags::AllowPartiallyOffScreen`, which +// allows a window to relaunch partially off-screen (outside the work area). +// If `false`: the relaunch position is moved as needed to be 100% inside the +// work area. +// +// If the new position is more than 50% outside the work area, PlacementEx +// assumes this was accidental and repositions the window anyway. +bool AllowOffScreen = true; +PCWSTR AllowOffScreenKeyName = L"AllowPartiallyOffscreen"; +RECT rcAllowOffScreenTxt = {}; + +// Creates the window, picks an initial position, and shows/activates it. +bool CreateMainWindow(HINSTANCE hInstance, PWSTR cmdLine) +{ + // PlacementParams allows us to pick the initial window position. + // Here we pick the default size (to use the very first time launching on + // a machine), and a registry key to use to store the last close position, + // which is used by default when launching. (The app normally launches + // where it was when it closed.) + PlacementParams pp(defaultSize, appRegKeyName, lastCloseRegKeyName); + + // Look for existing windows with the same class name. + // If one exists (the top/last activated one) the new window will be + // placed above the previous window, shifted down/right a bit to keep the + // previous window's title bar visible (aka 'cascading'). + pp.FindPrevWindow(wndClassName); + + // Create the new window using default (CW_USEDEFAULT) position and size. + // Do not set WS_VISIBLE (keep the window hidden). + HWND hwnd = CreateWindowEx( + 0, + wndClassName, + L"SaveRestore Sample", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + nullptr, + nullptr, + hInstance, + nullptr); + + if (!hwnd) + { + return false; + } + + // Register to be restarted if the system reboots while this app is running. + // Also check if this is a restart (using a command line string). This + // modifies how we initially position the window (for example, if closed + // while Minimized or on a background virtual desktop). + PCWSTR restartCmdLine = L"/restart"; + RegisterApplicationRestart(restartCmdLine, 0); + if (wcsstr(cmdLine, restartCmdLine) != nullptr) + { + pp.SetIsRestart(); + } + + // Turning off UseMonitorHint disables the default StartupInfo::MonitorHint flag. + if (!UseMonitorHint) + { + pp.ClearStartupInfoFlag(StartupInfoFlags::MonitorHint); + } + + // AllowPartiallyOffScreen is enabled by default, but the user setting can + // disable it. + if (AllowOffScreen) + { + pp.SetAllowPartiallyOffscreen(); + } + + // Move the window to the initial position and show it. + pp.PositionAndShow(hwnd); + + return true; +} + +// Called when the window is closing. +// This updates the last close position, stored in the registry. We'll use +// this position by default when launching a new instance. +void SaveLastClosePosition(HWND hwnd) +{ + PlacementEx::StorePlacementInRegistry( + hwnd, + appRegKeyName, + lastCloseRegKeyName); +} + +// Read registry key for MonitorHint user setting. This is called on first +// launch and after changing the settings (clicking text toggles the setting). +void ReadUserSettings() +{ + // UseMonitorHint default is 1 (enabled) + DWORD dw = ReadDwordRegKey(appRegKeyName, UseMonitorHintKeyName, 1); + UseMonitorHint = (dw == 1); + + // AllowOffScreen default is 0 (disabled) + dw = ReadDwordRegKey(appRegKeyName, AllowOffScreenKeyName, 0); + AllowOffScreen = (dw == 1); +} + +// Handle WM_LBUTTONDOWN: Clicking on settings text toggles setting +void OnWmLButtonDown(HWND hwnd, LPARAM lParam) +{ + POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + + if (PtInRect(&rcUseMonitorHintTxt, pt)) + { + // Toggle monitor hint setting + WriteDwordRegKey(appRegKeyName, UseMonitorHintKeyName, !UseMonitorHint); + ReadUserSettings(); + InvalidateRect(hwnd, nullptr, true); + } + + if (PtInRect(&rcAllowOffScreenTxt, pt)) + { + // Toggle allow offscreen setting + WriteDwordRegKey(appRegKeyName, AllowOffScreenKeyName, !AllowOffScreen); + ReadUserSettings(); + InvalidateRect(hwnd, nullptr, true); + } + +} + +// Handle WM_GETMINMAXINFO: Enforce a minimum logical size +void OnWmGetMinMaxInfo(HWND hwnd, LPARAM lParam) +{ + MINMAXINFO* pmmi = reinterpret_cast(lParam); + UINT dpi = GetDpiForWindow(hwnd); + pmmi->ptMinTrackSize.x = MulDiv(minSize.cx, dpi, 96); + pmmi->ptMinTrackSize.y = MulDiv(minSize.cy, dpi, 96); +} + +// Handle WM_GETMINMAXINFO: +// - escape closes the window +// - 1 key sizes window to 500x500 +void OnWmChar(HWND hwnd, WPARAM wParam) +{ + switch(wParam) + { + case VK_ESCAPE: + SendMessage(hwnd, WM_CLOSE, 0, 0); + return; + + case 0x31: /* VK_1 */ + { + PlacementEx pex; + if (PlacementEx::GetPlacement(hwnd, &pex)) + { + pex.showCmd = SW_SHOWNORMAL; + WI_ClearFlag(pex.flags, PlacementFlags::Arranged); + pex.SetLogicalSize(defaultSize); + PlacementEx::SetPlacement(hwnd, &pex); + } + return; + } + } +} + +void OnWmPaint(HWND hwnd, HDC hdc) +{ + const COLORREF rgbMaize = RGB(255, 203, 5); + const COLORREF rgbBlue = RGB(0, 39, 76); + const COLORREF rgbRed = RGB(122, 18, 28); + static HBRUSH hbrMaize = CreateSolidBrush(rgbMaize); + static HBRUSH hbrBlue = CreateSolidBrush(rgbBlue); + static HBRUSH hbrRed = CreateSolidBrush(rgbRed); + const UINT dpi = GetDpiForWindow(hwnd); + const UINT textSize = 25; + static HFONT hfont = nullptr; + static UINT dpiLast = 0; + + // Create a font and store it until the window changes DPI (scale). + if (dpiLast != dpi) + { + if (hfont) + { + DeleteObject(hfont); + } + + hfont = CreateFont(MulDiv(textSize, dpi, 96), + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + L"Courier New"); + + dpiLast = dpi; + } + + SetBkMode(hdc, TRANSPARENT); + SelectObject(hdc, hfont); + + // If maximized - Blue (yellow text) + // If arranged - Red (yellow text) + // Restored - Yellow (blue text) + COLORREF rgbText; + HBRUSH hbrBackground; + if (IsZoomed(hwnd)) + { + rgbText = rgbMaize; + hbrBackground = hbrBlue; + } + else if (IsWindowArranged(hwnd)) + { + rgbText = rgbMaize; + hbrBackground = hbrRed; + } + else + { + rgbText = rgbBlue; + hbrBackground = hbrMaize; + } + SetTextColor(hdc, rgbText); + + RECT rc; + GetClientRect(hwnd, &rc); + FillRect(hdc, &rc, hbrBackground); + UINT nudge = MulDiv(10, dpi, 96); + InflateRect(&rc, -1 * nudge, -1 * nudge); + RECT rcTxt = rc; + + const UINT lineHeight = nudge + MulDiv(textSize, dpi, 96); + + // If Maximized or Arranged + if (IsZoomed(hwnd) || IsWindowArranged(hwnd)) + { + PCWSTR maxTxt = IsZoomed(hwnd) ? L"Maximized" : L"Arranged"; + DrawText(hdc, maxTxt, (int)wcslen(maxTxt), &rc, DT_LEFT); + rc.top += lineHeight; + } + + RECT rcWindow; + GetWindowRect(hwnd, &rcWindow); + + // Current window size, then rect. + std::wstring sizeStr = wil::str_printf(L"(%d x %d)", + RECTWIDTH(rcWindow), RECTHEIGHT(rcWindow)); + DrawText(hdc, sizeStr.c_str(), (int)wcslen(sizeStr.c_str()), &rc, DT_LEFT); + rc.top += lineHeight; + + std::wstring rectStr = wil::str_printf(L"[%d, %d, %d, %d]", + rcWindow.left, rcWindow.top, rcWindow.right, rcWindow.bottom); + DrawText(hdc, rectStr.c_str(), (int)wcslen(rectStr.c_str()), &rc, DT_LEFT); + + // Reset the rect to the full window (with nudge) and move to the bottom + // row (the user settings). + rc = rcTxt; + rc.top = rc.bottom - lineHeight; + + // UseMonitorHint -> Launch Monitor: Last/Best (where 'use hint' == best) + std::wstring lastMonSettingTxt = wil::str_printf( + L"Launch Monitor: %ws", UseMonitorHint ? L"Best" : L"Last"); + DrawText(hdc, lastMonSettingTxt.c_str(), (int)wcslen(lastMonSettingTxt.c_str()), + &rc, DT_SINGLELINE | DT_LEFT | DT_BOTTOM); + + // Remember the rect for the monitor hint rect; clicking toggles the setting. + rcUseMonitorHintTxt = rc; + + // AllowOffScreen. If no, this sets PlacementFlags::AllowPartiallyOffScreen. + rc.top -= lineHeight; + rc.bottom -= lineHeight; + std::wstring offscreenSettingTxt = wil::str_printf( + L"Allow OffScreen: %ws", AllowOffScreen ? L"Yes" : L"No"); + DrawText(hdc, offscreenSettingTxt.c_str(), (int)wcslen(offscreenSettingTxt.c_str()), + &rc, DT_SINGLELINE | DT_LEFT | DT_BOTTOM); + + // Remember the rect for the offscreen text; clicking toggles the setting. + rcAllowOffScreenTxt = rc; +} + +LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) + { + case WM_PAINT: + { + PAINTSTRUCT ps; + OnWmPaint(hwnd, BeginPaint(hwnd, &ps)); + EndPaint(hwnd, &ps); + break; + } + + case WM_WINDOWPOSCHANGED: + { + // Repaint when the window rect changes + auto pwp = reinterpret_cast(lParam); + RECT rc = { pwp->x, pwp->y, pwp->x + pwp->cx, pwp->y + pwp->cy }; + static RECT rcWindowLast = {}; + if (!EqualRect(&rc, &rcWindowLast)) + { + rcWindowLast = rc; + InvalidateRect(hwnd, nullptr, true); + } + break; + } + + case WM_LBUTTONDOWN: + OnWmLButtonDown(hwnd, lParam); + break; + + case WM_GETMINMAXINFO: + OnWmGetMinMaxInfo(hwnd, lParam); + break; + + case WM_DPICHANGED: + { + RECT* prc = reinterpret_cast(lParam); + SetWindowPos(hwnd, + nullptr, + prc->left, + prc->top, + RECTWIDTH(*prc), + RECTHEIGHT(*prc), + SWP_NOZORDER | SWP_NOACTIVATE); + break; + } + + case WM_CHAR: + OnWmChar(hwnd, wParam); + break; + + case WM_DESTROY: + SaveLastClosePosition(hwnd); + PostQuitMessage(0); + break; + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +int wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR cmdLine, int) +{ + // Run as Per-Monitor DPI Aware, or Unaware if 'u' in the command line. + SetThreadDpiAwarenessContext( + (wcsstr(cmdLine, L"u") != nullptr) ? + DPI_AWARENESS_CONTEXT_UNAWARE : + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + // Initialize COM, needed for virtual desktop APIs (USE_VIRTUAL_DESKTOP_APIS). + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + // Read settings stored in registry. + ReadUserSettings(); + + WNDCLASSEX wc = { sizeof(WNDCLASSEX) }; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.hInstance = hInstance; + wc.lpfnWndProc = WndProc; + wc.lpszClassName = wndClassName; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDC_SAVE)); + + // Create the window and show it. + if (!RegisterClassEx(&wc) || !CreateMainWindow(hInstance, cmdLine)) + { + return 1; + } + + // Pump messages until exit. + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return 0; +} diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.filters b/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.filters new file mode 100644 index 00000000..388ab2e2 --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.filters @@ -0,0 +1,37 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + + Source Files + + + Source Files + + + + + + + + + + \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.vcxproj b/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.vcxproj new file mode 100644 index 00000000..4c45a120 --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/SaveRestoreSample.vcxproj @@ -0,0 +1,134 @@ + + + + true + 15.0 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3} + Win32Proj + SaveRestoreSample + 10.0.26100.0 + 10.0.17134.0 + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + Application + v143 + v142 + v141 + v140 + Unicode + + + true + true + + + false + true + false + + + + + + + + + + + + + + + + _CONSOLE;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + %(AdditionalOptions) /permissive- /bigobj + ..\inc;%(AdditionalIncludeDirectories) + stdcpp17 + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + Windows + false + gdi32.lib;shcore.lib;dwmapi.lib;advapi32.lib;%(AdditionalDependencies) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + Windows + true + true + false + gdi32.lib;shcore.lib;dwmapi.lib;advapi32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/packages.config b/Samples/WindowPlacement/cpp/SaveRestoreSample/packages.config new file mode 100644 index 00000000..bc4698db --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/resource.h b/Samples/WindowPlacement/cpp/SaveRestoreSample/resource.h new file mode 100644 index 00000000..7361a484 --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/resource.h @@ -0,0 +1,2 @@ + +#define IDC_SAVE 10101 diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/resources.rc b/Samples/WindowPlacement/cpp/SaveRestoreSample/resources.rc new file mode 100644 index 00000000..08f5aacf --- /dev/null +++ b/Samples/WindowPlacement/cpp/SaveRestoreSample/resources.rc @@ -0,0 +1,4 @@ + +#include "resource.h" + +IDC_SAVE ICON "save.ico" diff --git a/Samples/WindowPlacement/cpp/SaveRestoreSample/save.ico b/Samples/WindowPlacement/cpp/SaveRestoreSample/save.ico new file mode 100644 index 00000000..e538112d Binary files /dev/null and b/Samples/WindowPlacement/cpp/SaveRestoreSample/save.ico differ diff --git a/Samples/WindowPlacement/cpp/WindowPlacement.sln b/Samples/WindowPlacement/cpp/WindowPlacement.sln new file mode 100644 index 00000000..1749f56a --- /dev/null +++ b/Samples/WindowPlacement/cpp/WindowPlacement.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36408.4 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FullScreenSample", "FullScreenSample\FullScreenSample.vcxproj", "{04A94FC2-E929-4327-9E0A-F70EF4059773}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SaveRestoreSample", "SaveRestoreSample\SaveRestoreSample.vcxproj", "{4654392F-E2E8-4B21-960B-1CD9E88E77E3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Debug|x64.ActiveCfg = Debug|x64 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Debug|x64.Build.0 = Debug|x64 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Debug|x86.ActiveCfg = Debug|Win32 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Debug|x86.Build.0 = Debug|Win32 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Release|x64.ActiveCfg = Release|x64 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Release|x64.Build.0 = Release|x64 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Release|x86.ActiveCfg = Release|Win32 + {04A94FC2-E929-4327-9E0A-F70EF4059773}.Release|x86.Build.0 = Release|Win32 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Debug|x64.ActiveCfg = Debug|x64 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Debug|x64.Build.0 = Debug|x64 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Debug|x86.ActiveCfg = Debug|Win32 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Debug|x86.Build.0 = Debug|Win32 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Release|x64.ActiveCfg = Release|x64 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Release|x64.Build.0 = Release|x64 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Release|x86.ActiveCfg = Release|Win32 + {4654392F-E2E8-4B21-960B-1CD9E88E77E3}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {886355CC-4702-425E-958D-3163266C6EA0} + EndGlobalSection +EndGlobal diff --git a/Samples/WindowPlacement/cpp/inc/CurrentMonitorTopology.h b/Samples/WindowPlacement/cpp/inc/CurrentMonitorTopology.h new file mode 100644 index 00000000..2ca10980 --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/CurrentMonitorTopology.h @@ -0,0 +1,726 @@ +#pragma once +// Note: Intended to be included by User32Utils.h. + +// +// CurrentMonitorTopology +// +// This class caches the data about all of the connected monitors. It contains +// helpers that can be used to get data about the monitors, or find the data +// for a monitor given a point/rect/window, etc. +// +// IMPORTANT: This requires the thread is pumping messages (Get/PeekMessage). +// The monitor list changes when a window created by this class receives a +// WM_DISPLAYCHANGE or WM_SETTINGCHANGE/SPI_SETWORKAREA message. +// +// There are two benefits of using this class, as opposed to calling OS APIs +// (MonitorFromRect, GetMonitorInfo, etc) whenever the data is needed. +// +// 1. Performance +// +// This class caches all monitor info. The OS APIs read the real monitor +// data, which requires taking a global system lock. Calling these APIs +// frequently enough can cause this app and the whole system to slow down. +// +// 2. Consistency +// +// The real monitor list (the HMONITORs) can change at any time, including +// between calls to MonitorFromPoint and GetMonitorInfo. +// Using this class provides a consistent view of the monitor topology. +// It will never change while this thread is processing another message +// + +// A SharedMonitorPtr is a referenced counted pointer to data about a monitor. +// This data is for a moment in time (potentially in the past). +// It is safe to use this pointer in the future, because it is a shared_ptr, +// but the current monitor topology may have changed. +// See MonitorData.h for MonitorData definition. +typedef std::shared_ptr SharedMonitorPtr; + +// TODO: Split out 'slim' version, which doesn't create a window and requires +// the user to call Update() on WM_DISPLAYCHANGE/WM_SETTINGCHANGE. + +class CurrentMonitorTopology +{ +public: + // Returns the number of monitors. + UINT NumMonitors() const + { + return m_numMonitors; + } + + // Returns the primary monitor (the one whose origin is 0, 0). + SharedMonitorPtr + GetPrimaryMonitor() + { + return MonitorFromPoint( {0, 0} ); + } + + // The topology ID is a unique ID for the monitor topology. + // + // This can be used to see if any changes have been made since some point + // in the past. + // + // Note: In GE_RELEASE+, this uses a new system API, GetCurrentMonitorTopologyId. + // On releases that have the API, this topology ID is global (shared by all + // apps in the user session). On earlier releases, this class implements a + // local counter whenever the monitor topology is refreshed. + UINT GetTopologyId() const + { + return m_lastTopologyId; + } + + // + // ForEachMonitor + // Allows the caller to do something for each monitor. + // The provided function takes a MonitorData and returns true if the walk + // should continue. + // + typedef std::function ForEachMonitorFn; + void ForEachMonitor(ForEachMonitorFn fn); + + // + // MonitorFromX functions. + // + // MonitorFromPoint + // MonitorFromRect + // MonitorFromWindow + // MonitorFromPastMonitor + // + // These functions default to 'nearest'. This means that if the point/rect + // is not on any monitor, the function falls back to the nearest monitor. + // Each function also accepts 'null' and 'primary' as fallback options. + // + + enum class Fallback + { + Null = 0, // MONITOR_DEFAULTTONULL + Primary = 1, // MONITOR_DEFAULTTOPRIMARY + Nearest = 2, // MONITOR_DEFAULTTONEAREST + }; + + SharedMonitorPtr + MonitorFromPoint( + POINT pt, + Fallback fallback = Fallback::Nearest); + + SharedMonitorPtr + MonitorFromRect( + const RECT& rc, + Fallback fallback = Fallback::Nearest); + + // A window's monitor is defined as the monitor it's RECT is mostly on. + // (If a window is not overlapping any monitor, the fallback determines if + // this returns null/primary/nearest.) + // + // IMPORTANT: This could be different from the monitor that the window is + // scaling to (for DPI). For all scaling purposes, use GetDpiForWindow, + // NOT the DPI of the MonitorFromWindow. + SharedMonitorPtr + MonitorFromWindow( + HWND hwnd, + Fallback fallback = Fallback::Nearest) + { + RECT rc; + GetWindowRect(hwnd, &rc); + return MonitorFromRect(rc, fallback); + } + + SharedMonitorPtr + MonitorFromName(PCWSTR deviceName); + + // Given a monitor from the past (copied from the current monitor topology + // at some point in the past), this finds a monitor in the current topology + // that best matches the past monitor. + SharedMonitorPtr + MonitorFromPastMonitor( + const MonitorData& previousMonitor, + Fallback fallback = Fallback::Nearest); + + // + // Topology change notification. + // + // A window can register to be notified when the topology changes. It + // provides its window handle and a message ID, and when the topology + // data is changed the window is sent this message, allowing it to respond + // to the updated topology data. + // + + void RegisterTopologyChangeMessage(HWND hwndNotify, UINT msgId); + + // + // Constructor and destructor. + // + + CurrentMonitorTopology() + { + m_validDpiAwarenesses[GetThreadDpiAwareness()] = true; + + RefreshMonitorData(false /* force */); + + m_hwndListener = CreateListenerWindow(); + } + + ~CurrentMonitorTopology() + { + DestroyWindow(m_hwndListener); + } + +private: + // Note: This function attempts to dynamically load (GetProcAddress) a + // recently-added API: + // + // user32!GetCurrentMonitorTopologyId + // + // This API returns a non-zero unique ID for the monitor topology. If any + // of the monitors change, this ID is updated. If the API is not available, + // this returns 0. + // + // Note: this API was added to user32 in GE_RELEASE but is not present in + // header files, so it must be dynamically loaded. + // https://learn.microsoft.com/en-us/windows/win32/winmsg/winuser/nf-winuser-getcurrentmonitortopologyid + static UINT GetCurrentMonitorTopologyId(); + + // Called for each monitor when syncing the monitor data with the current + // monitor topology. This queries for some additional data, stores the data + // in a MonitorData, and adds it to the list. + static BOOL + MonitorEnumProc(HMONITOR handle, HDC, PRECT prcMonitor, LPARAM pThisPtr); + + // Called when the object is created and whenever the listener window gets + // a WM_DISPLAYCHANGE or WM_SETTINGCHANGE/SPI_SETWORKAREA. + // This refreshes all monitor data to reflect the current monitor topology. + void RefreshMonitorData(bool force = false); + + // Called after refreshing the monitor data. + // Notifies all windows that have registered for topology change notifications. + void NotifyWindowsForTopologyChange(); + + // + // The listener window is a top level (but never visible) window used to + // listen for WM_DISPLAYCHANGE and WM_SETTINGCHANGE/SPI_SETWORKAREA. + // These messages are broadcast to all top level windows when the monitors + // change. + // + + static LRESULT CALLBACK + ListenerWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); + + HWND CreateListenerWindow(); + + // + // The monitor topology data + // + + UINT m_lastTopologyId = 0; + UINT m_numMonitors = 0; + + // There are three monitor lists, one for each DPI awareness. If the + // monitors have >100% DPI, the values (coordinates) for each monitor are + // different depending on the thread's awareness. + // This class uses the data queried at the same awareness as the thread. + // This allows a thread to switch between awarenesses, + // SetThreadDpiAwarenessContext, and still see correct information (the + // same data that would be returned by MonitorFromRect and GetMonitorInfo). + bool m_validDpiAwarenesses[NUM_DPI_AWARENESS] = {}; + std::vector m_monitorLists[NUM_DPI_AWARENESS]; + + // The listener window. + HWND m_hwndListener = nullptr; + + // Windows that have requested topology change notifications. + // Used by RegisterTopologyChangeMessage. + struct NotifyChangeRegistration + { + HWND hwndNotify; + UINT msgId; + }; + std::vector m_notifyChangeRegistrations; +}; + +inline void +CurrentMonitorTopology::RegisterTopologyChangeMessage( + HWND hwndNotify, + UINT msgId) +{ + NotifyChangeRegistration info{}; + info.hwndNotify = hwndNotify; + info.msgId = msgId; + m_notifyChangeRegistrations.push_back(info); +} + +inline void +CurrentMonitorTopology::NotifyWindowsForTopologyChange() +{ + for (const auto& registration : m_notifyChangeRegistrations) + { + SendMessage(registration.hwndNotify, registration.msgId, 0, 0); + } +} + +inline void +CurrentMonitorTopology::ForEachMonitor(ForEachMonitorFn fn) +{ + DPI_AWARENESS awareness = GetThreadDpiAwareness(); + + // If the current thread is running at an awareness that we haven't + // seen before, make this awareness valid and refresh the monitor data + // with 'force' parameter. This forces us to reload the monitor data + // at this awareness. + if (!m_validDpiAwarenesses[awareness]) + { + m_validDpiAwarenesses[awareness] = true; + + RefreshMonitorData(true /* force */); + } + + // Call the function for each monitor in the list. + for (const auto& monitor : m_monitorLists[awareness]) + { + if (!fn(monitor)) + { + break; + } + } +} + +/* static */ +inline BOOL +CurrentMonitorTopology::MonitorEnumProc( + HMONITOR handle, + HDC, + PRECT, + LPARAM pMonListPtr) +{ + // Get the data for this monitor. + // This can fail (HMONITORs can become invalid at any time). + MonitorData monData; + if (!MonitorData::FromHandle(handle, &monData)) + { + return false; + } + + // Allocate a SharedMonitorPtr (std::shared_ptr). + SharedMonitorPtr monitorData = SharedMonitorPtr(new MonitorData(monData)); + if (!monitorData) + { + return false; + } + + // Add the shared monitor pointer to the list provided by the caller. + auto pMonitorList = reinterpret_cast*>(pMonListPtr); + pMonitorList->push_back(monitorData); + + return true; +} + +inline void +CurrentMonitorTopology::RefreshMonitorData(bool force) +{ + // There is a new API GetCurrentMonitorTopologyId, added in GE_RELEASE. + // + // We use this API if it is available to know the current topology ID. + // This makes this ID global (shared with all apps). + // + // Using the system's ID also allows us to skip unneeded work when we get + // several WM_DISPLAYCHANGE in a row. It's likely we receive the first one + // after all changes to the monitors have been made, so we can throw out + // subsequent messages by comparing the topology ID and finding it hasn't + // changed. + const UINT topologyId = GetCurrentMonitorTopologyId(); + if (topologyId) + { + // If called with force, we're being called because the thread changed + // its DPI awareness, requiring that we rebuild the monitor lists + // because the number of awarenesses we're maintaining has changed, not + // because we think the monitors have changed. + if (!force && (m_lastTopologyId == topologyId)) + { + return; + } + m_lastTopologyId = topologyId; + } + else + { + // Fallback (topology ID API not available): Increment the ID every + // time we receive a display change or setting change. + // This ID could be used by someone using this class in the same way + // that we use the API when it is available (to know if anything about + // the monitors may have changed between two points in time). + m_lastTopologyId++; + } + + bool succeeded = true; + + m_numMonitors = GetSystemMetrics(SM_CMONITORS); + + DPI_AWARENESS_CONTEXT dpiPrev = GetThreadDpiAwarenessContext(); + for (UINT i = 0; i < NUM_DPI_AWARENESS; i++) + { + // We only maintain the list for this awareness if the awareness is + // valid (if we've seen the thread running at this awareness before). + if (!m_validDpiAwarenesses[i]) + { + continue; + } + + // Switch the thread to this awareness. + SetThreadDpiAwarenessContext( + (i == 0) ? DPI_AWARENESS_CONTEXT_UNAWARE : + (i == 1) ? DPI_AWARENESS_CONTEXT_SYSTEM_AWARE : + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + // TODO: Build new data in a temp list and swap it with the real list + // iff successful? (avoiding ever leaving the state invalid, without + // any monitors) + + // Delete the previous monitors in the list. + m_monitorLists[i].clear(); + + // Build the monitor list. + if (!EnumDisplayMonitors(nullptr, nullptr, + MonitorEnumProc, reinterpret_cast(&m_monitorLists[i]))) + { + succeeded = false; + } + } + SetThreadDpiAwarenessContext(dpiPrev); + + // If we succeeded but the topology ID has changed since we started, the + // data we've gathered is invalid. Don't treat this as a success. We know + // in this case (and other failures) that it was caused by a mode change + // that we haven't yet been notified for (and hopefully the next time we + // try will be very soon and will succeed). + if (succeeded && + topologyId && + (topologyId != GetCurrentMonitorTopologyId())) + { + succeeded = false; + } + + // Iff the operation succeeded, notify anyone that has registered for + // topology change messages. + if (succeeded) + { + NotifyWindowsForTopologyChange(); + } +} + +inline SharedMonitorPtr +CurrentMonitorTopology::MonitorFromPoint( + POINT pt, + Fallback fallback) +{ + SharedMonitorPtr candidate = nullptr; + double nearestDistance = 0; + + ForEachMonitor([&](SharedMonitorPtr monitor) + { + RECT rc = monitor->monitorRect; + + // If the point is on this monitor, pick it. + if (PtInRect(&rc, pt)) + { + candidate = monitor; + + // Don't continue walking the monitor list. + return false; + } + + // If fallback is nearest, keep track of the nearest monitor. + if (fallback == Fallback::Nearest) + { + // Measure the distance between the center of each monitor and the + // point. + // + // Note: This is an approximation. A very wide monitor and very + // small monitor that are both close to a point would prefer the + // smaller monitor (because its center is closer to the point, + // even if the wider monitor's edge is really closer). + // + // See the MonitorFromPoint implementation (internal): + // MONITORFROMPOINTALGORITHM, onecoreuap/windows/core/ntuser/rtl/mmrtl.cxx + // https://microsoft.visualstudio.com/DefaultCollection/OS/_git/0d54b6ef-7283-444f-847a-343728d58a4d?path=%2fonecoreuap%2fwindows%2fcore%2fntuser%2frtl%2fmmrtl.cxx&version=GBofficial/ge_current_directadept_hip1&line=126&lineEnd=126&lineStartColumn=8&lineEndColumn=34&lineStyle=plain + + POINT ptCenter = { + rc.left + (RECTWIDTH(rc) / 2), + rc.top + (RECTHEIGHT(rc) / 2) }; + + double distanceSquared = + std::pow(pt.x - ptCenter.x, 2) + std::pow(pt.y - ptCenter.y, 2); + + if (!candidate || (nearestDistance > distanceSquared)) + { + candidate = monitor; + nearestDistance = distanceSquared; + } + } + + // Keep walking the monitor list. + return true; + }); + + if (candidate) + { + return candidate; + } + + switch (fallback) + { + case Fallback::Nearest: + // Assert(false) + // We're guarenteed >1 monitors and we should have picked some + // monitor as the nearest (candidate). + + case Fallback::Primary: + return GetPrimaryMonitor(); + + default: /* Fallback::Null */ + return nullptr; + } +} + +inline SharedMonitorPtr +CurrentMonitorTopology::MonitorFromRect( + const RECT& rc, + Fallback fallback) +{ + SharedMonitorPtr candidate = nullptr; + UINT candidateArea = 0; + const UINT rectArea = RECTWIDTH(rc) * RECTHEIGHT(rc); + + // Intersect the rect with each monitor rect, looking for the monitor + // that intersects with the rect the most. + ForEachMonitor([&](SharedMonitorPtr monitor) + { + RECT rcI; + if (IntersectRect(&rcI, &monitor->monitorRect, &rc)) + { + UINT area = RECTWIDTH(rcI) * RECTHEIGHT(rcI); + + // If the rect is entirely on this monitor, set the result and + // stop the search. + if (area == rectArea) + { + candidate = monitor; + return false; + } + + // If this monitor intersects with the rect more than the current + // candidate, set this monitor as the candidate (but continue + // walking the monitor list). + if (area > candidateArea) + { + candidate = monitor; + candidateArea = area; + } + } + + // Keep walking the monitor list. + return true; + }); + + if (candidate) + { + return candidate; + } + + switch (fallback) + { + case Fallback::Nearest: + // Return the monitor that is closest to the center of the rect. + // + // TODO: This is an approximation. It may not be the best (closest) + // monitor to the point. We could consider improving this. + // + // See MonitorFromRect implementation (internal): + // MONITORFROMRECTALGORITHM, onecoreuap/windows/core/ntuser/rtl/mmrtl.cxx + // https://microsoft.visualstudio.com/DefaultCollection/OS/_git/0d54b6ef-7283-444f-847a-343728d58a4d?path=%2fonecoreuap%2fwindows%2fcore%2fntuser%2frtl%2fmmrtl.cxx&version=GBofficial/ge_current_directadept_hip1&line=371&lineEnd=371&lineStartColumn=8&lineEndColumn=33&lineStyle=plain + return MonitorFromPoint({ + rc.left + (RECTWIDTH(rc) / 2), + rc.top + (RECTHEIGHT(rc) / 2) + }); + + case Fallback::Primary: + return GetPrimaryMonitor(); + + default: /* Fallback::Null */ + return nullptr; + } +} + +inline SharedMonitorPtr +CurrentMonitorTopology::MonitorFromName(PCWSTR deviceName) +{ + SharedMonitorPtr result = nullptr; + + ForEachMonitor([&](SharedMonitorPtr monitor) + { + if (monitor->MatchesDeviceName(deviceName)) + { + result = monitor; + return false; + } + + // Keep walking the monitor list. + return true; + }); + + return result; +} + +inline SharedMonitorPtr +CurrentMonitorTopology::MonitorFromPastMonitor( + const MonitorData& previousMonitor, + Fallback fallback) +{ + // If any monitor has a matching display name, use that monitor. + // Consider the case where the primary monitor changes. One monitor's + // handle, rect, etc, may have changed, but still be around. The most + // stable thing we know about the monitor is the device name string. + + SharedMonitorPtr result = MonitorFromName(previousMonitor.deviceName); + + if (result) + { + return result; + } + + switch (fallback) + { + case Fallback::Nearest: + // Return the monitor nearest to the previous monitor rect. + return MonitorFromRect(previousMonitor.monitorRect, fallback); + + case Fallback::Primary: + return GetPrimaryMonitor(); + + default: /* Fallback::Null */ + return nullptr; + } +} + +/* static */ +inline UINT +CurrentMonitorTopology::GetCurrentMonitorTopologyId() +{ + using pfnType = UINT(*)(); + static pfnType pfnGetTopologyId = nullptr; + static bool loadedApi = false; + + // This public API is not in header files; load it dynamically. + // https://learn.microsoft.com/en-us/windows/win32/winmsg/winuser/nf-winuser-getcurrentmonitortopologyid + if (!loadedApi) + { + loadedApi = true; + HMODULE hmod = LoadLibraryA("user32.dll"); + if (hmod) + { + pfnGetTopologyId = reinterpret_cast( + GetProcAddress(hmod, "GetCurrentMonitorTopologyId")); + } + } + + if (pfnGetTopologyId) + { + return pfnGetTopologyId(); + } + + return 0; +} + +/* static */ +inline LRESULT CALLBACK +CurrentMonitorTopology::ListenerWndProc( + HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam) +{ + switch (msg) + { + case WM_CREATE: + { + // Store the instance pointer (CreateWindow params) in the window. + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast( + reinterpret_cast(lParam)->lpCreateParams)); + break; + } + + // Most changes to the monitors will broadcast WM_DISPLAYCHANGE. + // But, apps (or the taskbar) can change the work area directly, which + // isn't considered a display change. To respond to these changes as + // well, also listen for WM_SETTINGCHANGE/SPI_SETWORKAREA. + case WM_SETTINGCHANGE: + if (wParam != SPI_SETWORKAREA) + { + break; + } + case WM_DISPLAYCHANGE: + { + // Get the instance pointer, stored on the window. + CurrentMonitorTopology* instance = + reinterpret_cast( + GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + + // Refresh the monitor data. + if (instance) + { + instance->RefreshMonitorData(); + } + + break; + } + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +inline HWND +CurrentMonitorTopology::CreateListenerWindow() +{ + PCWSTR windowTitle = L"TrackMonitorsListenerWindow"; + PCWSTR wndClassName = L"TrackMonitorsListenerWindowClass"; + HINSTANCE hInst = GetModuleHandle(NULL); + + static bool registered = false; + if (!registered) + { + WNDCLASSEX wc = { sizeof(WNDCLASSEX) }; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = ListenerWndProc; + wc.hInstance = hInst; + wc.lpszClassName = wndClassName; + + if (!RegisterClassEx(&wc)) + { + return nullptr; + } + + registered = true; + } + + // The listener window is mostly blank (no styles, position, etc), + // but it has the topmost flag, WS_EX_TOPMOST. This moves the window + // to the top, in Z-order (it is above other non-topmost windows). + // + // Broadcasted messages are received in Z-order, windows on top first. + // If another window on this thread is listening for the same messages + // and queries the monitor data from those messages, we want this + // window to receive these messages first. Otherwise, the other window + // could be left with stale monitor topology info. + // + // Note: RegisterTopologyChangeMessage allows a window to register for + // updates from this class. Using that is strictly better than listening + // to the broadcasted messages (but we make ourselves topmost anyway to + // hopefully account for that usage). + + return CreateWindowEx(WS_EX_TOPMOST, + wndClassName, + windowTitle, + 0, + 0, 0, 0, 0, + nullptr, + nullptr, + hInst, + reinterpret_cast(this)); +} diff --git a/Samples/WindowPlacement/cpp/inc/MiscUser32.h b/Samples/WindowPlacement/cpp/inc/MiscUser32.h new file mode 100644 index 00000000..6b01febc --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/MiscUser32.h @@ -0,0 +1,209 @@ +#pragma once +// Note: Intended to be included by User32Utils.h. + +inline LONG RECTWIDTH(const RECT& rc) +{ + return (rc.right - rc.left); +} + +inline LONG RECTHEIGHT(const RECT& rc) +{ + return (rc.bottom - rc.top); +} + +constexpr UINT NUM_DPI_AWARENESS = 3; + +// Returns true if the window is a top-level window (parented to the desktop window). +inline bool IsTopLevel(HWND hwnd) +{ + return (GetAncestor(hwnd, GA_PARENT) == GetDesktopWindow()); +} + +// Returns the DPI the thread is virtualized to, or 0 if the thread is not +// virtualized (Per-Monitor DPI Aware). +inline UINT GetThreadVirtualizedDpi() +{ + return GetDpiFromDpiAwarenessContext(GetThreadDpiAwarenessContext()); +} + +// Returns one of: +// - DPI_AWARENESS_UNAWARE +// Virtualized to 100% DPI +// - DPI_AWARENESS_SYSTEM_AWARE +// Virtualized to an arbitrary DPI, the DPI of the primary monitor +// when the process started. +// - DPI_AWARENESS_PER_MONITOR_AWARE +// Not virtualized. Expected to scale to the current DPI of the monitor. +inline DPI_AWARENESS GetThreadDpiAwareness() +{ + return GetAwarenessFromDpiAwarenessContext(GetThreadDpiAwarenessContext()); +} + +// +// Cloaking +// + +inline bool IsCloaked(HWND hwnd) +{ + DWORD dwCloak = 0; + DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &dwCloak, sizeof(dwCloak)); + return dwCloak != 0; +} + +inline bool IsShellCloaked(HWND hwnd) +{ + DWORD dwCloak = 0; + DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &dwCloak, sizeof(dwCloak)); + return WI_IsFlagSet(dwCloak, DWM_CLOAKED_SHELL); +} + +inline void CloakWindow(HWND hwnd, BOOL cloak = TRUE) +{ + DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak)); +} + +inline void UnCloakWindow(HWND hwnd) +{ + CloakWindow(hwnd, false /* cloak */); +} + +// Cloaking a window temporarily allows it to be moved multiple times, +// Maximized, etc without flashing or animating from unexpected locations. +class TempCloakWindowIf +{ + HWND _hwnd = nullptr; +public: + TempCloakWindowIf(HWND hwnd, bool shouldHide = true) + { + if (shouldHide && !IsCloaked(hwnd)) + { + CloakWindow(hwnd); + _hwnd = hwnd; + } + } + ~TempCloakWindowIf() + { + if (_hwnd) + { + UnCloakWindow(_hwnd); + } + } +}; + +// Note: 'Margins' below refers to invisible resize borders. +// +// This uses DWMWA_EXTENDED_FRAME_BOUNDS (the visible bounds) and GetWindowRect +// (the real/input bounds). The margins is the difference between these rects. +// +// DWMWA_EXTENDED_FRAME_BOUNDS returns PHYSICAL values, even if the caller is +// virtualized for DPI. GetWindowRect (and most other APIs) return logical +// coordinates. To allow using the margins from DPI virtualized apps, this +// switches explicitly to Per-Monitor DPI Aware (Physical) to call GetWindowRect +// and scales the values from the monitor DPI to the logical DPI, if needed. +inline RECT GetWindowMargins(HWND hwnd) +{ + DPI_AWARENESS_CONTEXT prev = SetThreadDpiAwarenessContext( + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + RECT margins{}; + + RECT rcWindow, rcFrame; + if (GetWindowRect(hwnd, &rcWindow) && + SUCCEEDED(DwmGetWindowAttribute( + hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rcFrame, sizeof(rcFrame)))) + { + margins.left = rcFrame.left - rcWindow.left; + margins.top = rcFrame.top - rcWindow.top; + margins.right = rcWindow.right - rcFrame.right; + margins.bottom = rcWindow.bottom - rcFrame.bottom; + } + + MonitorData mon; + if ((GetAwarenessFromDpiAwarenessContext(prev) != DPI_AWARENESS_PER_MONITOR_AWARE) && + (MonitorData::FromRect(rcWindow, &mon))) + { + UINT monitorDpi = mon.dpi; + UINT threadDpi = GetDpiFromDpiAwarenessContext(prev); + + margins.left = MulDiv(margins.left, threadDpi, monitorDpi); + margins.top = MulDiv(margins.top, threadDpi, monitorDpi); + margins.right = MulDiv(margins.right, threadDpi, monitorDpi); + margins.bottom = MulDiv(margins.bottom, threadDpi, monitorDpi); + } + + SetThreadDpiAwarenessContext(prev); + + return margins; +} + +inline void ExtendByMargins(RECT* rc, const RECT& margins) +{ + rc->left -= margins.left; + rc->top -= margins.top; + rc->right += margins.right; + rc->bottom += margins.bottom; +} + +inline void ReduceByMargins(RECT* rc, const RECT& margins) +{ + rc->left += margins.left; + rc->top += margins.top; + rc->right -= margins.right; + rc->bottom -= margins.bottom; +} + +// Gets a windows 'extended frame bounds'. +// +// This is the visible bounds of the window, not including the invisible resize +// area. +// +// Note: This doesn't call DWMWA_EXTENDED_FRAME_BOUNDS directly, because it +// does not work in DPI virtualized apps. (The result canot always be compared +// to GetWindowRect.) GetWindowMargins determines logical margins and this +// reduces the result of GetWindowRect by that amount, 'logical extended frame +// bounds'. +inline bool DwmGetExtendedFrameBounds( + HWND hwnd, + _Out_ PRECT prcFrame) +{ + if (!GetWindowRect(hwnd, prcFrame)) + { + return false; + } + + ReduceByMargins(prcFrame, GetWindowMargins(hwnd)); + + return true; +} + +// Returns true if the user setting for snapping is enabled. +// Settings, system, multitasking, 'snap windows'. +inline bool IsSnappingEnabled() +{ + BOOL snapEnabled = FALSE; + return SystemParametersInfo(SPI_GETWINARRANGING, 0, &snapEnabled, 0) && snapEnabled; +} + +// Wrapper that dynamically loads the API user32!IsWindowArranged. +// +// There is currently no header file for this function, so load it +// dynamically per the documentation. +// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-iswindowarranged +inline bool IsWindowArrangedWrapper(HWND hwnd) +{ + using PFNISWINDOWARRANGED = BOOL(*)(HWND hwnd); + static PFNISWINDOWARRANGED fnIsWindowArranged = nullptr; + static bool doOnce = false; + if (!doOnce) + { + doOnce = true; + HMODULE hmodUser = LoadLibraryW(L"user32.dll"); + fnIsWindowArranged = reinterpret_cast( + GetProcAddress(hmodUser, "IsWindowArranged")); + } + if (fnIsWindowArranged) + { + return !!fnIsWindowArranged(hwnd); + } + return false; +} diff --git a/Samples/WindowPlacement/cpp/inc/MonitorData.h b/Samples/WindowPlacement/cpp/inc/MonitorData.h new file mode 100644 index 00000000..15ecf91e --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/MonitorData.h @@ -0,0 +1,224 @@ +#pragma once +// Note: Intended to be included by User32Utils.h. + +// +// MonitorData +// +// A MonitorData has information about a monitor (HMONITOR). This is similar +// to the MONITORINFOEX struct, but it also includes the DPI. +// + +class MonitorData +{ +public: + // These helpers use MonitorFrom(Point/Rect/Window) to pick the nearest + // monitor, and returns a MonitorData with info about that monitor. + // Note: HMONITORs can become invalid at any time. Callers are expected + // to check the return value and handle failure. + static bool FromHandle(HMONITOR monitorHandle, _Out_ MonitorData* monitorData) noexcept; + static bool FromPoint(POINT pt, _Out_ MonitorData* monitorData) noexcept; + static bool FromRect(RECT rc, _Out_ MonitorData* monitorData) noexcept; + static bool FromWindow(HWND hwnd, _Out_ MonitorData* monitorData) noexcept; + static bool FromDeviceName(PCWSTR deviceName, _Out_ MonitorData* monitorData) noexcept; + + // Compare two monitors to see if they are different in ANY way. + // The MonitorData is information about a monitor at a point in time. If + // the monitor changes (for example resolution or DPI changes, but handle + // and name do not), this will detect that the monitors are NOT equal. + bool operator==(const MonitorData& otherMonitor) const noexcept + { + return Equals(otherMonitor); + } + bool Equals(const MonitorData& otherMonitor) const noexcept; + + // Compares only the device name (the string) for two monitors. This is + // used when finding a monitor that is 'best' to use, given a monitor from + // the past. (If the monitor from the past changed, it will likely have the + // same device name, but other fields have changed.) + bool MatchesDeviceName(PCWSTR otherDeviceName) const noexcept; + +private: + static BOOL MonitorEnumProc(HMONITOR, HDC, PRECT, LPARAM); + +public: + // The HMONITOR is the system's handle for this monitor, which can be + // used by APIs that take a monitor, like GetMonitorInfo. + // WARNING: This handle can become invalid at any time! It is best to read + // all needed data about a monitor before making any changes, and not + // assuming that the handle is still valid later. + HMONITOR handle = nullptr; + + // The monitor rect is the position/size of the monitor. These values are + // in screen coordinates, where the primary monitor origin is 0, 0. + RECT monitorRect = {}; + + // The work area is a subset of the monitor rect. This is the part of the + // monitor that is not covered by the taskbar (or taskbar-like apps). + // Non-topmost windows should generally stay within the work area of their + // monitor. + RECT workArea = {}; + + // The DPI is used for calculating the monitor's scale factor (the density + // of the pixels that the window's output is being displayed on). All UI + // drawn by an app should be scaled by the DPI scale factor: + // + // If the logical height of a button is 25, and the DPI is 192 (200%), + // the physical height of the button is 50: MulDiv(25, 192, 96). + // + // WARNING: While it is often the case that a window's DPI matches the DPI + // of the monitor it is mostly on, this is NOT always true! Apps should use + // GetDpiForWindow in most cases to know what scale factor a window should + // be using, or listen for WM_DPICHANGED to know when the DPI changes after + // the window is created. Apps should NOT assume that finding the DPI of the + // monitor that the window position is currently on will be the same. (In + // cases where the monitors change, or when a window is dragged between + // monitors, there are times when these two DPIs disagree, and using the + // monitor DPI can cause the window to get stuck at the wrong DPI!) + UINT dpi = 0; + + // The device name is a string representing the monitor. In cases where a + // monitor's position, handle, and other fields change (like changing + // primary monitor), the device id remains stable. + WCHAR deviceName[CCHDEVICENAME] = {}; +}; + +/* static */ +inline bool +MonitorData::FromHandle(HMONITOR monitorHandle, _Out_ MonitorData* monitorData) noexcept +{ + MONITORINFOEX mi = { sizeof(mi) }; + // Note: GetDpiForMonitor returns x/y DPI but these values are always equal. + UINT _dpi, unused; + + if (!GetMonitorInfo(monitorHandle, &mi) || + (GetDpiForMonitor(monitorHandle, MDT_EFFECTIVE_DPI, &_dpi, &unused) != S_OK)) + { + return false; + } + + monitorData->handle = monitorHandle; + monitorData->monitorRect = mi.rcMonitor; + monitorData->workArea = mi.rcWork; + monitorData->dpi = _dpi; + + if (FAILED(StringCchCopy( + monitorData->deviceName, ARRAYSIZE(monitorData->deviceName), mi.szDevice))) + { + return false; + } + + return true; +} + +/* static */ +inline bool +MonitorData::FromPoint(POINT pt, _Out_ MonitorData* monitorData) noexcept +{ + return FromHandle(MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST), monitorData); +} + +/* static */ +inline bool +MonitorData::FromRect(RECT rc, _Out_ MonitorData* monitorData) noexcept +{ + return FromHandle(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), monitorData); +} + +/* static */ +inline bool +MonitorData::FromWindow(HWND hwnd, _Out_ MonitorData* monitorData) noexcept +{ + return FromHandle(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), monitorData); +} + +struct DeviceNameSearchData +{ + PCWSTR deviceName; + MonitorData result; + bool resultSet = false; +}; + +/* static */ +inline BOOL +MonitorData::MonitorEnumProc( + HMONITOR handle, + HDC, + PRECT, + LPARAM lParam) +{ + DeviceNameSearchData* data = reinterpret_cast(lParam); + + MonitorData monitor; + if (FromHandle(handle, &monitor)) + { + if (monitor.MatchesDeviceName(data->deviceName)) + { + data->result = monitor; + data->resultSet = true; + return FALSE; + } + } + + return TRUE; +} + +/* static */ +inline bool +MonitorData::FromDeviceName( + PCWSTR deviceName, + _Out_ MonitorData* monitorData) noexcept +{ + DeviceNameSearchData data; + data.deviceName = deviceName; + + EnumDisplayMonitors(nullptr, nullptr, + MonitorEnumProc, reinterpret_cast(&data)); + + if (data.resultSet) + { + *monitorData = data.result; + return true; + } + + return false; +} + +inline bool +MonitorData::MatchesDeviceName(PCWSTR otherDeviceName) const noexcept +{ + return (wcscmp(deviceName, otherDeviceName) == 0); +} + +inline bool +MonitorData::Equals(const MonitorData& otherMonitor) const noexcept +{ + // If provided a reference to the same address as this monitor, we + // know all of the fields are the same (we don't need to check). + if (this == &otherMonitor) + { + return true; + } + + if (dpi != otherMonitor.dpi) + { + return false; + } + + if (!EqualRect(&monitorRect, &otherMonitor.monitorRect)) + { + return false; + } + + if (!EqualRect(&workArea, &otherMonitor.workArea)) + { + return false; + } + + if (!MatchesDeviceName(otherMonitor.deviceName)) + { + return false; + } + + // All fields match; the provided monitor data is equivalent to this one. + return true; +} diff --git a/Samples/WindowPlacement/cpp/inc/PlacementEx.h b/Samples/WindowPlacement/cpp/inc/PlacementEx.h new file mode 100644 index 00000000..3b212ee0 --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/PlacementEx.h @@ -0,0 +1,1596 @@ +#pragma once +// Note: Intended to be included by User32Utils.h. + +// +// PlacementEx +// +// This stores a top-level window's position and monitor info. +// +// GetPlacement stores a window's position into the provided PlacementEx, and +// SetPlacement moves a window to this stored position, adjusting it as needed +// to fit the best monitor. +// +// This also has helpers that do other operations related to storing window +// positions, such as FullScreen, Cascading, StartupInfo, etc. +// +// See the readme.md in the same directory as this file more information. +// + +// Internal flags used to track window state between launches +enum class PlacementFlags +{ + None = 0x0000, + + RestoreToMaximized = 0x0001, // WPF_RESTORETOMAXIMIZED + // Valid only if minimized (SW_MINIMIZE). + // When restored from minimized, the + // window will be maximized (restoring + // it again will move it to the normal + // position). + + Arranged = 0x0002, // Window is arranged (snapped). + // Set by GetPlacement if IsWindowArranged. + // + // This is used by SetPlacement if not + // maximizing or minimizing, and if the + // user setting for snapping is enabled. + // + // Like maximized/minimized windows, + // arranged windows have separate normal + // positions and 'real' position. Calling + // ShowWindow SW_RESTORE will move the + // window back to its normal position. + // + // The arrange position is stored in + // the arrangeRect field. This is the + // visible bounds of the window, + // GetWindowRect, reduced by the size of + // the window's invisible resize borders. + + AllowPartiallyOffScreen = 0x0004, // Skips forcing the normal position + // within the bounds of the work area, + // which SetPlacement does by default. + + AllowSizing = 0x0008, // Set (by GetPlacement) if window has + // WS_THICKFRAME. + // In SetPlacement, if the normal rect + // becomes larger than the work area, for + // example if a wide window is moved to a + // small monitor. + // When this happens, the size of the + // window is picked using the difference + // in monitor sizes, instead of respecting + // the logical size of the window. + + KeepHidden = 0x0010, // Any show command other than SW_HIDE + // will show the window. The KeepHidden + // flag will cause the window to stay + // hidden if it is not already visible. + + RestoreToArranged = 0x0020, // Like RestoreToMaximized, but restores + // arranged. + // Like Arranged flag, this flag indicates + // the arrangeRect field is set to the + // visible bounds (the arrange rect, + // aligned with the work area). + // Valid only if minimized (SW_MINIMIZE). + + FullScreen = 0x0040, // A FullScreen window fills the monitor + // rect, and has no caption (WS_CAPTION) + // or resize borders (WS_THICKFRAME). + // To Enter/Exit FullScreen, the app must + // remember the previous position (unlike + // Maximize/Minimize, where the system + // remembers this position). PlacementEx + // handles this scenario. + + VirtualDesktopId = 0x0080, // The virtualDesktopId field is set. + // This is a GUID used with the virtual + // desktop APIs. + // Apps should only restore windows to a + // past virtual desktop if restarting, + // either because of a session reboot or + // app re-launch. + // (Default app launch should not launch + // to a background desktop; similar to + // launching minimized.) + // Note: Virtual desktops require defining + // USE_VIRTUAL_DESKTOP_APIS prior to including + // User32Utils.h. See VirtualDesktopIds.h. + + NoActivate = 0x0100, // Don't activate the window. + // This is implied if VirtualDesktopId. +}; +DEFINE_ENUM_FLAG_OPERATORS(PlacementFlags); + +// Flags to adjust how PlacementEx uses STARTUPINFO +enum class StartupInfoFlags +{ + ShowCommand = 0x0001, // Heed the show command set by the command + // line: + // cmd: 'start /max ' + // pwsh: 'start -WindowStyle Maximized' + // This allows the user to launch an app + // Maximized or Minimized. + + MonitorHint = 0x0002, // Heed the monitor hint set by the taskbar, + // start menu, desktop icons, file explorer, + // etc. It indicates which monitor the app + // should launch on. + + None = 0, + + // [Default] Heed both ShowCommand and MonitorHint. This matches the + // default behavior of CreateWindow. + All = ShowCommand | MonitorHint, +}; +DEFINE_ENUM_FLAG_OPERATORS(StartupInfoFlags); + +class PlacementEx +{ +public: + // Stores a window's position/monitor data in a PlacementEx. + static bool GetPlacement( + HWND hwnd, + _Out_ PlacementEx* placement); + + // Moves a window to a position stored in a PlacementEx. + static bool SetPlacement(HWND hwnd, PlacementEx* placement); + + // Stores a placement in the registry. + void StoreInRegistry( + PCWSTR registryPath, + PCWSTR registryKeyName); + + static void StorePlacementInRegistry( + HWND hwnd, + PCWSTR registryPath, + PCWSTR registryKeyName); + + // FullScreen + // + // A FullScreen window is one that fills its monitor, and has some styles + // removed (no WS_CAPTION or WS_THICKFRAME). When entering FullScreen, the + // previous window position is stored. When exiting FullScreen, this position + // is moved to the window's monitor (which may have changed) and used to + // move the window to it's previous position. + bool EnterFullScreen( + HWND hwnd, + LONG_PTR styles = WS_CAPTION | WS_THICKFRAME); + + bool ExitFullScreen( + HWND hwnd, + LONG_PTR styles = WS_CAPTION | WS_THICKFRAME); + + bool IsFullScreen() const + { + return HasFlag(PlacementFlags::FullScreen); + } + + bool ToggleFullScreen( + HWND hwnd, + LONG_PTR styles = WS_CAPTION | WS_THICKFRAME) + { + if (IsFullScreen()) + { + return ExitFullScreen(hwnd, styles); + } + + return EnterFullScreen(hwnd, styles); + } + + // Sets the size. + // The provided size is logical, and scaled up by the DPI stored in the + // placement. The final position is adjusted to stay entirely within the + // work area. + void SetLogicalSize(SIZE size); + + // Fits the placement to a different monitor. + // This changes the normal rect, work area, and dpi fields. + // By default (unless AllowPartiallyOffScreen), the final normal rect will + // be entirely within the bounds of the provided monitor's work area. + void MoveToMonitor(const MonitorData& targetMonitor); + + void MoveToWindowMonitor(HWND hwnd) + { + MonitorData monitor; + if (MonitorData::FromWindow(hwnd, &monitor)) + { + MoveToMonitor(monitor); + } + } + + // Finds the monitor that best matches the position stored in the placement. + // This uses the device name (a string) to match monitors, and falls back to + // closest monitor (MonitorFromRect). + bool FindClosestMonitor(_Out_ MonitorData* monitor) const; + + // Moves the position down/right by roughly the height of the title bar. + // This is done when launching two windows to the same place. Rather than + // have both windows in the exact same place (one covering the other), + // an app can choose to cascade the windows, ensuring the user can see both + // window's title bars. Note that this function doesn't check that scenario: + // it always moves the window. + void Cascade(); + + // Updates a placement to account for flags set in the STARTUPINFO. + // If no startup info is provided, this uses GetStartupInfo. + // These are flags set when launching an application. For example, they + // can request the app launch Maximized/Minimized, or on a certain monitor. + void AdjustForStartupInfo( + _In_opt_ STARTUPINFO* psi = nullptr, + StartupInfoFlags siFlags = StartupInfoFlags::All); + + // AdjustForStartupInfo and also modify the placement for use as a 'main + // window' (normal launch). When launching the main window normally, it + // should not be minimized or on a background virtual desktop. + void AdjustForMainWindow( + _In_opt_ STARTUPINFO* psi = nullptr, + StartupInfoFlags siFlags = StartupInfoFlags::All); + + // Sets the show command (SW_ value) for this placement. + // There are a few special cases, including 'restore to maximize' (if + // changing the placement from maximize to minimize, this sets the + // RestoreToMaximized placement flag). + void SetShowCommand(UINT cmdFromStartupIfo); + + void RestoreIfMinimized() + { + if (IsMinimizeShowCmd(showCmd)) + { + SetShowCommand(SW_NORMAL); + } + } + + // Returns true if the provided show command is minimize/restore. + // (There are several SW_ values for each of these, for example + // SW_SHOWMINNOACTIVE, SW_SHOWMINIMIZED, SW_MINIMIZE.) + static bool IsMinimizeShowCmd(UINT cmd); + static bool IsRestoreShowCmd(UINT cmd); + + // Helper to move a RECT as needed to be fully within a work area. + static RECT KeepRectOnMonitor( + const RECT& rcPrev, + const RECT & workArea); + + // Helper to move a normal position from one monitor to another. + void AdjustNormalRect(const RECT& rcWorkNew, UINT dpiNew); + + // Given an arrangement rect stored from one monitor (work area), adjusts + // the rect to stay the same relative position (like top-left corner) on + // a different monitor's work area. + void AdjustArrangeRect(const RECT rcWorkNew); + + // Adds (or Removes) some window styles. + static void AddRemoveWindowStyles( + HWND hwnd, + bool add, + LONG_PTR stylesToChange); + +#ifdef USE_WINDOW_ACTION_APIS + // If available, SetPlacement is implemented mostly using the new API + // ApplyWindowAction (in WindowActions.h). + // https://learn.microsoft.com/en-us/windows/win32/winmsg/winuser/nf-winuser-applywindowaction + static bool ApplyPlacementExAsAction( + HWND hwnd, + PlacementEx* placement); +#endif + + // ToString() and FromString() allow you to store a placement in a string, + // for example to write it to the registry. + std::wstring ToString() const; + + static std::optional FromString( + const std::wstring& placementString); + + static bool FromString( + const std::wstring& placementString, + _Out_ PlacementEx* placement) + { + std::optional plex = FromString(placementString); + if (plex) + { + *placement = plex.value(); + return true; + } + return false; + } + + static bool FromRegistryKey( + PCWSTR registryPath, + PCWSTR registryKeyName, + _Out_ PlacementEx* placement); + + // Checks if a PlacementEx is valid (filled in by GetPlacement). + bool IsValid() const; + + // Zeros out a PlacementEx, making it invalid. + void Clear(); + + bool HasFlag(PlacementFlags pf) const + { + return (flags & pf) == pf; + } + + // Position (in screen coordinates) of the window. + // For Maximized/Minimized windows this rect is the restore position. + RECT normalRect = {}; + + // The work area of the monitor (in screen coordinates). + RECT workArea = {}; + + // The (potentially virtualized) DPI of the window. If the window is DPI- + // aware, returns the actual DPI of the window. Otherwise returns the + // virtualized DPI that the app sees before the system stretches it to the + // monitor's actual DPI. + // + // For DPI-aware windows, scale factor can be calculated by dividing by 96 + // (e.g. 120 DPI would be a scale factor of 120/96 = 125%). These windows + // must manually scale their content by this scale factor, or content will + // be rendered too small on high-DPI screens. + UINT dpi = 0; + + // The 'Show Command' is a value accepted by ShowWindow(). + // This defines the window's state (SW_MAXIMIZE, SW_MINIMIZE, SW_NORMAL). + UINT showCmd = 0; + + // The arrange rect is used only when the flag Arranged is set. + // + // This rect is the 'frame bounds' (visible bounds) of the window when it + // is snapped (arranged). This rect does not include the invisible resize + // borders (it is the rect that is expected to be aligned with the monitor + // edges). + // + // Like maximized/minimized windows, arranged windows have two positions, + // their normal rect (GetWindowPlacement) and their current position. For + // maximized/minimized, this positions are always re-evaluated for the + // monitor automatically, so we don't need to store the min/max positions. + // But we DO need to store the arranged positions, for example to remember + // that a window was snapped to the left half of its monitor (or top-left + // corner, etc). + RECT arrangeRect = {}; + + // Additional flags. + PlacementFlags flags = PlacementFlags::None; + + // The device name is a string representing the monitor (also 'device ID'). + // In cases like changing the primary monitor, the handle, rect, etc, all + // may change but the device name would remain stable. + WCHAR deviceName[CCHDEVICENAME] = {}; + +#ifdef USE_VIRTUAL_DESKTOP_APIS + GUID virtualDesktopId = {}; +#endif +}; + +// A PlacementEx is created zero initialized, which is invalid. +// +// A valid placement has: +// - a non-zero size normal position +// - a non-zero size work area +// - a normal position that intersects with the work area +// - a DPI that is >=96 (100%, the minimum supported DPI) +inline bool +PlacementEx::IsValid() const +{ + RECT rcI; + return (dpi >= 96) && + !IsRectEmpty(&normalRect) && + !IsRectEmpty(&workArea) && + IntersectRect(&rcI, &normalRect, &workArea); +} + +inline void +PlacementEx::Clear() +{ + dpi = 0; + showCmd = 0; + flags = PlacementFlags::None; + normalRect = {}; + arrangeRect = {}; + workArea = {}; + deviceName[0] = 0; +} + +/* static */ +inline bool +PlacementEx::GetPlacement( + HWND hwnd, + _Out_ PlacementEx* placement) +{ + placement->Clear(); + + // PlacementEx is for top-level windows only (parented to the Desktop Window). + // For other windows, child or message windows, you should call SetWindowPos + // directly. + if (!IsTopLevel(hwnd)) + { + return false; + } + + // Call GetWindowPlacement to get the window's show state (max/min/normal) + // and the normal position. + // Note: If the window is arranged (snapped) this returns 'normal' as the + // state but the normal position is the restore position (not where the + // window is, but where it will go if restored from arrange). + WINDOWPLACEMENT wp = { sizeof(wp) }; + if (!GetWindowPlacement(hwnd, &wp)) + { + return false; + } + + // Get the monitor info for the window's current monitor. + MonitorData monitorData; + if (!MonitorData::FromWindow(hwnd, &monitorData)) + { + return false; + } + + PlacementFlags flags = PlacementFlags::None; + RECT rcWork = monitorData.workArea; + RECT rcMonitor = monitorData.monitorRect; + RECT rcNormal = wp.rcNormalPosition; + RECT rcArrange{}; + + // Offset the normal rect by the work area offset from the monitor rect, + // aka 'Workspace Coordinates'. (PlacementEx doesn't use workspace + // coordinates, but Get/SetWindowPlacement do). + OffsetRect(&rcNormal, + rcWork.left - rcMonitor.left, rcWork.top - rcMonitor.top); + + // Set RestoreToMaximize flag if window is minimized and GetWindowPlacement + // set the WPF_RESTORETOMAXIMIZED flag. + // Note: ASYNC flag is never expected here, and the min position is ignored. + if (IsMinimizeShowCmd(wp.showCmd) && + (WI_IsFlagSet(wp.flags, WPF_RESTORETOMAXIMIZED))) + { + WI_SetFlag(flags, PlacementFlags::RestoreToMaximized); + } + + // The DPI in the PlacementEx is the window's DPI, GetDpiForWindow. + // But, if the current thread is virtualized for DPI, we instead use the + // thread's DPI. This could be different from the DPI that the window + // is running at (which is what GetDpiForwindow returns). The other APIs + // we're calling, querying the window and monitor info, are using the + // thread DPI. + const UINT threadDpi = GetThreadVirtualizedDpi(); + const UINT dpi = (threadDpi == 0) ? GetDpiForWindow(hwnd) : threadDpi; + + // Set the arranged flag and arranged ret if the window is arranged. + if (IsWindowArrangedWrapper(hwnd)) + { + // The arranged rect is the visible bounds of the window. + // This does not include the invisible resize borders (if the window + // has any). + // Note: This helper is in MiscUser32.h, and calls DwmGetWindowAttribute + // (DWMWA_EXTENDED_FRAME_BOUNDS) with some adjustments (to make it safe + // to call from DPI virtualized apps). + if (!DwmGetExtendedFrameBounds(hwnd, &rcArrange)) + { + return false; + } + + WI_SetFlag(flags, PlacementFlags::Arranged); + } + + LONG_PTR styles = GetWindowLongPtr(hwnd, GWL_STYLE); + + // Set the AllowSizing flag if the window has the WS_THICKFRAME style. + if (WI_IsFlagSet(styles, WS_THICKFRAME)) + { + WI_SetFlag(flags, PlacementFlags::AllowSizing); + } + + // If window has no caption/resize border styles and is positioned + // perfectly fitting the monitor rect, set FullScreen flag. + RECT rcWindow; + if (WI_AreAllFlagsClear(styles, WS_CAPTION | WS_THICKFRAME) && + GetWindowRect(hwnd, &rcWindow) && + EqualRect(&rcWindow, &rcMonitor)) + { + WI_SetFlag(flags, PlacementFlags::FullScreen); + } + +#ifdef USE_VIRTUAL_DESKTOP_APIS + if (GetVirtualDesktopId(hwnd, &placement->virtualDesktopId)) + { + WI_SetFlag(flags, PlacementFlags::VirtualDesktopId); + } +#endif + + // Fill in the provided PlacementEx. + placement->normalRect = rcNormal; + placement->arrangeRect = rcArrange; + placement->dpi = dpi; + placement->showCmd = wp.showCmd; + placement->workArea = rcWork; + placement->flags = flags; + + if (FAILED(StringCchCopy( + placement->deviceName, ARRAYSIZE(placement->deviceName), monitorData.deviceName))) + { + return false; + } + + return true; +} + +inline bool +PlacementEx::FindClosestMonitor(_Out_ MonitorData* monitor) const +{ + // Use the device name first to try to match the monitor. If two monitors + // change places (user switches primary monitor), the position and other + // monitor fields will change, but the device names should be most stable. + // + // Fall back to finding the monitor closest to the normal rect. + + return MonitorData::FromDeviceName(deviceName, monitor) || + MonitorData::FromRect(normalRect, monitor); +} + +/* static */ +inline bool +PlacementEx::SetPlacement(HWND hwnd, PlacementEx* placement) +{ +#ifdef USE_WINDOW_ACTION_APIS + // If using the Window Action APIs (ApplyWindowAction), SetPlacement is + // handled entirely in ApplyPlacementExAsAction (in WindowActions.h). + if (IsApplyWindowActionSupported()) + { + return ApplyPlacementExAsAction(hwnd, placement); + } +#endif + + // Determine which monitor is best/closest. + MonitorData targetMonitor; + if (!placement->FindClosestMonitor(&targetMonitor)) + { + return false; + } + + // Adjust the position to fit the monitor picked above. + placement->MoveToMonitor(targetMonitor); + + // The RestoreToArranged flag is read only if Minimizing the window and + // if snapping (the global user setting) is enabled. + // If this is set, we'll arrange the window first, then minimize it. + const bool isMinFromArranged = + IsMinimizeShowCmd(placement->showCmd) && + placement->HasFlag(PlacementFlags::RestoreToArranged) && + IsSnappingEnabled(); + + // The Arranged flag is read only when not Maximizing/Minimizing, and + // if snapping (the global user setting) is enabled. + // If Arranged (or RestoreToArranged), the arrangeRect field is set to + // the visible bounds of the arranged window, fit to the work area. + // If arranging the window, after moving it to the normal position, we'll + // arrange (snap) the window and move it to the arrange rect, updated as + // needed to fit the new monitor's work area. + const bool isArranged = + isMinFromArranged || + (placement->HasFlag(PlacementFlags::Arranged) && + !IsMinimizeShowCmd(placement->showCmd) && + (placement->showCmd != SW_MAXIMIZE) && + IsSnappingEnabled()); + + // Determine if the window is changing monitors. + // Note: This is the monitor the window is on now (not the monitor this + // placement is from). + // When moving a window between monitors, we always need to move it twice + // (or else moving between monitors with different DPIs could result in + // the wrong final window position). + MonitorData previousMonitor; + const bool isChangingMonitor = + MonitorData::FromWindow(hwnd, &previousMonitor) && + !previousMonitor.Equals(targetMonitor); + + // Determine if the window was previously Maximized/Minimized/Arranged. + const bool wasMinMaxArranged = + IsZoomed(hwnd) || IsIconic(hwnd) || IsWindowArrangedWrapper(hwnd); + + // The KeepHidden flag is only read if the window is currently hidden. + // Most show commands (other than SW_HIDE) will show the window. If this + // flag is set (and window is not already visible) we'll hide the window + // at the end. + const bool keepHidden = + !IsWindowVisible(hwnd) && + placement->HasFlag(PlacementFlags::KeepHidden); + + // The FullScreen flag causes the window to be sized to the monitor. + const bool fullScreen = placement->HasFlag(PlacementFlags::FullScreen); + + // The VirtualDesktopId flag will set the window's virtual desktop ID, + // potentially moving it to a background virtual desktop. + const bool setVirtualDesktop = + placement->HasFlag(PlacementFlags::VirtualDesktopId); + +#ifdef USE_VIRTUAL_DESKTOP_APIS + // If we're going to set the virtual desktop ID, remember the last + // foreground window. This is used before moving the window to a background + // virtual desktop, to avoid changing the active virtual desktop. + const HWND prevFg = setVirtualDesktop ? GetForegroundWindow() : nullptr; +#endif + + // If moving the window multiple times, cloak the window during the operation. + // This avoids the window flashing or animating from an unexpected location. + TempCloakWindowIf hideIf(hwnd, isChangingMonitor || + isArranged || + wasMinMaxArranged || + keepHidden || + fullScreen || + setVirtualDesktop); + + // Restore the window if it is maximized, minimized, or arranged. + // The normal position is the last non-Max/Min/Arrange position the window + // had. This means that we need the window to be 'normal' temporarily. + if (wasMinMaxArranged) + { + // SW_SHOWNOACTIVATE is like SW_RESTORE, but doesn't activate. + // Note: SW_SHOWNA shows but doesn't restore from Min/Max/Arrange. + ShowWindow(hwnd, SW_SHOWNOACTIVATE); + } + + // If the window is changing monitors, we need to move it onto the monitor + // before moving it into the final position. + // + // This is needed to handle DPI changes. When moving a window over a DPI + // change, it's final size can be unpredictable. To ensure the window ends + // up at the position we picked above (MoveToMonitor) we need to move it + // to that position after it is already on the monitor it is moving to. + if (isChangingMonitor) + { + UINT toDpi = GetDpiForWindow(hwnd); + UINT fromDpi = targetMonitor.dpi; + int cx = MulDiv(RECTWIDTH(placement->normalRect), toDpi, fromDpi); + int cy = MulDiv(RECTHEIGHT(placement->normalRect), toDpi, fromDpi); + + SetWindowPos(hwnd, + nullptr, + placement->normalRect.left, + placement->normalRect.top, + cx, + cy, + SWP_NOACTIVATE | SWP_NOZORDER); + } + + // Transform coordinates to Workspace Coordinates. + // PlacementEx stores coordinates in Screen Coordinates, but + // Get/SetWindowPlacement use workspace: offset by difference in + // work/monitor rect origin. + OffsetRect(&placement->normalRect, + targetMonitor.monitorRect.left - targetMonitor.workArea.left, + targetMonitor.monitorRect.top - targetMonitor.workArea.top); + + WINDOWPLACEMENT wp = { sizeof(wp) }; + wp.rcNormalPosition = placement->normalRect; + + // Use SW_NORMAL if minimizing from arrange. + // After setting the normal position, we'll arrange the window, and then + // we'll minimize the window (setting the caller's minimize show command). + wp.showCmd = isMinFromArranged ? SW_NORMAL : placement->showCmd; + + // If RestoreToMaximized, set WPF_RESTORETOMAXIMIZED. + if (placement->HasFlag(PlacementFlags::RestoreToMaximized)) + { + WI_SetFlag(wp.flags, WPF_RESTORETOMAXIMIZED); + } + + // Call SetWindowPlacement. + // This sets the normal position set above, and Min/Maximize state + // depending on the caller's show command (and decisions above). + if (!SetWindowPlacement(hwnd, &wp)) + { + return false; + } + + // If Arranging the window (and no failure from call above), make the + // window arranged and move it into the adjusted arrange rect (fit to + // the new monitor and expanded by the invisible resize area for this + // window on its current monitor). + if (isArranged) + { + // It is possible that we ended up Minimized/Maximized in the call + // above, even if we requested SW_NORMAL. This can happen if this is + // the first window shown by this process and the process was launched + // Minimized (and the caller set the RestoreToArranged flag, indicating + // the window should be Arranged and then Minimized). + if (IsIconic(hwnd) || IsZoomed(hwnd)) + { + ShowWindow(hwnd, SW_RESTORE); + } + + // 'Double click' on the top resize border. + // This causes the window to become arranged, snapping the top and + // bottom edges of the window to it's monitor's work area. + DefWindowProc(hwnd, WM_NCLBUTTONDBLCLK, HTTOP, 0); + + // The arrange rect is stored as frame bounds (without the invisible + // resize borders, aka window margins). We need to add these invisible + // resize borders back in, at the window's current DPI (now that it is + // on the final monitor). + RECT rcArrange = placement->arrangeRect; + ExtendByMargins(&rcArrange, GetWindowMargins(hwnd)); + + // Move the window to the adjusted arranged window position. + SetWindowPos( + hwnd, + nullptr, + rcArrange.left, + rcArrange.top, + RECTWIDTH(rcArrange), + RECTHEIGHT(rcArrange), + SWP_NOACTIVATE | SWP_NOZORDER); + } + + // If minimizing but RestoreToArrange flag was set, we still need to + // minimize the window (above we arranged it). + if (isMinFromArranged) + { + ShowWindow(hwnd, placement->showCmd); + } + + // FullScreen causes the window to be sized to the monitor, and have some + // window styles removed. + if (fullScreen) + { + placement->EnterFullScreen(hwnd); + } + +#ifdef USE_VIRTUAL_DESKTOP_APIS + // If provided a virtual desktop ID, move the window to that virtual desktop. + // + // Note: This is done after showing/activating the window (on the current + // virtual desktop). If we were to move the window to a background virtual + // desktop prior to activating it, that would switch the current virtual + // desktop. + // + // This assumes that the caller is restarting immediately after a reboot. + // For normal app launch, where app does not want to ever launch in a + // background virtual desktop, the VirtualDesktopId flag should not be set. + if (setVirtualDesktop) + { + // TODO: Activation/Virtual desktop switch problems... + // + // SetWindowPlacement above has activated the window. In most cases + // (if window ends up visible and on the current virtual desktop) this + // is what we want. The window should activate and come to foreground + // if possible. + // + // Note: We could tweak the SW_ commands, but there is no way to + // Maximize without activating :(. + // + // If an app launches several windows, and some are on a background + // virtual desktop, we MUST make sure to not cause a change in the + // current/active virtual desktop. This is destructive, but unfortunately + // happens if we attempt to move the foreground window to a background + // virtual desktop. + // + // Another problem: IVirtualDesktopManager can check if a window is on + // a background virtual desktop, (IsWindowOnCurrentVirtualDesktop), but + // we cannot tell from the GUID alone if it will move the window to a + // background virtual desktop. (So, by the time we can check if we're + // moving to a background desktop it is too late to avoid doing so with + // the foreground window.) + // + // Ideally, if an app launches several windows and ANY are on the active + // virtual desktop, those windows would be activated (and foreground if + // the app has foreground rights). But if the first window created ends + // up on a background virtual desktop, the app has no other eligible + // windows to make foreground when it must give it up on the one that + // was just created (to move it to a background desktop). + // + // So, we call GetForegroundWindow prior to activating this window, + // and here (before setting the virtual desktop ID), SetForegroundWindow + // on that window, 'giving up' foreground rights (this app will launch + // all windows without activating them). This NORMALLY works, but can + // still show flashing or cause a virtual desktop switch (but it avoids + // it in most cases). + // + // Ideas: + // - Should the VD APIs handle this (enforcing that calling the APIs + // never changes the active virtual desktop?) + // + // - Should/can we ask if a GUID is a background desktop, and use it to + // avoid giving up foreground here if launching first window on the + // current desktop? + // + // - Should there be an Activate/NoActivate placement flag, which handles + // showing without activating separately from virtual desktops? + if (prevFg) + { + SetForegroundWindow(prevFg); + } + + MoveToVirtualDesktop(hwnd, placement->virtualDesktopId); + // Note: Return value is true if window moved to a background desktop. + } +#endif + + // Hide the window if KeepHidden flag and window started invisible. + if (keepHidden) + { + ShowWindow(hwnd, SW_HIDE); + } + + return true; +} + +// +// PlacementParams +// +// This is used when creating a window, to pick the initial position of the +// window. +// +// Most apps will want to do a bit better than the default. This helper allows +// apps to opt into customized behavior: +// +// - Setting a custom fallback size (the size of the window the first time the +// user ever launches the app). +// +// - Setting a registry key to store the last close position. +// +// - Adjusting behavior for 'restart' scenarios, where it is ok to launch +// minimized or on a background virtual desktop. +// +// - Using another window as the position, cascading to keep the new position +// from covering the previous window. This avoids launching two instances +// of the app and ending up with two windows in the exact same place (one +// covering the other). +// +class PlacementParams +{ + const SIZE _defaultSize; + PlacementEx _placement = {}; + bool _isRestart = false; + bool _allowOffscreen = false; + StartupInfoFlags _startupInfoFlags = StartupInfoFlags::All; + +public: + // Apps should specify the default size, which is used only if no other + // size is available (the default is 600 x 400). + // + // If the app saves its last close position in the registry using + // PlacementEx::StorePlacementInRegistry, that registry key can be provided + // here, to launch the window where it closed by default. + PlacementParams( + SIZE defSize = { 600, 400 }, + PCWSTR keyPath = nullptr, + PCWSTR keyName = nullptr) : _defaultSize(defSize) + { + if (keyPath && keyName) + { + PlacementEx::FromRegistryKey(keyPath, keyName, &_placement); + } + } + + // Called if relaunching the app (as opposed to a 'normal' launch). This + // allows the window to launch Minimized or on a background desktop (if + // closed in that state). + void SetIsRestart() + { + _isRestart = true; + } + + // The Previous Window + // + // Apps that can launch many instances of itself should ideally not blindly + // use the position in the registry. Else, launching many instances of the + // app would 'pile up' (many windows would be in exactly the same place). + // + // Instead, if another instance is running, the new instance should 'cascade' + // over the other window (down/right a bit, keeping both title bars visible). + void FindPrevWindow(PCWSTR className) + { + HWND hwndPrev = FindWindow(className, nullptr); + + if (hwndPrev) + { + SetPrevWindow(hwndPrev); + } + } + + void SetPrevWindow(HWND hwndPrev) + { + PlacementEx placementT; + if (PlacementEx::GetPlacement(hwndPrev, &placementT)) + { + // If the other window is FullScreen, reject (ignore) it. + // We don't want to interpret the FullScreen position as a normal RECT, + // but we don't know the other window's normal RECT... + if (!placementT.HasFlag(PlacementFlags::FullScreen)) + { + placementT.Cascade(); + _placement = placementT; + } + } + } + + // By default, the position is always entirely within the bounds of the + // work area. Allowing offscreen skips this adjustment, but only if the new + // position is at least 50% within the work area. (SetPlacement does not + // allow moving a window more than 50% off screen.) + void SetAllowPartiallyOffscreen() + { + _allowOffscreen = true; + } + + // StartupInfo flags are set by default, and can be cleared. + void ClearStartupInfoFlag(StartupInfoFlags flag) + { + WI_ClearAllFlags(_startupInfoFlags, flag); + } + + // Positions the window and show it (make it visible). This should be done + // after all setup is complete, and when the thread is ready to start + // receiving messages. + PlacementEx PositionAndShow(HWND hwnd) + { + // If no position set, start with the window's current position and set + // the size using the default size. + if (!_placement.IsValid() && + PlacementEx::GetPlacement(hwnd, &_placement)) + { + _placement.SetLogicalSize(_defaultSize); + } + + if (_placement.IsValid()) + { + // If not a restart, make sure the window is not minimized (or cloaked). + // This also modifies the placement as needed if any of the StartupInfo + // flags are set (for example, a request to launch this app Maximized/ + // Minimized or on a particular monitor). + if (!_isRestart) + { + _placement.AdjustForMainWindow(nullptr, _startupInfoFlags); + } + + if (_allowOffscreen) + { + WI_SetFlag(_placement.flags, PlacementFlags::AllowPartiallyOffScreen); + } + + // Set the initial window position and show the window. + if (PlacementEx::SetPlacement(hwnd, &_placement)) + { + return _placement; + } + } + + // Something failed above. Fall back to showing the window at it's current + // position. + ShowWindow(hwnd, SW_SHOW); + PlacementEx::GetPlacement(hwnd, &_placement); + return _placement; + } +}; + +/* static */ +inline void +PlacementEx::StorePlacementInRegistry( + HWND hwnd, + PCWSTR registryPath, + PCWSTR registryKeyName) +{ + PlacementEx placement; + if (GetPlacement(hwnd, &placement)) + { + placement.StoreInRegistry(registryPath, registryKeyName); + } +} + +inline void +PlacementEx::StoreInRegistry( + PCWSTR registryPath, + PCWSTR registryKeyName) +{ + WriteStringRegKey(registryPath, registryKeyName, ToString()); +} + +inline void +PlacementEx::SetLogicalSize(SIZE size) +{ + normalRect.right = normalRect.left + MulDiv(size.cx, dpi, 96); + normalRect.bottom = normalRect.top + MulDiv(size.cy, dpi, 96); + normalRect = KeepRectOnMonitor(normalRect, workArea); +} + +/* static */ +inline void +PlacementEx::AddRemoveWindowStyles( + HWND hwnd, + bool add, + LONG_PTR stylesToChange) +{ + LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); + if (add) + { + style = style | stylesToChange; + } + else + { + style = style & ~stylesToChange; + } + + SetWindowLongPtr(hwnd, GWL_STYLE, style); + + // Recompute the client rect (which may change depending on styles). + SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER); +} + +inline bool +PlacementEx::EnterFullScreen( + HWND hwnd, + LONG_PTR styles) +{ + // If the FullScreen flag is not already set, update this placement to + // the window's current position. + if (!IsFullScreen() && !GetPlacement(hwnd, this)) + { + return false; + } + + // Get the monitor info. + MonitorData monitor; + if (!MonitorData::FromRect(normalRect, &monitor)) + { + return false; + } + RECT rcMonitor = monitor.monitorRect; + + // Set the FullScreen flag. + WI_SetFlag(flags, PlacementFlags::FullScreen); + + // Remove styles for window border and caption. + AddRemoveWindowStyles(hwnd, false /* remove styles */, styles); + + // Move the window to fit the monitor rect. + // Also show the window (if it is hidden) and activate it. + SetWindowPos(hwnd, + nullptr, + rcMonitor.left, + rcMonitor.top, + rcMonitor.right - rcMonitor.left, + rcMonitor.bottom - rcMonitor.top, + SWP_SHOWWINDOW); + + return true; +} + +inline bool +PlacementEx::ExitFullScreen( + HWND hwnd, + LONG_PTR styles) +{ + if (!IsFullScreen()) + { + return false; + } + + // Make sure the position is on the monitor the window is currently on. + MoveToWindowMonitor(hwnd); + + // Clear the FullScreen flag. + WI_ClearFlag(flags, PlacementFlags::FullScreen); + + // Add styles for window border and caption. + AddRemoveWindowStyles(hwnd, true /* add styles */, styles); + + // Move the window to the restore position (where it was before entering + // FullScreen). + SetPlacement(hwnd, this); + + return true; +} + +/* static */ +inline RECT +PlacementEx::KeepRectOnMonitor( + const RECT& rcPrev, + const RECT & workArea) +{ + RECT rc = rcPrev; + + // Check right/bottom before left/top. + // This keeps the title bar (top-left of the window) on-screen in cases + // where the new size is larger than the new monitor's work area. + + if (rc.right > workArea.right) + { + OffsetRect(&rc, workArea.right - rc.right, 0); + } + if (rc.left < workArea.left) + { + OffsetRect(&rc, workArea.left - rc.left, 0); + } + if (rc.bottom > workArea.bottom) + { + OffsetRect(&rc, 0, workArea.bottom - rc.bottom); + } + if (rc.top < workArea.top) + { + OffsetRect(&rc, 0, workArea.top - rc.top); + } + + return rc; +} + +inline void +PlacementEx::AdjustNormalRect( + const RECT& rcWorkNew, + UINT dpiNew) +{ + const RECT rcNormalPrev = normalRect; + const RECT rcWorkPrev = workArea; + const UINT dpiPrev = dpi; + const int cxWorkPrev = RECTWIDTH(rcWorkPrev); + const int cxWorkNew = RECTWIDTH(rcWorkNew); + const int cyWorkPrev = RECTHEIGHT(rcWorkPrev); + const int cyWorkNew = RECTHEIGHT(rcWorkNew); + + // Scale the offset from the monitor's work area by the difference in work + // area size. This ensures that the position is roughly in the same place + // if moved to a monitor with a very different size work area and back + // (after forcing the position onto the work area if necessary). + normalRect.left = rcWorkNew.left + + MulDiv(rcNormalPrev.left - rcWorkPrev.left, cxWorkPrev, cxWorkNew); + normalRect.top = rcWorkNew.top + + MulDiv(rcNormalPrev.top - rcWorkPrev.top, cyWorkPrev, cyWorkNew); + + // Scale the size (width/height) from the old monitor's DPI to the new + // monitor's DPI. This retains the logical window size. + normalRect.right = normalRect.left + + MulDiv(RECTWIDTH(rcNormalPrev), dpiNew, dpiPrev); + normalRect.bottom = normalRect.top + + MulDiv(RECTHEIGHT(rcNormalPrev), dpiNew, dpiPrev); + + // The AllowPartiallyOffScreen skips remaining adjustments, but only if the + // new position is at least 50% within the new monitor's work area. + if (HasFlag(PlacementFlags::AllowPartiallyOffScreen)) + { + RECT rcI{}; + IntersectRect(&rcI, &normalRect, &rcWorkNew); + UINT onscreenArea = RECTWIDTH(rcI) * RECTHEIGHT(rcI); + UINT totalArea = RECTWIDTH(normalRect) * RECTHEIGHT(normalRect); + + if (onscreenArea > (totalArea / 2)) + { + return; + } + } + + // Nudge the window to stay within the bounds of the work area. + normalRect = KeepRectOnMonitor(normalRect, rcWorkNew); + + // If the window is now the same size as the work area or larger, pick a + // new size using the previous size and work area. (If previously half the + // monitor width, in the center, choose something similar fit to the new + // monitor's work area. + if (HasFlag(PlacementFlags::AllowSizing)) + { + if (RECTWIDTH(normalRect) > cxWorkNew) + { + normalRect.left = rcWorkNew.left + + MulDiv(rcNormalPrev.left - rcWorkPrev.left, cxWorkNew, cxWorkPrev); + + normalRect.right = rcWorkNew.right - + MulDiv(rcWorkPrev.right - rcNormalPrev.right, cxWorkNew, cxWorkPrev); + } + + if (RECTHEIGHT(normalRect) > cyWorkNew) + { + normalRect.top = rcWorkNew.top + + MulDiv(rcNormalPrev.top - rcWorkPrev.top, cyWorkNew, cyWorkPrev); + + normalRect.bottom = rcWorkNew.bottom - + MulDiv(rcWorkPrev.bottom - rcNormalPrev.bottom, cyWorkNew, cyWorkPrev); + } + } +} + +inline void +PlacementEx::AdjustArrangeRect( + const RECT rcWorkNew) +{ + const RECT rcWorkPrev = workArea; + const LONG cxPrev = RECTWIDTH(rcWorkPrev); + const LONG cyPrev = RECTHEIGHT(rcWorkPrev); + const LONG cxNew = RECTWIDTH(rcWorkNew); + const LONG cyNew = RECTHEIGHT(rcWorkNew); + + // Adjust each side of the window, keeping the window at the same relative + // position wrt each monitor edge. Don't allow the window to be outside the + // new work area (even if it was previously partially outside the monitor). + arrangeRect.left = rcWorkNew.left + + max(0, MulDiv((arrangeRect.left - rcWorkPrev.left), cxNew, cxPrev)); + + arrangeRect.right = rcWorkNew.right - + max(0, MulDiv((rcWorkPrev.right - arrangeRect.right), cxNew, cxPrev)); + + arrangeRect.top = rcWorkNew.top + + max(0, MulDiv((arrangeRect.top - rcWorkPrev.top), cyNew, cyPrev)); + + arrangeRect.bottom = rcWorkNew.bottom - + max(0, MulDiv((rcWorkPrev.bottom - arrangeRect.bottom), cyNew, cyPrev)); +} + +inline void +PlacementEx::MoveToMonitor(const MonitorData& targetMonitor) +{ + // Move the normal rect to the target monitor. + AdjustNormalRect(targetMonitor.workArea, targetMonitor.dpi); + + // If the Arranged or RestoreToArranged flag are set, transform the arrange + // rect from the previous work area to the new work area. This retains the + // alignment with the work area edges (for example, left snapped). + if (HasFlag(PlacementFlags::Arranged) || + HasFlag(PlacementFlags::RestoreToArranged)) + { + AdjustArrangeRect(targetMonitor.workArea); + } + + // This changes the DPI and monitor info this placement stores. + workArea = targetMonitor.workArea; + dpi = targetMonitor.dpi; + StringCchCopy(deviceName, ARRAYSIZE(deviceName), targetMonitor.deviceName); +} + +inline void +PlacementEx::Cascade() +{ + // Compute the height of the caption bar, using the DPI in the PlacementEx. + // Note: This adds 2x the size of the border because the caption bar's true + // height doesn't include the top resize area. (When we cascade a window, + // we want the full caption bar on the previous window to be visible, and + // using only the reported height of the caption bar from system metrics + // would cause the window to visibly overlap the other window's title bar.) + const UINT captionHeight = + GetSystemMetricsForDpi(SM_CYCAPTION, dpi) + + (2 * GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi)); + + // Move to the right/down by the height of the caption bar. + OffsetRect(&normalRect, captionHeight, captionHeight); + + // If new position is now past the right side of the work area, move it + // back to the left side of the work area. + if (normalRect.right > workArea.right) + { + OffsetRect(&normalRect, workArea.left - normalRect.left, 0); + } + + // If new position is now past the bottom of the work area, move it back + // to the left side of the work area. + if (normalRect.bottom > workArea.bottom) + { + OffsetRect(&normalRect, 0, workArea.top - normalRect.top); + } +} + +inline void +PlacementEx::AdjustForMainWindow( + _In_opt_ STARTUPINFO* psi, + StartupInfoFlags siFlags) +{ + // If Minimized, switch to restored (or Maximized). + RestoreIfMinimized(); + + // Do not launch on a background virtual desktop (cloaked). + WI_ClearFlag(flags, PlacementFlags::VirtualDesktopId); + + // Apply startup info parameters (start Max/Min or Monitor Hint). + AdjustForStartupInfo(psi, siFlags); +} + +inline void +PlacementEx::AdjustForStartupInfo( + _In_opt_ STARTUPINFO* psi, + StartupInfoFlags siFlags) +{ + STARTUPINFO si{}; + + // Read the startup info if one was not provided explicitly. + if (!psi) + { + si.cb = sizeof(si); + GetStartupInfo(&si); + psi = &si; + } + + // If the monitor hint is set, move the stored position to the hint monitor. + MonitorData monitor; + if (WI_IsFlagSet(siFlags, StartupInfoFlags::MonitorHint) && + MonitorData::FromHandle((HMONITOR)psi->hStdOutput, &monitor)) + { + MoveToMonitor(monitor); + } + + // Set the show command if the flag is set (both by the caller and the + // startup info). + // This notably handles cases where the startup info requests Maximized or + // Minimized. The other show commands, SW_NORMAL, SW_SHOWDEFAULT, are NOT + // used. This ensures a Maximized window that is closed and launched in a + // default manner relaunches Maximized (not at the normal position). + // Also check for show commands like SW_HIDE and SW_SHOWNA, though these + // flags are not generally used with the startup info. + if (WI_IsFlagSet(siFlags, StartupInfoFlags::ShowCommand) && + WI_IsFlagSet(psi->dwFlags, STARTF_USESHOWWINDOW)) + { + switch (psi->wShowWindow) + { + case SW_HIDE: + WI_SetFlag(flags, PlacementFlags::KeepHidden); + break; + + case SW_SHOWNOACTIVATE: + case SW_SHOWNA: + WI_SetFlag(flags, PlacementFlags::NoActivate); + break; + + case SW_NORMAL: + case SW_SHOW: + case SW_RESTORE: + case SW_SHOWDEFAULT: + // Keep the existing show command. + break; + + default: + SetShowCommand(psi->wShowWindow); + } + } +} + +/* static */ +inline bool +PlacementEx::IsMinimizeShowCmd(UINT cmd) +{ + switch (cmd) + { + case SW_SHOWMINNOACTIVE: + case SW_SHOWMINIMIZED: + case SW_MINIMIZE: + return true; + } + return false; +} + +/* static */ +inline bool +PlacementEx::IsRestoreShowCmd(UINT cmd) +{ + switch (cmd) + { + case SW_NORMAL: + case SW_RESTORE: + case SW_SHOWDEFAULT: + return true; + } + return false; +} + +inline void +PlacementEx::SetShowCommand(UINT newShowCmd) +{ + // If Restoring a Minimized placement with 'restore to maximize' flag, swap + // the new show command to SW_MAXIMIZE and clear the restore to maximize flag. + if (IsRestoreShowCmd(newShowCmd) && + IsMinimizeShowCmd(showCmd) && + WI_IsFlagSet(flags, PlacementFlags::RestoreToMaximized)) + { + newShowCmd = SW_MAXIMIZE; + WI_ClearFlag(flags, PlacementFlags::RestoreToMaximized); + } + + // If Maximized and now Minimizing, set the 'restore to maximize' flag. + if (IsMinimizeShowCmd(newShowCmd) && + (showCmd == SW_MAXIMIZE)) + { + WI_SetFlag(flags, PlacementFlags::RestoreToMaximized); + } + + // If arranged and now Minimizing, set 'restore to arranged' flag instead + // of 'arranged'. + if (IsMinimizeShowCmd(newShowCmd) && + WI_IsFlagSet(flags, PlacementFlags::Arranged)) + { + WI_ClearFlag(flags, PlacementFlags::Arranged); + WI_SetFlag(flags, PlacementFlags::RestoreToArranged); + } + + showCmd = newShowCmd; +} + +/* static */ +inline bool +PlacementEx::FromRegistryKey( + PCWSTR registryPath, + PCWSTR registryKeyName, + _Out_ PlacementEx* placement) +{ + std::wstring placementStr = ReadStringRegKey(registryPath, registryKeyName); + + return !placementStr.empty() && FromString(placementStr, placement); +} + +// TODO: Something better for To/FromString()... +// - maybe switch to PCWSTR? +// StringCchPrintf +// see %SDXROOT%\clientcore\windows\tools\uiext\util.cpp +// _ParseDecimalUINT +// limit numbers to 16 bit + + +constexpr PCWSTR GUID_FORMAT_STR = + L"%08X-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX"; + +inline void StringToGuid(const std::wstring& guidStr, _Out_ GUID* guid) +{ + swscanf(guidStr.c_str(), GUID_FORMAT_STR, + &guid->Data1, &guid->Data2, &guid->Data3, + &guid->Data4[0], &guid->Data4[1], &guid->Data4[2], &guid->Data4[3], + &guid->Data4[4], &guid->Data4[5], &guid->Data4[6], &guid->Data4[7]); +} + +inline std::wstring GuidToString(const GUID& guid) +{ + return wil::str_printf( + GUID_FORMAT_STR, + guid.Data1, + guid.Data2, + guid.Data3, + guid.Data4[0], + guid.Data4[1], + guid.Data4[2], + guid.Data4[3], + guid.Data4[4], + guid.Data4[5], + guid.Data4[6], + guid.Data4[7] + ); +} + +// Serializes the placement information into a string. +inline std::wstring +PlacementEx::ToString() const +{ + // Serialize the data to a string. + // 15 integers then a string, separated by commas: + // - [left,top,right,bottom] normal rect + // - [left,top,right,bottom] work area + // - dpi + // - show command + // - placement flags + // - [left,top,right,bottom] arrange rect + // - virtual desktop GUID + // - device name + +#ifdef USE_VIRTUAL_DESKTOP_APIS + std::wstring guidString = GuidToString(virtualDesktopId); + + return wil::str_printf( + L"%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%ws,%ws\n", + normalRect.left, + normalRect.top, + normalRect.right, + normalRect.bottom, + workArea.left, + workArea.top, + workArea.right, + workArea.bottom, + dpi, + showCmd, + flags, + arrangeRect.left, + arrangeRect.top, + arrangeRect.right, + arrangeRect.bottom, + guidString.c_str(), + deviceName); +#else + return wil::str_printf( + L"%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%ws\n", + normalRect.left, + normalRect.top, + normalRect.right, + normalRect.bottom, + workArea.left, + workArea.top, + workArea.right, + workArea.bottom, + dpi, + showCmd, + flags, + arrangeRect.left, + arrangeRect.top, + arrangeRect.right, + arrangeRect.bottom, + deviceName); +#endif + +} + +// Converts a string (produced by PlacementEx::ToString()) into a PlacementEx. +/* static */ +inline std::optional +PlacementEx::FromString(const std::wstring& placementString) +{ + if (placementString.empty()) + { + return std::nullopt; + } + + std::wstring delim = L","; + + #define NUM_INTEGERS_IN_PLACEMENT_STRING 15 + int parsedInts[NUM_INTEGERS_IN_PLACEMENT_STRING]; + + PlacementEx pex; + try + { + auto start = 0U; + auto end = placementString.find(delim); + int index = 0; +#ifdef USE_VIRTUAL_DESKTOP_APIS + GUID virtualDesktopId = {}; +#endif + + while (end != std::wstring::npos) + { + std::wstring token = placementString.substr(start, end - start); + + start = (UINT)(end + delim.length()); + end = placementString.find(delim, start); + + if (index == NUM_INTEGERS_IN_PLACEMENT_STRING) + { +#ifdef USE_VIRTUAL_DESKTOP_APIS + StringToGuid(token, &virtualDesktopId); +#endif + break; + } + + parsedInts[index] = stoi(token); + + index++; + } + + if (index != NUM_INTEGERS_IN_PLACEMENT_STRING) + { + return std::nullopt; + } + + // The remainder of the string is the deviceName. + std::wstring deviceName = placementString.substr( + start, placementString.length() - start - 1); + // TODO: registry string has a newline, or a '.'? + // its important this string matches what we read live: + // - close, change primary monitor, relaunch to same position + + pex.normalRect.left = parsedInts[0]; + pex.normalRect.top = parsedInts[1]; + pex.normalRect.right = parsedInts[2]; + pex.normalRect.bottom = parsedInts[3]; + pex.workArea.left = parsedInts[4]; + pex.workArea.top = parsedInts[5]; + pex.workArea.right = parsedInts[6]; + pex.workArea.bottom = parsedInts[7]; + pex.dpi = parsedInts[8]; + pex.showCmd = parsedInts[9]; + pex.flags = static_cast(parsedInts[10]); + pex.arrangeRect.left = parsedInts[11]; + pex.arrangeRect.top = parsedInts[12]; + pex.arrangeRect.right = parsedInts[13]; + pex.arrangeRect.bottom = parsedInts[14]; +#ifdef USE_VIRTUAL_DESKTOP_APIS + pex.virtualDesktopId = virtualDesktopId; +#endif + + StringCchCopy(pex.deviceName, + ARRAYSIZE(pex.deviceName), deviceName.c_str()); + } + catch(std::invalid_argument const& /* ex */) + { + return std::nullopt; + } + + return pex; +} diff --git a/Samples/WindowPlacement/cpp/inc/RegistryHelpers.h b/Samples/WindowPlacement/cpp/inc/RegistryHelpers.h new file mode 100644 index 00000000..407e4226 --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/RegistryHelpers.h @@ -0,0 +1,85 @@ +#pragma once +// Note: Intended to be included by User32Utils.h. + +// +// Helpers for reading and writing values (strings and DWORDs) to app-specific +// registry keys, used to persist data like window positions. +// + +inline HKEY GetAppRegKey(PCWSTR appName) +{ + HKEY hKey = nullptr; + + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, appName, + 0, KEY_ALL_ACCESS , &hKey); + + if (openRes != ERROR_SUCCESS) + { + RegCreateKeyEx(HKEY_CURRENT_USER, appName, + 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); + } + + return hKey; +} + +inline std::wstring ReadStringRegKey(PCWSTR appName, PCWSTR keyName) +{ + HKEY hKey = GetAppRegKey(appName); + + WCHAR textBuffer[500]; + DWORD dwBufferSize = sizeof(textBuffer); + + // Read the registry key. + ULONG nError = RegQueryValueExW(hKey, keyName, + 0, NULL, (LPBYTE)textBuffer, &dwBufferSize); + + // Return empty string if registry read failed. + if (nError != ERROR_SUCCESS) + { + return L""; + } + + RegCloseKey(hKey); + + return (PWSTR)&textBuffer; +} + +inline void WriteStringRegKey(PCWSTR appName, PCWSTR keyName, std::wstring keyValue) +{ + HKEY hKey = GetAppRegKey(appName); + + RegSetValueEx(hKey, keyName, 0, REG_SZ, + (LPBYTE)keyValue.c_str(), (DWORD)((wcslen(keyValue.c_str()) * 2) + 1)); + + RegCloseKey(hKey); +} + +inline void DeleteRegValue(PCWSTR appName, PCWSTR keyName) +{ + HKEY hKey = GetAppRegKey(appName); + + RegDeleteValue(hKey, keyName); + + RegCloseKey(hKey); +} + +inline DWORD ReadDwordRegKey(PCWSTR appName, PCWSTR keyName, DWORD dwDefault) +{ + HKEY hKey = GetAppRegKey(appName); + + unsigned long type = REG_DWORD, size = 1024; + RegQueryValueEx(hKey, keyName, nullptr, &type, (PBYTE)&dwDefault, &size); + + RegCloseKey(hKey); + + return dwDefault; +} + +inline void WriteDwordRegKey(PCWSTR appName, PCWSTR keyName, DWORD dwValue) +{ + HKEY hKey = GetAppRegKey(appName); + + RegSetValueEx(hKey, keyName, 0, REG_DWORD, (PBYTE)&dwValue, sizeof(dwValue)); + + RegCloseKey(hKey); +} diff --git a/Samples/WindowPlacement/cpp/inc/User32Utils.h b/Samples/WindowPlacement/cpp/inc/User32Utils.h new file mode 100644 index 00000000..c94ad9e2 --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/User32Utils.h @@ -0,0 +1,38 @@ +#pragma once + +#include "windows.h" +#include "shellscalingapi.h" +#include "dwmapi.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MonitorData.h" +#include "MiscUser32.h" +#include "RegistryHelpers.h" +#include "CurrentMonitorTopology.h" + +#ifdef USE_VIRTUAL_DESKTOP_APIS +#include "shobjidl.h" +#include "shobjidl_core.h" +#include +#include "VirtualDesktopIds.h" +#endif + +#ifdef USE_WINDOW_ACTION_APIS +bool IsApplyWindowActionSupported(); +bool ApplyWindowActionWrapper(HWND hwnd, WINDOW_ACTION* action); +#endif + +#include "PlacementEx.h" + +#ifdef USE_WINDOW_ACTION_APIS +#include "WindowActions.h" +#endif diff --git a/Samples/WindowPlacement/cpp/inc/VirtualDesktopIds.h b/Samples/WindowPlacement/cpp/inc/VirtualDesktopIds.h new file mode 100644 index 00000000..3e1380c0 --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/VirtualDesktopIds.h @@ -0,0 +1,118 @@ +#pragma once +// Note: Included by User32Utils.h, if USE_VIRTUAL_DESKTOP_APIS is defined. + +// +// This file has wrapper functions used by PlacementEx.h to use virtual desktops. +// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ivirtualdesktopmanager +// +// This requires: +// - initializing COM, by running this on each thread: +// CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); +// - linking against: +// $(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib +// $(ONECORE_INTERNAL_PRIV_SDK_LIB_PATH_L)\OneCore_Forwarder_ole32.lib +// - Not querying window state from a SendMessage call +// (the virtual desktop APIs fail in this case...) +// +// These APIs require linking to additional binaries, and can also (because +// of COM usage) lead to deadlocks depending on how the APIs are called. See +// VirtualDesktopHelper in chromium, which moves these calls to a background +// thread for this reason: +// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/frame/browser_desktop_window_tree_host_win.cc +// +// TODO: Maybe add a helper like VirtualDesktopHelper from chromium? +// - manage background thread that initializes COM, and allows calls from +// within a SendMessage call. +// - dynamically link com dlls? +// - RegisterCloakedNotification/WM_CLOAKED_STATE_CHANGED? +// + +// Uncomment the below if debugging virtual desktops issues: +// #define BREAK_ON_VIRTUAL_DESKTOP_FAILURE + +// Returns the GUID for the virtual desktop that a window belongs to. +inline bool +GetVirtualDesktopId( + HWND hwnd, + _Out_opt_ GUID* desktopId) +{ + Microsoft::WRL::ComPtr spVirtualDesktopManager; + + HRESULT hr = CoCreateInstance(__uuidof(VirtualDesktopManager), + nullptr, + CLSCTX_INPROC_SERVER, + IID_IVirtualDesktopManager, + (void**)&spVirtualDesktopManager); + + if (hr != S_OK) + { + // Note: The API above fails if the current thread needs to call: + // CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); +#ifdef BREAK_ON_VIRTUAL_DESKTOP_FAILURE + __debugbreak(); +#endif + return false; + } + + // Get the ID (GUID) of the window's virtual desktop. + hr = spVirtualDesktopManager->GetWindowDesktopId(hwnd, desktopId); + if (hr != S_OK) + { +#ifdef BREAK_ON_VIRTUAL_DESKTOP_FAILURE + // ELEMENTNOTFOUND is expected if newly created window queries its + // desktop ID (if taskbar isn't aware of the window yet). + if (hr != TYPE_E_ELEMENTNOTFOUND) + { + // Note: The call above fails if called while handling a SendMessage, + // error code: RPC_E_CANTCALLOUT_ININPUTSYNCCALL + __debugbreak(); + } +#endif + return false; + } + + return true; +} + +// Moves a window to a Virtual Desktop, specified by the virtual desktop GUID. +// This returns true if the window was successfully moved AND if the window is +// now on a background (not active) virtual desktop. +inline bool +MoveToVirtualDesktop( + HWND hwnd, + const GUID& desktopId) +{ + Microsoft::WRL::ComPtr spVirtualDesktopManager; + + HRESULT hr = CoCreateInstance(__uuidof(VirtualDesktopManager), + nullptr, + CLSCTX_INPROC_SERVER, + IID_IVirtualDesktopManager, + (void**)&spVirtualDesktopManager); + + // Note: Virtual desktop IDs are best effort. + // They may fail for legit reasons, like user deleted the virtual desktop. + + if (hr != S_OK) + { + return false; + } + + hr = spVirtualDesktopManager->MoveWindowToDesktop(hwnd, desktopId); + + if (hr != S_OK) + { + return false; + } + + BOOL isCurrentDesktop = TRUE; + hr = spVirtualDesktopManager->IsWindowOnCurrentVirtualDesktop(hwnd, &isCurrentDesktop); + + if (hr != S_OK) + { + return false; + } + + // Return true if the window is now on a background virtual desktop. + return !isCurrentDesktop; +} diff --git a/Samples/WindowPlacement/cpp/inc/WindowActions.h b/Samples/WindowPlacement/cpp/inc/WindowActions.h new file mode 100644 index 00000000..c9432055 --- /dev/null +++ b/Samples/WindowPlacement/cpp/inc/WindowActions.h @@ -0,0 +1,386 @@ +#pragma once +// Note: Included by User32Utils.h, if USE_WINDOW_ACTION_APIS is defined. + +// +// Note: These APIs are only available on newer OS builds. Apps using these +// APIs that need to run on older releases should handle a fallback when the +// APIs fail. +// + +// ======================================================================== + +// +// CWindowAction +// +// Helper class to set fields in a WINDOW_ACTION and call ApplyWindowAction. +// +// A Window Action describes changes to make to a top level window. Moving, +// sizing, activating, maximizing, etc. +// +// The 'kinds' field of the action is flags that describe the changes to make. +// Some have no additional payload, like WAK_ACTIVATE, and others like +// WAK_POSITION have a corresponding field containing the value. +// +// The 'modifiers' field is also flags, some of which have their own field. The +// modifiers change the behavior of one or more of the kinds in some way. For +// example, the modifier WAM_FRAME_BOUNDS changes how the provided rect +// (position/size) are interpretted. +// +class CWindowAction : public WINDOW_ACTION +{ +public: + CWindowAction() + { + RtlZeroMemory(this, sizeof(this)); + } + + // Calls ApplyWindowAction to apply the changes in the action to the window. + bool Apply(HWND hwnd) + { + return ApplyWindowActionWrapper(hwnd, this); + } + + // + // The functions below set fields in the action. + // + + // Activates the window. + // This makes the window the active window (GetActiveWindow), and the + // foreground window (GetForegroundWindow) if the app is in foreground. + void SetActivate() + { + WI_SetFlag(kinds, WAK_ACTIVATE); + } + + // Shows (or hides) a window. This sets/clears WS_VISIBLE. + void SetVisible(bool val = true) + { + WI_SetFlag(kinds, WAK_VISIBILITY); + visible = val; + } + + // The insert after window is the window that this window is behind, or + // below (in z-order). This window can be special sentinel values like + // HWND_TOP or HWND_TOPMOST, which have special meaning. + void SetInsertAfter(HWND val) + { + WI_SetFlag(kinds, WAK_INSERT_AFTER); + insertAfter = val; + } + + // Set the position and size (the rect). + // By default, this rect includes parts of the window that are invisible + // resize borders. (As opposed to 'frame bounds', the visible bounds of + // the window, with invisible resize borders removed.) + void SetRect(RECT rc) + { + WI_SetAllFlags(kinds, WAK_POSITION | WAK_SIZE); + position.x = rc.left; + position.y = rc.top; + size.cx = RECTWIDTH(rc); + size.cy = RECTHEIGHT(rc); + } + + // Changes the behavior of the provided rect, indicating the rect does not + // include invisible resize borders (it is the desired visible bounds of + // the window). This is most useful with Arranged positions, which normally + // align the visible bounds of the window with the edges of the monitor. + void SetFrameBounds() + { + WI_SetFlag(modifiers, WAM_FRAME_BOUNDS); + } + + // Sets the state, Minimize, Maximize, Arrange, Restore. + // When Min/Max/Arranged, the window has a 'normal' position that is + // separate from it's current position. (Normal is the last non-special + // position.) Restoring a window from Max/Min/Arrange moves it back to the + // normal position. + void SetState(WINDOW_PLACEMENT_STATE state) + { + WI_SetFlag(kinds, WAK_PLACEMENT_STATE); + placementState = state; + } + + // Sets the normal rect. This requires also setting the state. + // + // If Max/Min/Arranged, the normal rect overrides the default normal + // position (which is the previous non-special position the window had). + // If the state is Restored, the normal position has the same meaning as + // the position and size (the rect). + void SetNormalRect(RECT rc) + { + WI_SetFlag(kinds, WAK_NORMAL_RECT); + normalRect = rc; + } + + void SetMaximized() + { + SetState(WPS_MAXIMIZED); + } + + void SetRestored() + { + SetState(WPS_NORMAL); + } + + // Setting Arranged requires an arranged rect, which is in frame bounds + // (visible bounds, no invisible resize borders). This is expected to be + // aligned with edges of the work area (like left half, or corner, etc). + void SetArranged(RECT arrangeRect) + { + SetState(WPS_ARRANGED); + SetRect(arrangeRect); + SetFrameBounds(); + } + + // Minimize normally remembers the previous state of the window (if + // Maximized or Arranged). Restoring a Minimized windows returns the window + // to that previous state. Optionally, an action can specify the window be + // Minimized but restore to Maximized or Arranged. + void SetMinimized() + { + SetState(WPS_MINIMIZED); + } + + void SetMinRestoreToMaximized() + { + SetState(WPS_MINIMIZED); + WI_SetFlag(modifiers, WAM_RESTORE_TO_MAXIMIZED); + } + + void SetMinRestoreToArranged(RECT arrangeRect) + { + SetState(WPS_MINIMIZED); + WI_SetFlag(modifiers, WAM_RESTORE_TO_ARRANGED); + SetRect(arrangeRect); + SetFrameBounds(); + } + + // The fit to monitor flag causes the window's normal position to be moved + // as needed to stay entirely within the bounds of the work area. + void SetFitToMonitor() + { + WI_SetFlag(kinds, WAK_FIT_TO_MONITOR); + } + + // The Move to Monitor flag specifies the monitor the window should be on, + // using a point (this picks the nearest monitor to this point, in screen + // coordinates). + void SetMoveToMonitorPoint(POINT point) + { + WI_SetFlag(kinds, WAK_MOVE_TO_MONITOR); + pointOnMonitor = point; + } + + void SetMoveToMonitor(MonitorData monitor) + { + SetMoveToMonitorPoint({ monitor.workArea.left, monitor.workArea.top }); + } + + // The provided position and size are assumed to be picked for the current + // monitors. If a position is from the past, the action can specify the + // work area from the past. The provided position is adjusted if the work + // area has changed, to ensure the window's position relative to the monitor + // stays the same. + void SetPreviousWorkArea(RECT prevWorkArea) + { + WI_SetFlag(modifiers, WAM_WORK_AREA); + workArea = prevWorkArea; + } + + // The provided size is assumed to be picked for the window's current DPI. + // If the size is picked from a past DPI, or for the DPI of the monitor, + // the DPI field in the action should be set to that DPI. This ensures that + // if the window changes DPI, the final size matches the one provided. + void SetPreviousDpi(UINT prevDpi) + { + WI_SetFlag(modifiers, WAM_DPI); + dpi = prevDpi; + } +}; + +// Called by PlacementEx::SetPlacement, if IsApplyWindowActionSupported. +// This translates the PlacementEx into a Window Action and calls ApplyWindowAction. +// If this succeeds, the caller will return without making additional changes +// (all the changes SetPlacement makes are made here if ApplyWindowAction is supported). +/* static */ +inline bool +PlacementEx::ApplyPlacementExAsAction(HWND hwnd, PlacementEx* placement) +{ + const bool fVirtDesktop = placement->HasFlag(PlacementFlags::VirtualDesktopId); + const bool fFullScreen = placement->HasFlag(PlacementFlags::FullScreen); + + // Hide window temporarily if moving multiple times. + TempCloakWindowIf hideIf(hwnd, fVirtDesktop || fFullScreen); + + CWindowAction action; + + // Activate by default, unless NoActivate flag. + // If setting a virtual desktop this implies NoActivate. + if (!placement->HasFlag(PlacementFlags::NoActivate) && !fVirtDesktop) + { + action.SetActivate(); + } + + // Show window by default, unless KeepHidden and previously invisible. + if (!IsWindowVisible(hwnd) && + !placement->HasFlag(PlacementFlags::KeepHidden)) + { + action.SetVisible(); + } + + // Set Fit to Monitor by default, unless AllowPartiallyOffScreen. + if (!placement->HasFlag(PlacementFlags::AllowPartiallyOffScreen)) + { + action.SetFitToMonitor(); + } + + // Set the state (Min/Max/Arrange/Restore). + // + // This is determined by the show command (SW_MAXIMIZE, SW_MINIMIZE, etc), + // and by the Arranged PlacementEx flag. + // + // If Minimize, the two PlacementEx flags RestoreToMaximized/Arranged set + // the restore state (when window restores from Min). + // + // If Arrange, or Min restore to Arrange, the PlacementEx has an arrange + // position. This is a rect within the work area in the placement, normally + // aligned with 2 or 3 edges of the work area. (Position does not include + // invisible frame bounds, WAM_FRAME_BOUNDS.) + if (placement->showCmd == SW_MAXIMIZE) + { + action.SetMaximized(); + } + else if (IsMinimizeShowCmd(placement->showCmd)) + { + if (placement->HasFlag(PlacementFlags::RestoreToMaximized)) + { + action.SetMinRestoreToMaximized(); + } + else if (placement->HasFlag(PlacementFlags::RestoreToArranged)) + { + action.SetMinRestoreToArranged(placement->arrangeRect); + } + else + { + action.SetMinimized(); + } + } + else if (placement->HasFlag(PlacementFlags::Arranged)) + { + action.SetArranged(placement->arrangeRect); + } + else + { + action.SetRestored(); + } + + // Set the normal rect. This is the restore position if Min/Max/Arrange, + // or the window position if restored. + action.SetNormalRect(placement->normalRect); + + // Set the work area and DPI. These are from the window/monitor when the + // placement was created, and are used to update the position as needed if + // the monitors have changed. + action.SetPreviousWorkArea(placement->workArea); + action.SetPreviousDpi(placement->dpi); + + // Call ApplyWindowAction to apply the changes. + if (!action.Apply(hwnd)) + { + return false; + } + + // Enter FullScreen, if needed. + if (fFullScreen) + { + placement->EnterFullScreen(hwnd); + } + +#ifdef USE_VIRTUAL_DESKTOP_APIS + // Set virtual desktop ID, if needed. + if (fVirtDesktop) + { + MoveToVirtualDesktop(hwnd, placement->virtualDesktopId); + } +#endif + + return true; +} + +// ApplyWindowAction is dynamically loaded the first time it is called. +using fnApplyWindowAction = BOOL(*)(HWND hwnd, WINDOW_ACTION* action); +fnApplyWindowAction pfnApplyWindowAction = nullptr; + +// Called once per process to load ApplyWindowAction (and check if supported). +inline void LoadApplyWindowActionApi() +{ + // Dynamically load the user32!ApplyWindowAction API. + pfnApplyWindowAction = reinterpret_cast( + GetProcAddress(LoadLibrary(L"user32.dll"), "ApplyWindowAction")); + + // There are some OS builds that have the API export prior to the API being + // supported. To know if the API is supported (prior to attempting to use + // it to move the window), we create a dummy window on the first call and + // attempt to call ApplyWindowAction on it, to see if it returns false. + if (pfnApplyWindowAction) + { + HINSTANCE hInstance = GetModuleHandle(NULL); + PCWSTR className = L"ProbeApplyApiWindowClassName"; + + WNDCLASSEX wc = { sizeof(WNDCLASSEX) }; + wc.hInstance = hInstance; + wc.lpfnWndProc = DefWindowProc; + wc.lpszClassName = className; + RegisterClassEx(&wc); + + HWND hwnd = CreateWindowEx( + 0, className, nullptr, + 0, 0, 0, 0, 0, + nullptr, nullptr, hInstance, nullptr); + + WINDOW_ACTION action{}; + action.kinds = WAK_POSITION; + + if (!pfnApplyWindowAction(hwnd, &action)) + { + // The API is present but disabled. Clear the pointer so that we + // consider it not available. + pfnApplyWindowAction = nullptr; + } + + DestroyWindow(hwnd); + UnregisterClass(className, hInstance); + } +} + +// Returns true if the ApplyWindowAction API is supported on the current OS. +bool IsApplyWindowActionSupported() +{ + static bool initOnce = false; + if (!initOnce) + { + initOnce = true; + LoadApplyWindowActionApi(); + } + + return (pfnApplyWindowAction != nullptr); +} + +// Calls ApplyWindowAction, if it is available and supported on the current OS. +inline bool ApplyWindowActionWrapper(HWND hwnd, WINDOW_ACTION* action) +{ + if (IsApplyWindowActionSupported()) + { + if (pfnApplyWindowAction(hwnd, action)) + { + return true; + } + +#ifdef BREAK_ON_WINDOW_ACTIONS_FAILURE + __debugbreak(); +#endif + } + + return false; +}