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;
+}