Compare commits

...

2 Commits

Author SHA1 Message Date
Mike Griese
f8733dc8ca iconpathconvert in cs 2025-08-08 15:49:46 -05:00
Yu Leng
e1474c1f30 [Tests] Fix PowerToys.sln to make cmdpal unit tests listed in the Test folder (#40804)
<!-- 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)?
-->
## Summary of the Pull Request
1. Just change the sln to show cmdpal unit test in the Tests folder.
2. Move cmdpal UITest into Tests folder

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

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **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

<!-- 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: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-07-25 15:35:36 +08:00
6 changed files with 477 additions and 3 deletions

View File

@@ -760,7 +760,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.FuzzTests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{988C9FAF-5AEC-EB15-578D-FED0DF52BF55}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UITests", "src\modules\cmdpal\Microsoft.CmdPal.UITests\Microsoft.CmdPal.UITests.csproj", "{6748A29D-DA6A-033A-825B-752295FF6AA0}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UITests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UITests\Microsoft.CmdPal.UITests.csproj", "{6748A29D-DA6A-033A-825B-752295FF6AA0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZones.FuzzTests", "src\modules\fancyzones\FancyZones.FuzzTests\FancyZones.FuzzTests.csproj", "{6EABCF9A-6526-441F-932F-658B1DC3E403}"
EndProject
@@ -776,6 +776,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{8131151D-B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Calc.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Calc.UnitTests\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj", "{E816D7AC-4688-4ECB-97CC-3D8E798F3825}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Registry.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Registry.UnitTests\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj", "{E816D7AD-4688-4ECB-97CC-3D8E798F3826}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.System.UnitTests\Microsoft.CmdPal.Ext.System.UnitTests.csproj", "{E816D7AE-4688-4ECB-97CC-3D8E798F3827}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.TimeDate.UnitTests\Microsoft.CmdPal.Ext.TimeDate.UnitTests.csproj", "{E816D7AF-4688-4ECB-97CC-3D8E798F3828}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WindowWalker.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj", "{E816D7B0-4688-4ECB-97CC-3D8E798F3829}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2790,6 +2798,38 @@ Global
{E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|ARM64.Build.0 = Release|ARM64
{E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|x64.ActiveCfg = Release|x64
{E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|x64.Build.0 = Release|x64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|x64.ActiveCfg = Debug|x64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|x64.Build.0 = Debug|x64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|ARM64.Build.0 = Release|ARM64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|x64.ActiveCfg = Release|x64
{E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|x64.Build.0 = Release|x64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|x64.ActiveCfg = Debug|x64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|x64.Build.0 = Debug|x64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|ARM64.Build.0 = Release|ARM64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|x64.ActiveCfg = Release|x64
{E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|x64.Build.0 = Release|x64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|x64.ActiveCfg = Debug|x64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|x64.Build.0 = Debug|x64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|ARM64.Build.0 = Release|ARM64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|x64.ActiveCfg = Release|x64
{E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|x64.Build.0 = Release|x64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|x64.Build.0 = Debug|x64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|x64.ActiveCfg = Release|x64
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3077,6 +3117,11 @@ Global
{D9BD324E-1D80-44AA-8E7B-73EB00944434} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
{8EF25507-2575-4ADE-BF7E-D23376903AB8} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{070AC093-C9F2-20AD-0BCD-F318FC2761EA} = {B1234567-1234-1234-1234-123456789ABC}
{E816D7AC-4688-4ECB-97CC-3D8E798F3825} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7AD-4688-4ECB-97CC-3D8E798F3826} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7AE-4688-4ECB-97CC-3D8E798F3827} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{2C318EC3-BA86-4372-B1BC-DB0F33C208B2} = {322566EF-20DC-43A6-B9F8-616AF942579A}
{BFFB607F-7C78-434B-86B9-DA4C8196A1B5} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC}
{66E1534A-1587-42B2-912F-45C994D32904} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}

View File

@@ -0,0 +1,429 @@
// Copyright (c) Microsoft Corporation
// 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.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.WindowsAndMessaging;
// This is an unfortunate interaction between CsWin32 and CsWinRT.
// The warning is technically correct, but because you can't edit generated
// files, you can't really fix it. Ideally CsWin32 would generate all types as
// partial, but it doesn't do that today
[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "Type is not passed across the WinRT ABI", Scope = "type", Target = "Windows.Win32.DeleteObjectSafeHandle")]
[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "Type is not passed across the WinRT ABI", Scope = "type", Target = "Windows.Win32.DestroyIconSafeHandle")]
// Relatedly, we added a NativeMethods.json to be able to fix some
// error CA1420: Setting SetLastError to 'true' requires runtime marshalling to be enabled
// errors.
namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// Converts icon paths to IconSource and IconElement objects for WinUI controls.
/// This is a C# port of the C++ IconPathConverter from Microsoft.Terminal.UI.
/// </summary>
public static class IconPathConverter
{
/// <summary>
/// Creates an IconSource for the given path with default settings.
/// </summary>
/// <param name="iconPath">The path to the icon.</param>
/// <returns>An IconSource with its source set, if possible.</returns>
public static IconSource? IconSource(string iconPath, string? fontFamily = null)
{
return IconSource(iconPath, false, fontFamily, 24);
}
/// <summary>
/// Creates an IconSource for the given path.
/// </summary>
/// <param name="iconPath">The path to the icon.</param>
/// <param name="monochrome">Whether to show the icon as monochrome.</param>
/// <param name="targetSize">The target size for the icon.</param>
/// <returns>An IconSource with its source set, if possible.</returns>
public static IconSource? IconSource(string iconPath, bool monochrome, string? fontFamily = null, int targetSize = 24)
{
if (TryGetIconIndex(iconPath, out var iconPathWithoutIndex, out var index))
{
var bitmapSource = GetImageIconSourceForBinary(iconPathWithoutIndex, index, targetSize);
if (bitmapSource != null)
{
var imageIconSource = new ImageIconSource
{
ImageSource = bitmapSource
};
return imageIconSource;
}
}
return GetIconSource(iconPath, monochrome, fontFamily, targetSize);
}
/// <summary>
/// Creates an IconElement for the given path with default size.
/// </summary>
/// <param name="iconPath">The path to the icon.</param>
/// <returns>An IconElement with its IconSource set, if possible.</returns>
public static IconElement Icon(string iconPath)
{
return Icon(iconPath, 24);
}
/// <summary>
/// Creates an IconElement for the given path.
/// </summary>
/// <param name="iconPath">The path to the icon.</param>
/// <param name="targetSize">The target size for the icon.</param>
/// <returns>An IconElement with its IconSource set, if possible.</returns>
public static IconElement Icon(string iconPath, int targetSize)
{
if (TryGetIconIndex(iconPath, out var iconPathWithoutIndex, out var index))
{
var bitmapSource = GetImageIconSourceForBinary(iconPathWithoutIndex, index, targetSize);
if (bitmapSource != null)
{
var icon = new ImageIcon
{
Source = bitmapSource,
Width = targetSize,
Height = targetSize
};
return icon;
}
}
var source = IconSource(iconPath, false, targetSize);
var iconSourceElement = new IconSourceElement
{
IconSource = source
};
return iconSourceElement;
}
/// <summary>
/// Creates an IconSource for the given path.
/// </summary>
/// <param name="iconPath">The path to the icon.</param>
/// <param name="monochrome">Whether to show the icon as monochrome.</param>
/// <param name="targetSize">The target size for the icon.</param>
/// <returns>An IconSource with its source set, if possible.</returns>
private static IconSource? GetIconSource(string iconPath, bool monochrome, string? fontFamily, int targetSize)
{
IconSource? iconSource = null;
if (!string.IsNullOrEmpty(iconPath))
{
var expandedIconPath = ExpandIconPath(iconPath);
iconSource = GetColoredBitmapIcon(expandedIconPath, monochrome);
// If we fail to set the icon source using the "icon" as a path,
// let's try it as a symbol/emoji.
//
// Anything longer than 2 characters isn't an emoji or symbol, so
// don't do this if it's just an invalid path.
if (iconSource == null && iconPath.Length <= 2)
{
try
{
var icon = new FontIconSource();
var ch = iconPath[0];
// The range of MDL2 Icons isn't explicitly defined, but
// we're using this based off the table on:
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
var isMDL2Icon = ch >= '\uE700' && ch <= '\uF8FF';
if (isMDL2Icon)
{
icon.FontFamily = new FontFamily("Segoe Fluent Icons, Segoe MDL2 Assets");
}
else if (fontFamily != null)
{
icon.FontFamily = new FontFamily(fontFamily);
}
else
{
// Note: you do need to manually set the font here.
icon.FontFamily = new FontFamily("Segoe UI");
}
icon.FontSize = targetSize;
icon.Glyph = iconPath;
iconSource = icon;
}
catch
{
// Ignore exceptions when creating font icons
}
}
}
if (iconSource == null)
{
// Set the default IconSource to a BitmapIconSource with a null source
// (instead of just null) because there's a really weird crash when swapping
// data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette).
// Swapping between null IconSources and non-null IconSources causes a crash
// to occur, but swapping between IconSources with a null source and non-null IconSources
// work perfectly fine.
var icon = new BitmapIconSource
{
UriSource = null
};
iconSource = icon;
}
return iconSource;
}
/// <summary>
/// Creates a colored bitmap icon source for the given path.
/// </summary>
/// <param name="path">The full, expanded path to the icon.</param>
/// <param name="monochrome">Whether to show the icon as monochrome.</param>
/// <returns>An IconSource with its source set, if possible.</returns>
private static IconSource? GetColoredBitmapIcon(string path, bool monochrome)
{
// FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters.
// To skip throwing on Uri construction, we can quickly check if the first character is ASCII.
if (!string.IsNullOrEmpty(path) && path[0] < 128)
{
try
{
var iconUri = new Uri(path);
if (Path.GetExtension(path).Equals(".svg", StringComparison.OrdinalIgnoreCase))
{
var iconSource = new ImageIconSource();
var source = new SvgImageSource(iconUri);
iconSource.ImageSource = source;
return iconSource;
}
else
{
var iconSource = new BitmapIconSource
{
// Make sure to set this to false, so we keep the RGB data of the
// image. Otherwise, the icon will be white for all the
// non-transparent pixels in the image.
ShowAsMonochrome = monochrome,
UriSource = iconUri
};
return iconSource;
}
}
catch (UriFormatException)
{
// Ignore exceptions when creating URI-based icons
}
}
return null;
}
/// <summary>
/// Expands environment variables in the icon path.
/// </summary>
/// <param name="iconPath">The path that may contain environment variables.</param>
/// <returns>The expanded path.</returns>
private static string ExpandIconPath(string iconPath)
{
if (string.IsNullOrEmpty(iconPath))
{
return iconPath;
}
// Use Environment.ExpandEnvironmentVariables as the C# equivalent of wil::ExpandEnvironmentStringsW
return Environment.ExpandEnvironmentVariables(iconPath);
}
/// <summary>
/// Attempts to get the icon index from the icon path provided.
/// </summary>
/// <param name="iconPath">The full icon path, including the index if present.</param>
/// <param name="iconPathWithoutIndex">The icon path without the index.</param>
/// <param name="iconIndex">The icon index if present.</param>
/// <returns>True if the iconPath is an exe/dll/lnk file, false otherwise.</returns>
private static bool TryGetIconIndex(string iconPath, out string iconPathWithoutIndex, out int iconIndex)
{
iconPathWithoutIndex = iconPath;
iconIndex = 0;
if (string.IsNullOrEmpty(iconPath))
{
return false;
}
// Does iconPath have a comma in it? If so, split the string on the
// comma and look for the index and extension.
var commaIndex = iconPath.IndexOf(',');
// Split the path on the comma
iconPathWithoutIndex = commaIndex >= 0 ? iconPath.Substring(0, commaIndex) : iconPath;
// It's an exe, dll, or lnk, so we need to extract the icon from the file.
var extension = Path.GetExtension(iconPathWithoutIndex).ToLowerInvariant();
if (extension != ".exe" && extension != ".dll" && extension != ".lnk")
{
return false;
}
if (commaIndex >= 0)
{
// Convert the string iconIndex to a signed int to support negative numbers which represent an Icon's ID.
var indexString = iconPath.Substring(commaIndex + 1);
if (int.TryParse(indexString, out iconIndex))
{
return true;
}
// Failed to parse, return false
return false;
}
// We had a binary path, but no index. Default to 0.
iconIndex = 0;
return true;
}
/// <summary>
/// Gets an image icon source for a binary file (exe, dll, lnk).
/// </summary>
/// <param name="iconPathWithoutIndex">The path to the binary file.</param>
/// <param name="index">The icon index within the file.</param>
/// <param name="targetSize">The target size for the icon.</param>
/// <returns>A SoftwareBitmapSource if successful, null otherwise.</returns>
private static SoftwareBitmapSource? GetImageIconSourceForBinary(string iconPathWithoutIndex, int index, int targetSize)
{
try
{
using var swBitmap = GetBitmapFromIconFile(iconPathWithoutIndex, index, (uint)targetSize);
if (swBitmap == null)
{
return null;
}
var bitmapSource = new SoftwareBitmapSource();
_ = bitmapSource.SetBitmapAsync(swBitmap);
return bitmapSource;
}
catch
{
return null;
}
}
/// <summary>
/// Extracts a bitmap from an icon file using Win32 APIs.
/// This implementation uses SHDefExtractIcon to extract icons from executables, DLLs, and shortcut files.
/// </summary>
/// <param name="iconPath">The path to the icon file.</param>
/// <param name="iconIndex">The index of the icon within the file.</param>
/// <param name="iconSize">The desired icon size.</param>
/// <returns>A SoftwareBitmap if successful, null otherwise.</returns>
private static SoftwareBitmap? GetBitmapFromIconFile(string iconPath, int iconIndex, uint iconSize)
{
try
{
DestroyIconSafeHandle? hIconLarge;
DestroyIconSafeHandle? hIconSmall;
// Extract the icon using SHDefExtractIcon
var result = PInvoke.SHDefExtractIcon(
iconPath,
iconIndex,
0,
out hIconLarge,
out hIconSmall,
iconSize);
using (hIconLarge)
using (hIconSmall)
{
if (result != 0 || hIconLarge.IsInvalid)
{
return null;
}
// For simplicity, convert HICON to bitmap using a more straightforward approach
// This could be enhanced to use WIC directly for better performance
return ConvertHIconToSoftwareBitmap(hIconLarge);
}
}
catch
{
return null;
}
}
/// <summary>
/// Converts an HICON to a SoftwareBitmap using Win32 APIs.
/// </summary>
/// <param name="hIcon">The icon handle to convert.</param>
/// <returns>A SoftwareBitmap if successful, null otherwise.</returns>
private static unsafe SoftwareBitmap? ConvertHIconToSoftwareBitmap(DestroyIconSafeHandle hIcon)
{
// Get icon information
if (!PInvoke.GetIconInfo(hIcon, out var iconInfo))
{
return null;
}
// Create device contexts
var hdcScreen = PInvoke.GetDC(HWND.Null);
var hdcMem = PInvoke.CreateCompatibleDC(hdcScreen);
// Get the icon size (assuming 32x32 for now, could be enhanced to get actual size)
const int iconSize = 32;
// Create a bitmap to draw the icon onto
using var hBitmap = PInvoke.CreateCompatibleBitmap_SafeHandle(hdcScreen, iconSize, iconSize);
using var hOldBitmap = PInvoke.SelectObject(hdcMem, hBitmap);
// Draw the icon onto the bitmap
PInvoke.DrawIconEx(hdcMem, 0, 0, hIcon, iconSize, iconSize, 0, null, (DI_FLAGS)0x0003); // DI_NORMAL
// Get bitmap info
var bitmapInfo = new BITMAPINFO();
bitmapInfo.bmiHeader.biSize = (uint)sizeof(BITMAPINFOHEADER);
bitmapInfo.bmiHeader.biWidth = iconSize;
bitmapInfo.bmiHeader.biHeight = -iconSize; // Top-down DIB
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression = 0; // BI_RGB
// Allocate buffer for pixel data
var pixelDataSize = iconSize * iconSize * 4; // 4 bytes per pixel (BGRA)
var pixelData = new byte[pixelDataSize];
// Get the pixel data - need to pin the byte array
fixed (byte* pixelPtr = pixelData)
{
var result = PInvoke.GetDIBits(
hdcMem,
hBitmap,
0,
iconSize,
pixelPtr,
&bitmapInfo,
0); // DIB_RGB_COLORS
if (result == 0)
{
return null;
}
}
// Create SoftwareBitmap from pixel data
var softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(
pixelData.AsBuffer(),
BitmapPixelFormat.Bgra8,
iconSize,
iconSize,
BitmapAlphaMode.Premultiplied);
return softwareBitmap;
}
}

View File

@@ -1,5 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.UITests</RootNamespace>
@@ -21,6 +21,6 @@
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>