Skip to content

Commit 103429b

Browse files
authored
CmdPal: Add hidden window as owner for tool windows (#42902)
## Summary of the Pull Request This PR changes the method used to hide tool windows from the taskbar and Alt+Tab to a more reliable approach. Previously, this was achieved by adding `WS_EX_TOOLWINDOW` to an unowned top-level window, which proved unreliable in several scenarios. The new implementation assigns a hidden window as the owner of each tool window. This ensures that the window does not appear on the taskbar even when the Windows setting **Settings → System → Multitasking → On the taskbar, show all opened windows** is set to **On all desktops**. ## Change log one-liner Fixes Command Palette windows occasionally appearing on the taskbar under certain system settings. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #42395 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** none - [x] **Documentation updated:** no need <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Tested alongside the stable CmdPal on a system with
1 parent 01fb831 commit 103429b

File tree

5 files changed

+104
-21
lines changed

5 files changed

+104
-21
lines changed

‎src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowExtensions.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static void SetIcon(this Window window)
2222
appWindow.SetIcon(@"Assets\icon.ico");
2323
}
2424

25-
private static HWND GetWindowHwnd(this Window window)
25+
public static HWND GetWindowHwnd(this Window window)
2626
{
2727
return window is null
2828
? throw new ArgumentNullException(nameof(window))
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CmdPal.UI.Helpers;
6+
using Microsoft.UI.Xaml;
7+
using Windows.Win32;
8+
using Windows.Win32.Foundation;
9+
using Windows.Win32.Graphics.Dwm;
10+
using Windows.Win32.UI.WindowsAndMessaging;
11+
12+
namespace Microsoft.CmdPal.UI;
13+
14+
/// <summary>
15+
/// Provides behavior to control taskbar and Alt+Tab presence by assigning a hidden owner
16+
/// and toggling extended window styles for a target window.
17+
/// </summary>
18+
internal sealed class HiddenOwnerWindowBehavior
19+
{
20+
private HWND _hiddenOwnerHwnd;
21+
private Window? _hiddenWindow;
22+
23+
/// <summary>
24+
/// Shows or hides a window in the taskbar (and Alt+Tab) by updating ownership and extended window styles.
25+
/// </summary>
26+
/// <param name="target">The <see cref="Microsoft.UI.Xaml.Window"/> to update.</param>
27+
/// <param name="isVisibleInTaskbar"> True to show the window in the taskbar (and Alt+Tab); false to hide it from both. </param>
28+
/// <remarks>
29+
/// When hiding the window, a hidden owner is assigned and <see cref="WINDOW_EX_STYLE.WS_EX_TOOLWINDOW"/>
30+
/// is enabled to keep it out of the taskbar and Alt+Tab. When showing, the owner is cleared and
31+
/// <see cref="WINDOW_EX_STYLE.WS_EX_APPWINDOW"/> is enabled to ensure taskbar presence. Since tool
32+
/// windows use smaller corner radii, the normal rounded corners are enforced via
33+
/// <see cref="DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND"/>.
34+
/// </remarks>
35+
/// <seealso href="https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons" />
36+
public void ShowInTaskbar(Window target, bool isVisibleInTaskbar)
37+
{
38+
/*
39+
* There are the three main ways to control whether a window appears on the taskbar:
40+
* https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons
41+
*
42+
* 1. Set the window's owner. Owned windows do not appear on the taskbar:
43+
* Turns out this is the most reliable way to hide a window from the taskbar and ALT+TAB. WinForms and WPF uses this method
44+
* to back their ShowInTaskbar property as well.
45+
*
46+
* 2. Use the WS_EX_TOOLWINDOW extended window style:
47+
* This mostly works, with some reports that it silently fails in some cases. The biggest issue
48+
* is that for certain Windows settings (like Multitasking -> Show taskbar buttons on all displays = On all desktops),
49+
* the taskbar button is always shown even for tool windows.
50+
*
51+
* 3. Using ITaskbarList:
52+
* This is what AppWindow.IsShownInSwitchers uses, but it's COM-based and more complex, and can
53+
* fail if Explorer isn't running or responding. It could be a good backup, if needed.
54+
*/
55+
56+
var visibleHwnd = target.GetWindowHwnd();
57+
58+
if (isVisibleInTaskbar)
59+
{
60+
// remove any owner window
61+
PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, HWND.Null);
62+
}
63+
else
64+
{
65+
// Set the hidden window as the owner of the target window
66+
var hiddenHwnd = EnsureHiddenOwner();
67+
PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, hiddenHwnd);
68+
}
69+
70+
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
71+
// Tool window and app window styles are mutually exclusive, change both just to be safe
72+
target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !isVisibleInTaskbar);
73+
target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_APPWINDOW, isVisibleInTaskbar);
74+
75+
// Since tool windows have smaller corner radii, we need to force the normal ones
76+
target.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
77+
}
78+
79+
private HWND EnsureHiddenOwner()
80+
{
81+
if (_hiddenOwnerHwnd.IsNull)
82+
{
83+
_hiddenWindow = new Window();
84+
_hiddenOwnerHwnd = _hiddenWindow.GetWindowHwnd();
85+
}
86+
87+
return _hiddenOwnerHwnd;
88+
}
89+
}

