mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
[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:
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -639,6 +639,7 @@ Hiber
|
||||
Hiberboot
|
||||
HIBYTE
|
||||
hicon
|
||||
HICONSM
|
||||
HIDEREADONLY
|
||||
HIDEWINDOW
|
||||
Hif
|
||||
|
||||
@@ -17,6 +17,7 @@ public class Settings : ISettingsInterface
|
||||
private readonly bool hideKillProcessOnElevatedProcesses;
|
||||
private readonly bool hideExplorerSettingInfo;
|
||||
private readonly bool inMruOrder;
|
||||
private readonly bool useWindowIcon;
|
||||
|
||||
public Settings(
|
||||
bool resultsFromVisibleDesktopOnly = false,
|
||||
@@ -27,7 +28,8 @@ public class Settings : ISettingsInterface
|
||||
bool openAfterKillAndClose = false,
|
||||
bool hideKillProcessOnElevatedProcesses = false,
|
||||
bool hideExplorerSettingInfo = true,
|
||||
bool inMruOrder = true)
|
||||
bool inMruOrder = true,
|
||||
bool useWindowIcon = true)
|
||||
{
|
||||
this.resultsFromVisibleDesktopOnly = resultsFromVisibleDesktopOnly;
|
||||
this.subtitleShowPid = subtitleShowPid;
|
||||
@@ -38,6 +40,7 @@ public class Settings : ISettingsInterface
|
||||
this.hideKillProcessOnElevatedProcesses = hideKillProcessOnElevatedProcesses;
|
||||
this.hideExplorerSettingInfo = hideExplorerSettingInfo;
|
||||
this.inMruOrder = inMruOrder;
|
||||
this.useWindowIcon = useWindowIcon;
|
||||
}
|
||||
|
||||
public bool ResultsFromVisibleDesktopOnly => resultsFromVisibleDesktopOnly;
|
||||
@@ -57,4 +60,6 @@ public class Settings : ISettingsInterface
|
||||
public bool HideExplorerSettingInfo => hideExplorerSettingInfo;
|
||||
|
||||
public bool InMruOrder => inMruOrder;
|
||||
|
||||
public bool UseWindowIcon => useWindowIcon;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Components;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowWalker.Commands;
|
||||
|
||||
@@ -16,9 +21,41 @@ internal sealed partial class SwitchToWindowCommand : InvokableCommand
|
||||
|
||||
public SwitchToWindowCommand(Window? window)
|
||||
{
|
||||
Icon = Icons.GenericAppIcon; // Fallback to default icon
|
||||
Name = Resources.switch_to_command_title;
|
||||
_window = window;
|
||||
if (_window is not null)
|
||||
{
|
||||
// Use window icon
|
||||
if (SettingsManager.Instance.UseWindowIcon)
|
||||
{
|
||||
if (_window.TryGetWindowIcon(out var icon) && icon is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use process icon
|
||||
else
|
||||
{
|
||||
var p = Process.GetProcessById((int)_window.Process.ProcessID);
|
||||
if (p is not null)
|
||||
@@ -34,6 +71,7 @@ internal sealed partial class SwitchToWindowCommand : InvokableCommand
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,4 +23,6 @@ public interface ISettingsInterface
|
||||
public bool HideExplorerSettingInfo { get; }
|
||||
|
||||
public bool InMruOrder { get; }
|
||||
|
||||
public bool UseWindowIcon { get; }
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ public static partial class NativeMethods
|
||||
[DllImport("user32.dll")]
|
||||
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")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr hObject);
|
||||
@@ -143,6 +149,41 @@ public static class Win32Constants
|
||||
/// </summary>
|
||||
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>
|
||||
/// RPC call succeeded
|
||||
/// </summary>
|
||||
|
||||
@@ -70,6 +70,12 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Resources.windowwalker_SettingInMruOrder_Description,
|
||||
true);
|
||||
|
||||
private readonly ToggleSetting _useWindowIcon = new(
|
||||
Namespaced(nameof(UseWindowIcon)),
|
||||
Resources.windowwalker_SettingUseWindowIcon,
|
||||
Resources.windowwalker_SettingUseWindowIcon_Description,
|
||||
true);
|
||||
|
||||
public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value;
|
||||
|
||||
public bool SubtitleShowPid => _subtitleShowPid.Value;
|
||||
@@ -88,6 +94,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
public bool InMruOrder => _inMruOrder.Value;
|
||||
|
||||
public bool UseWindowIcon => _useWindowIcon.Value;
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
@@ -110,6 +118,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_hideKillProcessOnElevatedProcesses);
|
||||
Settings.Add(_hideExplorerSettingInfo);
|
||||
Settings.Add(_inMruOrder);
|
||||
Settings.Add(_useWindowIcon);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
@@ -15,4 +15,6 @@ internal sealed class Icons
|
||||
internal static IconInfo CloseWindow { get; } = new IconInfo("\uE894"); // Clear
|
||||
|
||||
internal static IconInfo Info { get; } = new IconInfo("\uE946"); // Info
|
||||
|
||||
internal static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon
|
||||
}
|
||||
|
||||
@@ -401,5 +401,23 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,4 +235,10 @@
|
||||
<data name="windowwalker_NoResultsMessage" xml:space="preserve">
|
||||
<value>No open windows found</value>
|
||||
</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>
|
||||
Reference in New Issue
Block a user