diff --git a/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj b/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj index 9832179..0c06ca1 100644 --- a/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj +++ b/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj @@ -8,6 +8,7 @@ + diff --git a/ProcessDoctor.Backend.Core/SystemProcess.cs b/ProcessDoctor.Backend.Core/SystemProcess.cs index 9c1b8f9..e37c01c 100644 --- a/ProcessDoctor.Backend.Core/SystemProcess.cs +++ b/ProcessDoctor.Backend.Core/SystemProcess.cs @@ -1,3 +1,5 @@ +using SkiaSharp; + namespace ProcessDoctor.Backend.Core; public abstract record SystemProcess( @@ -5,4 +7,7 @@ public abstract record SystemProcess( uint? ParentId, string Name, string? CommandLine, - string? ExecutablePath); + string? ExecutablePath) +{ + public abstract SKBitmap ExtractIcon(); +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/Extensions/IconExtensions.cs b/ProcessDoctor.Backend.Windows/Imaging/Extensions/IconExtensions.cs new file mode 100644 index 0000000..7f98be3 --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/Extensions/IconExtensions.cs @@ -0,0 +1,19 @@ +using System.Drawing; +using System.Drawing.Imaging; +using SkiaSharp; + +namespace ProcessDoctor.Backend.Windows.Imaging.Extensions; + +public static class IconExtensions +{ + public static SKBitmap ToSkBitmap(this Icon icon) + { + using var nativeBitmap = icon.ToBitmap(); + + using var stream = new MemoryStream(); + nativeBitmap.Save(stream, ImageFormat.Bmp); + stream.Position = 0; + + return SKBitmap.Decode(stream); + } +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/IconFlags.cs b/ProcessDoctor.Backend.Windows/Imaging/IconFlags.cs new file mode 100644 index 0000000..8327cc4 --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/IconFlags.cs @@ -0,0 +1,9 @@ +namespace ProcessDoctor.Backend.Windows.Imaging; + +[Flags] +internal enum IconFlags : uint +{ + LargeSize = 0x0, + SmallSize = 0x1, + Icon = 0x100 +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/IconType.cs b/ProcessDoctor.Backend.Windows/Imaging/IconType.cs new file mode 100644 index 0000000..11631d4 --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/IconType.cs @@ -0,0 +1,6 @@ +namespace ProcessDoctor.Backend.Windows.Imaging; + +internal enum IconType : uint +{ + Application = 0x2 +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/Native/DestroyIconSafeHandle.cs b/ProcessDoctor.Backend.Windows/Imaging/Native/DestroyIconSafeHandle.cs new file mode 100644 index 0000000..c2447df --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/Native/DestroyIconSafeHandle.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace ProcessDoctor.Backend.Windows.Imaging.Native; + +internal sealed class DestroyIconSafeHandle : SafeHandle +{ + private static readonly IntPtr InvalidValue = new(-1L); + + internal DestroyIconSafeHandle() + : base(InvalidValue, ownsHandle: true) + { } + + internal DestroyIconSafeHandle(IntPtr preexistingHandle, bool ownsHandle = true) + : base(InvalidValue, ownsHandle) + => SetHandle(preexistingHandle); + + public override bool IsInvalid + => handle.ToInt64() == -1L || handle.ToInt64() == 0L; + + protected override bool ReleaseHandle() + => User32.DestroyIcon(handle); +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/Native/HRESULT.cs b/ProcessDoctor.Backend.Windows/Imaging/Native/HRESULT.cs new file mode 100644 index 0000000..98322e7 --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/Native/HRESULT.cs @@ -0,0 +1,16 @@ +namespace ProcessDoctor.Backend.Windows.Imaging.Native; + +internal readonly struct HRESULT +{ + private readonly int _value; + + internal HRESULT(int value) + => _value = value; + + internal bool Succeeded + => _value >= 0; + + + internal bool Failed + => _value < 0; +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/Native/SH_STOCK_ICON_INFO.cs b/ProcessDoctor.Backend.Windows/Imaging/Native/SH_STOCK_ICON_INFO.cs new file mode 100644 index 0000000..5181dd8 --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/Native/SH_STOCK_ICON_INFO.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace ProcessDoctor.Backend.Windows.Imaging.Native; + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct SH_STOCK_ICON_INFO +{ + public uint cbSize; + + public IntPtr hIcon; + + public int iSysIconIndex; + + public int iIcon; + + public fixed char szPath[260]; +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/Native/Shell32.cs b/ProcessDoctor.Backend.Windows/Imaging/Native/Shell32.cs new file mode 100644 index 0000000..f90ecfa --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/Native/Shell32.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace ProcessDoctor.Backend.Windows.Imaging.Native; + +internal static class Shell32 +{ + [DllImport("shell32.dll")] + public static extern HRESULT SHGetStockIconInfo(IconType siid, IconFlags uFlags, ref SH_STOCK_ICON_INFO psii); +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/Native/User32.cs b/ProcessDoctor.Backend.Windows/Imaging/Native/User32.cs new file mode 100644 index 0000000..b5d58ce --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/Native/User32.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace ProcessDoctor.Backend.Windows.Imaging.Native; + +internal static class User32 +{ + [DllImport("user32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool DestroyIcon(IntPtr hIcon); +} diff --git a/ProcessDoctor.Backend.Windows/Imaging/StockIcon.cs b/ProcessDoctor.Backend.Windows/Imaging/StockIcon.cs new file mode 100644 index 0000000..83508f0 --- /dev/null +++ b/ProcessDoctor.Backend.Windows/Imaging/StockIcon.cs @@ -0,0 +1,34 @@ +using System.Drawing; +using System.Runtime.InteropServices; +using ProcessDoctor.Backend.Windows.Imaging.Native; + +namespace ProcessDoctor.Backend.Windows.Imaging; + +internal static class StockIcon +{ + private const string ErrorMessage = "An error occured while creating the stock icon: {0}"; + + internal static Icon Create(IconType type) + { + var iconInformation = new SH_STOCK_ICON_INFO(); + iconInformation.cbSize = (uint)Marshal.SizeOf(iconInformation); + + var result = Shell32.SHGetStockIconInfo(type, IconFlags.Icon | IconFlags.SmallSize, ref iconInformation); + + if (result.Failed) + { + throw new InvalidOperationException( + string.Format(ErrorMessage, type)); + } + + using var iconHandle = new DestroyIconSafeHandle(iconInformation.hIcon, ownsHandle: true); + + if (iconHandle.IsInvalid) + { + throw new InvalidOperationException( + string.Format(ErrorMessage, type)); + } + + return (Icon)Icon.FromHandle(iconInformation.hIcon).Clone(); + } +} diff --git a/ProcessDoctor.Backend.Windows/ProcessDoctor.Backend.Windows.csproj b/ProcessDoctor.Backend.Windows/ProcessDoctor.Backend.Windows.csproj index 2e46605..192a8b9 100644 --- a/ProcessDoctor.Backend.Windows/ProcessDoctor.Backend.Windows.csproj +++ b/ProcessDoctor.Backend.Windows/ProcessDoctor.Backend.Windows.csproj @@ -13,6 +13,7 @@ + diff --git a/ProcessDoctor.Backend.Windows/WindowsProcess.cs b/ProcessDoctor.Backend.Windows/WindowsProcess.cs index 06f9604..aadc739 100644 --- a/ProcessDoctor.Backend.Windows/WindowsProcess.cs +++ b/ProcessDoctor.Backend.Windows/WindowsProcess.cs @@ -1,8 +1,12 @@ +using System.Drawing; using System.Management; using PInvoke; using ProcessDoctor.Backend.Core; +using ProcessDoctor.Backend.Windows.Imaging; +using ProcessDoctor.Backend.Windows.Imaging.Extensions; using ProcessDoctor.Backend.Windows.NT; using ProcessDoctor.Backend.Windows.NT.Extensions; +using SkiaSharp; namespace ProcessDoctor.Backend.Windows; @@ -68,4 +72,21 @@ private static unsafe WindowsProcess Create(Kernel32.PROCESSENTRY32 processEntry private WindowsProcess(uint id, uint? parentId, string name, string? commandLine, string? executablePath) : base(id, parentId, name, commandLine, executablePath) { } + + public override SKBitmap ExtractIcon() + { + if (string.IsNullOrWhiteSpace(ExecutablePath)) + { + return ExtractStockIcon(); + } + + using var icon = Icon.ExtractAssociatedIcon(ExecutablePath); + return icon?.ToSkBitmap() ?? ExtractStockIcon(); + } + + private SKBitmap ExtractStockIcon() + { + using var stockIcon = StockIcon.Create(IconType.Application); + return stockIcon.ToSkBitmap(); + } } diff --git a/ProcessDoctor.TestFramework/FakeProcess.cs b/ProcessDoctor.TestFramework/FakeProcess.cs index 2d7a647..cae3581 100644 --- a/ProcessDoctor.TestFramework/FakeProcess.cs +++ b/ProcessDoctor.TestFramework/FakeProcess.cs @@ -1,4 +1,5 @@ using ProcessDoctor.Backend.Core; +using SkiaSharp; namespace ProcessDoctor.TestFramework; @@ -8,4 +9,7 @@ public sealed record FakeProcess : SystemProcess public FakeProcess(uint id, uint? parentId, string name, string? commandLine, string? executablePath) : base(id, parentId, name, commandLine, executablePath) { } + + public override SKBitmap ExtractIcon() + => throw new NotImplementedException(); } diff --git a/ProcessDoctor/Imaging/Extensions/ProcessModelExtensions.cs b/ProcessDoctor/Imaging/Extensions/ProcessModelExtensions.cs deleted file mode 100644 index e0063a8..0000000 --- a/ProcessDoctor/Imaging/Extensions/ProcessModelExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Threading.Tasks; -using ProcessDoctor.Backend.Core; -using Bitmap = Avalonia.Media.Imaging.Bitmap; - -namespace ProcessDoctor.Imaging.Extensions; - -public static class ProcessModelExtensions -{ - public static Task ExtractAssociatedBitmapAsync(this SystemProcess process) - { - if (string.IsNullOrWhiteSpace(process.ExecutablePath)) - { - return Task.FromResult(null); - } - - return Task.Run(() => - { - var nativeBitmap = Icon - .ExtractAssociatedIcon(process.ExecutablePath)? - .ToBitmap(); - - if (nativeBitmap is null) - { - return null; - } - - using var stream = new MemoryStream(); - nativeBitmap.Save(stream, ImageFormat.Bmp); - stream.Position = 0; - - return new Bitmap(stream); - }); - } -} diff --git a/ProcessDoctor/ProcessDoctor.csproj b/ProcessDoctor/ProcessDoctor.csproj index a008829..7b15dc5 100644 --- a/ProcessDoctor/ProcessDoctor.csproj +++ b/ProcessDoctor/ProcessDoctor.csproj @@ -14,6 +14,7 @@ + diff --git a/ProcessDoctor/ViewModels/MainWindowViewModel.cs b/ProcessDoctor/ViewModels/MainWindowViewModel.cs index 6a36873..523d516 100644 --- a/ProcessDoctor/ViewModels/MainWindowViewModel.cs +++ b/ProcessDoctor/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Templates; @@ -11,8 +10,7 @@ using ProcessDoctor.Backend.Core.Interfaces; using ProcessDoctor.Backend.Windows; using ProcessDoctor.Backend.Windows.WMI; -using ReactiveUI; -using Image = Avalonia.Controls.Image; +using SkiaImageView; namespace ProcessDoctor.ViewModels; @@ -99,9 +97,9 @@ static Grid BuildNameControl(ProcessViewModel? viewModel) return grid; } - static Image BuildImageControl(ProcessViewModel? viewModel) + static SKImageView BuildImageControl(ProcessViewModel? viewModel) { - var image = new Image + var image = new SKImageView { Width = 16.0, Height = 16.0, @@ -115,8 +113,8 @@ static Image BuildImageControl(ProcessViewModel? viewModel) } image.Bind( - Image.SourceProperty, - viewModel.Image.ToObservable(RxApp.TaskpoolScheduler)); + SKImageView.SourceProperty, + viewModel.Image); return image; } diff --git a/ProcessDoctor/ViewModels/ProcessViewModel.cs b/ProcessDoctor/ViewModels/ProcessViewModel.cs index 2a743de..326be8d 100644 --- a/ProcessDoctor/ViewModels/ProcessViewModel.cs +++ b/ProcessDoctor/ViewModels/ProcessViewModel.cs @@ -1,8 +1,9 @@ +using System; using System.Collections.ObjectModel; +using System.Reactive.Linq; using System.Threading.Tasks; using ProcessDoctor.Backend.Core; -using ProcessDoctor.Imaging.Extensions; -using Bitmap = Avalonia.Media.Imaging.Bitmap; +using SkiaSharp; namespace ProcessDoctor.ViewModels; @@ -10,13 +11,13 @@ public record ProcessViewModel( uint Id, string Name, string? CommandLine, - Task Image, + IObservable Image, ObservableCollection Children) { public static ProcessViewModel Of(SystemProcess model) => new( model.Id, model.Name, model.CommandLine, - model.ExtractAssociatedBitmapAsync(), + Observable.FromAsync(() => Task.Run(model.ExtractIcon)), []); }