‎src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public sealed partial class MainWindow : WindowEx,
5757
private readonly List<TopLevelHotkey> _hotkeys = [];
5858
private readonly KeyboardListener _keyboardListener;
5959
private readonly LocalKeyboardListener _localKeyboardListener;
60+
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
6061
private bool _ignoreHotKeyWhenFullScreen = true;
6162

6263
private DesktopAcrylicController? _acrylicController;
@@ -65,6 +66,7 @@ public sealed partial class MainWindow : WindowEx,
6566
public MainWindow()
6667
{
6768
InitializeComponent();
69+
HideWindow();
6870

6971
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
7072

@@ -73,6 +75,8 @@ public MainWindow()
7375
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
7476
}
7577

78+
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
79+
7680
_keyboardListener = new KeyboardListener();
7781
_keyboardListener.Start();
7882

@@ -126,16 +130,6 @@ public MainWindow()
126130

127131
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
128132
HideWindow();
129-
130-
ApplyWindowStyle();
131-
}
132-
133-
private void ApplyWindowStyle()
134-
{
135-
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
136-
// Since tool windows have smaller corner radii, we need to force the normal ones
137-
this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !Debugger.IsAttached);
138-
this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
139133
}
140134

141135
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
@@ -264,7 +258,7 @@ private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
264258
// because that would make it hard to debug the app
265259
if (Debugger.IsAttached)
266260
{
267-
ApplyWindowStyle();
261+
_hiddenOwnerBehavior.ShowInTaskbar(this, true);
268262
}
269263

270264
// Just to be sure, SHOW our hwnd.

‎src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,9 @@ GetModuleHandle
5858

5959
GetWindowLong
6060
SetWindowLong
61-
WINDOW_EX_STYLE
61+
WINDOW_EX_STYLE
62+
CreateWindowEx
63+
WNDCLASSEXW
64+
RegisterClassEx
65+
GetStockObject
66+
GetModuleHandle

‎src/modules/cmdpal/Microsoft.CmdPal.UI/ToastWindow.xaml.cs‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ public sealed partial class ToastWindow : WindowEx,
2525
IRecipient<QuitMessage>
2626
{
2727
private readonly HWND _hwnd;
28+
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
29+
private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new();
2830

2931
public ToastViewModel ViewModel { get; } = new();
3032

31-
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
32-
3333
public ToastWindow()
3434
{
3535
this.InitializeComponent();
@@ -39,12 +39,7 @@ public ToastWindow()
3939
this.SetIcon();
4040
AppWindow.Title = RS_.GetString("ToastWindowTitle");
4141
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
42-
43-
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
44-
// Since tool windows have smaller corner radii, we need to force the normal ones
45-
// to visually match system toasts.
46-
this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, true);
47-
this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
42+
_hiddenOwnerWindowBehavior.ShowInTaskbar(this, false);
4843

4944
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
5045
PInvoke.EnableWindow(_hwnd, false);

0 commit comments

Comments
 (0)