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)),
[]);
}