[CmdPal] WindowWalker Show the actual window icon instead of the process icon (#42316)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->

<img width="629" height="767" alt="image"
src="https://github.com/user-attachments/assets/bc093640-db9d-4bc8-bc33-53729e692850"
/>


## Summary of the Pull Request

This is a PR for issue **#42260**.
It targets **CmdPal’s WindowWalker** and changes the icon retrieval to
use **SendMessage** to obtain the window’s actual icon, instead of using
the **process icon**.

To support this, I added a new configuration option.

<img width="400" height="401" alt="image"
src="https://github.com/user-attachments/assets/1a2d97a8-ff95-40b0-be42-746c2b1409d4"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #42260
- [ ] **Communication:** @jiripolasek 
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- 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


Actully, The `ThumbnailHelper` already contains code that converts an
`IntPtr` `hIcon` into an `IRandomAccessStream`, as shown below:

```
 private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon)
 {
     var memoryStream = new MemoryStream();

     // Ensure disposing the icon before freeing the handle
     using (var icon = Icon.FromHandle(hIcon))
     {
         icon.ToBitmap().Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
     }

     // Clean up the unmanaged handle without risking a use-after-free.
     NativeMethods.DestroyIcon(hIcon);

     memoryStream.Position = 0;
     return memoryStream;
 }

 private static async Task<IRandomAccessStream?> FromHIconToStream(IntPtr hIcon)
 {
     var stream = new InMemoryRandomAccessStream();

     using var memoryStream = GetMemoryStreamFromIcon(hIcon); // this will DestroyIcon hIcon
     using var outputStream = stream.GetOutputStreamAt(0);
     using var dataWriter = new DataWriter(outputStream);

     dataWriter.WriteBytes(memoryStream.ToArray());
     await dataWriter.StoreAsync();
     await dataWriter.FlushAsync();

     return stream;
 }
```

Without modifying (or using) this code, I implemented the almost same
logic directly in `SwitchToWindowCommand` (calling the async code with
`Wait` to block synchronously). The reasons are:

1. I wanted to limit changes to the **WindowWalker** project area. I
don’t expect other extensions to need this behavior.
2. Because this is resource-related work, exposing a public helper that
pulls memory from an `hIcon` pointer seems risky—especially in a class
like `ThumbnailHelper`.

Therefore, I implemented behavior that is nearly identical to the
snippet above.

I did use `using`/`Dispose` where appropriate, but the
`InMemoryRandomAccessStream` created for `IconInfo.FromStream` appears
to use internal referencing; disposing it would be incorrect. For that
reason I didn’t wrap it in a `using`. I’m not entirely sure whether GC
will handle this cleanly.

However, based on the implementation of `FromStream` itself and its
usage elsewhere (e.g., in `ThumbnailHelper`), this seems to be the
correct usage pattern, though I’m not entirely sure.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
This commit is contained in:
Lee Won Jun
2025-10-21 02:09:23 +09:00
committed by GitHub
parent bc8adb3189
commit f28d009131
10 changed files with 185 additions and 7 deletions

View File

@@ -639,6 +639,7 @@ Hiber
Hiberboot Hiberboot
HIBYTE HIBYTE
hicon hicon
HICONSM
HIDEREADONLY HIDEREADONLY
HIDEWINDOW HIDEWINDOW
Hif Hif

View File

@@ -17,6 +17,7 @@ public class Settings : ISettingsInterface
private readonly bool hideKillProcessOnElevatedProcesses; private readonly bool hideKillProcessOnElevatedProcesses;
private readonly bool hideExplorerSettingInfo; private readonly bool hideExplorerSettingInfo;
private readonly bool inMruOrder; private readonly bool inMruOrder;
private readonly bool useWindowIcon;
public Settings( public Settings(
bool resultsFromVisibleDesktopOnly = false, bool resultsFromVisibleDesktopOnly = false,
@@ -27,7 +28,8 @@ public class Settings : ISettingsInterface
bool openAfterKillAndClose = false, bool openAfterKillAndClose = false,
bool hideKillProcessOnElevatedProcesses = false, bool hideKillProcessOnElevatedProcesses = false,
bool hideExplorerSettingInfo = true, bool hideExplorerSettingInfo = true,
bool inMruOrder = true) bool inMruOrder = true,
bool useWindowIcon = true)
{ {
this.resultsFromVisibleDesktopOnly = resultsFromVisibleDesktopOnly; this.resultsFromVisibleDesktopOnly = resultsFromVisibleDesktopOnly;
this.subtitleShowPid = subtitleShowPid; this.subtitleShowPid = subtitleShowPid;
@@ -38,6 +40,7 @@ public class Settings : ISettingsInterface
this.hideKillProcessOnElevatedProcesses = hideKillProcessOnElevatedProcesses; this.hideKillProcessOnElevatedProcesses = hideKillProcessOnElevatedProcesses;
this.hideExplorerSettingInfo = hideExplorerSettingInfo; this.hideExplorerSettingInfo = hideExplorerSettingInfo;
this.inMruOrder = inMruOrder; this.inMruOrder = inMruOrder;
this.useWindowIcon = useWindowIcon;
} }
public bool ResultsFromVisibleDesktopOnly => resultsFromVisibleDesktopOnly; public bool ResultsFromVisibleDesktopOnly => resultsFromVisibleDesktopOnly;
@@ -57,4 +60,6 @@ public class Settings : ISettingsInterface
public bool HideExplorerSettingInfo => hideExplorerSettingInfo; public bool HideExplorerSettingInfo => hideExplorerSettingInfo;
public bool InMruOrder => inMruOrder; public bool InMruOrder => inMruOrder;
public bool UseWindowIcon => useWindowIcon;
} }

