Skip to content

Commit

Permalink
Merge pull request #22 from y0ung3r/feature/19.default-icon
Browse files Browse the repository at this point in the history
(#19) Add extraction of the stock icon for processes without an executable path
  • Loading branch information
ForNeVeR authored Mar 14, 2024
2 parents db11e9f + b434d12 commit 186ba7c
Show file tree
Hide file tree
Showing 18 changed files with 185 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="JetBrains.Lifetimes" Version="2023.3.3" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>

Expand Down
7 changes: 6 additions & 1 deletion ProcessDoctor.Backend.Core/SystemProcess.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using SkiaSharp;

namespace ProcessDoctor.Backend.Core;

public abstract record SystemProcess(
uint Id,
uint? ParentId,
string Name,
string? CommandLine,
string? ExecutablePath);
string? ExecutablePath)
{
public abstract SKBitmap ExtractIcon();
}
19 changes: 19 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/Extensions/IconExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
9 changes: 9 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/IconFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ProcessDoctor.Backend.Windows.Imaging;

[Flags]
internal enum IconFlags : uint
{
LargeSize = 0x0,
SmallSize = 0x1,
Icon = 0x100
}
6 changes: 6 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/IconType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ProcessDoctor.Backend.Windows.Imaging;

internal enum IconType : uint
{
Application = 0x2
}
Original file line number Diff line number Diff line change
@@ -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);
}
16 changes: 16 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/Native/HRESULT.cs
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/Native/SH_STOCK_ICON_INFO.cs
Original file line number Diff line number Diff line change
@@ -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];
}
9 changes: 9 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/Native/Shell32.cs
Original file line number Diff line number Diff line change
@@ -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);
}
9 changes: 9 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/Native/User32.cs
Original file line number Diff line number Diff line change
@@ -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);
}
34 changes: 34 additions & 0 deletions ProcessDoctor.Backend.Windows/Imaging/StockIcon.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />

<!-- PInvoke is no longer maintained. We should use https://github.com/microsoft/CsWin32 when stable version is released -->
<PackageReference Include="PInvoke.Kernel32" Version="0.7.124" />
Expand Down
21 changes: 21 additions & 0 deletions ProcessDoctor.Backend.Windows/WindowsProcess.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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();
}
}
4 changes: 4 additions & 0 deletions ProcessDoctor.TestFramework/FakeProcess.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ProcessDoctor.Backend.Core;
using SkiaSharp;

namespace ProcessDoctor.TestFramework;

Expand All @@ -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();
}
37 changes: 0 additions & 37 deletions ProcessDoctor/Imaging/Extensions/ProcessModelExtensions.cs

This file was deleted.

1 change: 1 addition & 0 deletions ProcessDoctor/ProcessDoctor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Avalonia.Desktop" Version="11.0.6"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6"/>
<PackageReference Include="SkiaImageView.Avalonia11" Version="1.5.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6"/>
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6"/>
Expand Down
12 changes: 5 additions & 7 deletions ProcessDoctor/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Reactive.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Templates;
Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -115,8 +113,8 @@ static Image BuildImageControl(ProcessViewModel? viewModel)
}

image.Bind(
Image.SourceProperty,
viewModel.Image.ToObservable(RxApp.TaskpoolScheduler));
SKImageView.SourceProperty,
viewModel.Image);

return image;
}
Expand Down
9 changes: 5 additions & 4 deletions ProcessDoctor/ViewModels/ProcessViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
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;

public record ProcessViewModel(
uint Id,
string Name,
string? CommandLine,
Task<Bitmap?> Image,
IObservable<SKBitmap?> Image,
ObservableCollection<ProcessViewModel> Children)
{
public static ProcessViewModel Of(SystemProcess model) => new(
model.Id,
model.Name,
model.CommandLine,
model.ExtractAssociatedBitmapAsync(),
Observable.FromAsync(() => Task.Run(model.ExtractIcon)),
[]);
}

0 comments on commit 186ba7c

Please sign in to comment.