[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

@@ -188,6 +188,62 @@ internal sealed class Window
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>
/// Converts the window name to string along with the process name
/// </summary>