View File

@@ -2,11 +2,16 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.IO;
using Microsoft.CmdPal.Ext.WindowWalker.Components; using Microsoft.CmdPal.Ext.WindowWalker.Components;
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
using Microsoft.CmdPal.Ext.WindowWalker.Properties; using Microsoft.CmdPal.Ext.WindowWalker.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; namespace Microsoft.CmdPal.Ext.WindowWalker.Commands;
@@ -16,20 +21,53 @@ internal sealed partial class SwitchToWindowCommand : InvokableCommand
public SwitchToWindowCommand(Window? window) public SwitchToWindowCommand(Window? window)
{ {
Icon = Icons.GenericAppIcon; // Fallback to default icon
Name = Resources.switch_to_command_title; Name = Resources.switch_to_command_title;
_window = window; _window = window;
if (_window is not null) if (_window is not null)
{ {
var p = Process.GetProcessById((int)_window.Process.ProcessID); // Use window icon
if (p is not null) if (SettingsManager.Instance.UseWindowIcon)
{ {
try if (_window.TryGetWindowIcon(out var icon) && icon is not null)
{ {
var processFileName = p.MainModule?.FileName; try
Icon = new IconInfo(processFileName); {
using var bitmap = icon.ToBitmap();
using var memoryStream = new MemoryStream();
bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
var raStream = new InMemoryRandomAccessStream();
using var outputStream = raStream.GetOutputStreamAt(0);
using var dataWriter = new DataWriter(outputStream);
dataWriter.WriteBytes(memoryStream.ToArray());
dataWriter.StoreAsync().AsTask().Wait();
dataWriter.FlushAsync().AsTask().Wait();
Icon = IconInfo.FromStream(raStream);
}
catch
{
}
finally
{
icon.Dispose();
}
} }
catch }
// Use process icon
else
{
var p = Process.GetProcessById((int)_window.Process.ProcessID);
if (p is not null)
{ {
try
{
var processFileName = p.MainModule?.FileName;
Icon = new IconInfo(processFileName);
}
catch
{
}
} }
} }
} }

View File

@@ -188,6 +188,62 @@ internal sealed class Window
thread.Start(); thread.Start();
} }
/// <summary>
/// Tries to get the window icon.
/// </summary>
/// <param name="icon">The window icon if found; otherwise, null.</param>
/// <returns>True if an icon was found; otherwise, false.</returns>
internal bool TryGetWindowIcon(out System.Drawing.Icon? icon)
{
icon = null;
if (hwnd == IntPtr.Zero)
{
return false;
}
// Try WM_GETICON with SendMessageTimeout
if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_BIG, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out var result) != 0 && result != 0)
{
icon = System.Drawing.Icon.FromHandle((IntPtr)result);
NativeMethods.DestroyIcon((IntPtr)result);
return true;
}
if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_SMALL, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0)
{
icon = System.Drawing.Icon.FromHandle((IntPtr)result);
NativeMethods.DestroyIcon((IntPtr)result);
return true;
}
if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_SMALL2, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0)
{
icon = System.Drawing.Icon.FromHandle((IntPtr)result);
NativeMethods.DestroyIcon((IntPtr)result);
return true;
}
// Fallback to GetClassLongPtr
var iconHandle = NativeMethods.GetClassLongPtr(hwnd, Win32Constants.GCLP_HICON);
if (iconHandle != IntPtr.Zero)
{
icon = System.Drawing.Icon.FromHandle(iconHandle);
NativeMethods.DestroyIcon((IntPtr)iconHandle);
return true;
}
iconHandle = NativeMethods.GetClassLongPtr(hwnd, Win32Constants.GCLP_HICONSM);
if (iconHandle != IntPtr.Zero)
{
icon = System.Drawing.Icon.FromHandle(iconHandle);
NativeMethods.DestroyIcon((IntPtr)iconHandle);
return true;
}
return false;
}
/// <summary> /// <summary>
/// Converts the window name to string along with the process name /// Converts the window name to string along with the process name
/// </summary> /// </summary>

View File

@@ -23,4 +23,6 @@ public interface ISettingsInterface
public bool HideExplorerSettingInfo { get; } public bool HideExplorerSettingInfo { get; }
public bool InMruOrder { get; } public bool InMruOrder { get; }
public bool UseWindowIcon { get; }
} }

View File

@@ -84,6 +84,12 @@ public static partial class NativeMethods
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern int SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam, int fuFlags, int uTimeout, out int lpdwResult); public static extern int SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam, int fuFlags, int uTimeout, out int lpdwResult);
[DllImport("user32.dll", EntryPoint = "GetClassLongPtr")]
public static extern IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern bool DestroyIcon(IntPtr hIcon);
[DllImport("kernel32.dll")] [DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject); public static extern bool CloseHandle(IntPtr hObject);
@@ -143,6 +149,41 @@ public static class Win32Constants
/// </summary> /// </summary>
public const int SC_CLOSE = 0xF060; public const int SC_CLOSE = 0xF060;
/// <summary>
/// Sent to a window to retrieve a handle to the large or small icon associated with a window.
/// </summary>
public const uint WM_GETICON = 0x007F;
/// <summary>
/// Retrieve the large icon for the window.
/// </summary>
public const int ICON_BIG = 1;
/// <summary>
/// Retrieve the small icon for the window.
/// </summary>
public const int ICON_SMALL = 0;
/// <summary>
/// Retrieve the small icon provided by the application.
/// </summary>
public const int ICON_SMALL2 = 2;
/// <summary>
/// The function returns if the receiving thread does not respond within the timeout period.
/// </summary>
public const int SMTO_ABORTIFHUNG = 0x0002;
/// <summary>
/// Retrieves a handle to the icon associated with the class.
/// </summary>
public const int GCLP_HICON = -14;
/// <summary>
/// Retrieves a handle to the small icon associated with the class.
/// </summary>
public const int GCLP_HICONSM = -34;
/// <summary> /// <summary>
/// RPC call succeeded /// RPC call succeeded
/// </summary> /// </summary>

View File

@@ -70,6 +70,12 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.windowwalker_SettingInMruOrder_Description, Resources.windowwalker_SettingInMruOrder_Description,
true); true);
private readonly ToggleSetting _useWindowIcon = new(
Namespaced(nameof(UseWindowIcon)),
Resources.windowwalker_SettingUseWindowIcon,
Resources.windowwalker_SettingUseWindowIcon_Description,
true);
public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value; public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value;
public bool SubtitleShowPid => _subtitleShowPid.Value; public bool SubtitleShowPid => _subtitleShowPid.Value;
@@ -88,6 +94,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public bool InMruOrder => _inMruOrder.Value; public bool InMruOrder => _inMruOrder.Value;
public bool UseWindowIcon => _useWindowIcon.Value;
internal static string SettingsJsonPath() internal static string SettingsJsonPath()
{ {
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
@@ -110,6 +118,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_hideKillProcessOnElevatedProcesses); Settings.Add(_hideKillProcessOnElevatedProcesses);
Settings.Add(_hideExplorerSettingInfo); Settings.Add(_hideExplorerSettingInfo);
Settings.Add(_inMruOrder); Settings.Add(_inMruOrder);
Settings.Add(_useWindowIcon);
// Load settings from file upon initialization // Load settings from file upon initialization
LoadSettings(); LoadSettings();

View File

@@ -15,4 +15,6 @@ internal sealed class Icons
internal static IconInfo CloseWindow { get; } = new IconInfo("\uE894"); // Clear internal static IconInfo CloseWindow { get; } = new IconInfo("\uE894"); // Clear
internal static IconInfo Info { get; } = new IconInfo("\uE946"); // Info internal static IconInfo Info { get; } = new IconInfo("\uE946"); // Info
internal static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon
} }

View File

@@ -401,5 +401,23 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
return ResourceManager.GetString("windowwalker_SettingTagPid", resourceCulture); return ResourceManager.GetString("windowwalker_SettingTagPid", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Use window icons.
/// </summary>
public static string windowwalker_SettingUseWindowIcon {
get {
return ResourceManager.GetString("windowwalker_SettingUseWindowIcon", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show the actual window icon instead of the process icon.
/// </summary>
public static string windowwalker_SettingUseWindowIcon_Description {
get {
return ResourceManager.GetString("windowwalker_SettingUseWindowIcon_Description", resourceCulture);
}
}
} }
} }

View File

@@ -235,4 +235,10 @@
<data name="windowwalker_NoResultsMessage" xml:space="preserve"> <data name="windowwalker_NoResultsMessage" xml:space="preserve">
<value>No open windows found</value> <value>No open windows found</value>
</data> </data>
<data name="windowwalker_SettingUseWindowIcon" xml:space="preserve">
<value>Use window icons</value>
</data>
<data name="windowwalker_SettingUseWindowIcon_Description" xml:space="preserve">
<value>Show the actual window icon instead of the process icon</value>
</data>
</root> </root>