From f26c3a306022866f27c67a5619c94d2942678fed Mon Sep 17 00:00:00 2001 From: dlprows Date: Wed, 4 Oct 2023 22:24:31 -0600 Subject: [PATCH] Refactor how icons work, and made java icons work a little better --- .../AudioHelpers/NameAndIconHelper.cs | 26 ++++- .../ActiveAudioSessionWrapper.cs | 36 +----- .../AudioSessions/IconWrapper.cs | 107 ++++++++++++++++++ .../FocusVolumeControl.csproj | 2 + .../UI/JavaIconExtractor.cs | 79 +++++++++++++ src/SoundBrowser/MainWindow.xaml.cs | 5 +- 6 files changed, 217 insertions(+), 38 deletions(-) create mode 100644 src/FocusVolumeControl/AudioSessions/IconWrapper.cs create mode 100644 src/FocusVolumeControl/UI/JavaIconExtractor.cs diff --git a/src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs b/src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs index 50553d6..87c2002 100644 --- a/src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs +++ b/src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs @@ -1,7 +1,10 @@ using BarRaider.SdTools; +using FocusVolumeControl.AudioSession; using FocusVolumeControl.AudioSessions; +using FocusVolumeControl.UI; using System; using System.Diagnostics; +using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Text; @@ -10,12 +13,12 @@ namespace FocusVolumeControl.AudioHelpers; public class NameAndIconHelper { - public (string name, string icon) GetProcessInfo(Process process) + public string GetProcessInfo(Process process) { //i know this is dumb, but its only used by the sound browser, not real prod code var blah = new ActiveAudioSessionWrapper(); SetProcessInfo(process, blah); - return (blah.DisplayName, blah.IconPath ?? blah.ExecutablePath); + return blah.DisplayName; } public void SetProcessInfo(Process process, ActiveAudioSessionWrapper results) @@ -45,12 +48,27 @@ public class NameAndIconHelper } } - results.ExecutablePath = fileVersionInfo?.FileName; + //for java apps (minecraft), the process will just have a java icon + //and there's not just a file that you can get the real icon from + //so you have to send some messages to the apps to get the icons. + //but they will only be 32x32 (or smaller) so we only want to use this logic for java + //because these will be lower resolution than the normal way of getting icons + if (process.ProcessName == "javaw" || process.ProcessName == "java") + { + var windowHandle = process.MainWindowHandle; + var lazyIcon = new Lazy(() => JavaIconExtractor.GetWindowBigIcon(windowHandle)); + results.IconWrapper = new RawIcon(windowHandle.ToString(), lazyIcon); + + } + else + { + results.IconWrapper = new NormalIcon(fileVersionInfo?.FileName); + } } else { results.DisplayName = appx.DisplayName; - results.IconPath = Path.Combine(appx.Path, appx.Logo); + results.IconWrapper = new AppxIcon(Path.Combine(appx.Path, appx.Logo)); } } catch { } diff --git a/src/FocusVolumeControl/AudioSessions/ActiveAudioSessionWrapper.cs b/src/FocusVolumeControl/AudioSessions/ActiveAudioSessionWrapper.cs index a8417de..711fb07 100644 --- a/src/FocusVolumeControl/AudioSessions/ActiveAudioSessionWrapper.cs +++ b/src/FocusVolumeControl/AudioSessions/ActiveAudioSessionWrapper.cs @@ -6,55 +6,27 @@ using System.Drawing; using System.Runtime.InteropServices; using FocusVolumeControl.UI; using BitFaster.Caching.Lru; +using FocusVolumeControl.AudioSession; namespace FocusVolumeControl.AudioSessions; public sealed class ActiveAudioSessionWrapper : IAudioSession { - static ConcurrentLru _iconCache = new ConcurrentLru(10); - public string DisplayName { get; set; } - public string ExecutablePath { get; set; } - public string IconPath { get; set; } private List Sessions { get; } = new List(); private IEnumerable Volume => Sessions.Cast(); - string GetIconFromIconPath() - { - return _iconCache.GetOrAdd(IconPath, (key) => - { - var tmp = (Bitmap)Bitmap.FromFile(IconPath); - tmp.MakeTransparent(); - return Tools.ImageToBase64(tmp, true); - }); - } - - string GetIconFromExecutablePath() - { - return _iconCache.GetOrAdd(ExecutablePath, (key) => - { - var tmp = IconExtraction.GetIcon(ExecutablePath); - //var tmp = Icon.ExtractAssociatedIcon(ExecutablePath); - return Tools.ImageToBase64(tmp, true); - }); - } + public IconWrapper? IconWrapper { get; set; } public string GetIcon() { try { - if (!string.IsNullOrEmpty(IconPath)) - { - return GetIconFromIconPath(); - } - else - { - return GetIconFromExecutablePath(); - } + return IconWrapper?.GetIconData(); } catch { - return "Images/encoderIcon"; + return IconWrapper.FallbackIconData; } } diff --git a/src/FocusVolumeControl/AudioSessions/IconWrapper.cs b/src/FocusVolumeControl/AudioSessions/IconWrapper.cs new file mode 100644 index 0000000..4f3bbc0 --- /dev/null +++ b/src/FocusVolumeControl/AudioSessions/IconWrapper.cs @@ -0,0 +1,107 @@ +using BarRaider.SdTools; +using BitFaster.Caching.Lru; +using FocusVolumeControl.UI; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FocusVolumeControl.AudioSession +{ + public abstract class IconWrapper + { + protected static ConcurrentLru _iconCache = new ConcurrentLru(10); + + public abstract string GetIconData(); + + internal const string FallbackIconData = "Images/encoderIcon"; + } + + internal class AppxIcon : IconWrapper + { + private readonly string _iconPath; + + public AppxIcon(string iconPath) + { + _iconPath = iconPath; + } + + public override string GetIconData() + { + if(string.IsNullOrEmpty(_iconPath)) + { + return FallbackIconData; + } + + return _iconCache.GetOrAdd(_iconPath, (key) => + { + var tmp = (Bitmap)Bitmap.FromFile(_iconPath); + tmp.MakeTransparent(); + return Tools.ImageToBase64(tmp, true); + }); + } + + } + + internal class NormalIcon : IconWrapper + { + private readonly string _iconPath; + + public NormalIcon(string iconPath) + { + _iconPath = iconPath; + } + + public override string GetIconData() + { + if(string.IsNullOrEmpty(_iconPath)) + { + return FallbackIconData; + } + + return _iconCache.GetOrAdd(_iconPath, (key) => + { + var tmp = IconExtraction.GetIcon(_iconPath); + return Tools.ImageToBase64(tmp, true); + }); + } + } + + internal class RawIcon : IconWrapper + { + private readonly string _data; + + public RawIcon(string name, Lazy lazyIcon) + { + _data = _iconCache.GetOrAdd(name, (key) => + { + var icon = lazyIcon.Value; + if (icon == null) + { + return FallbackIconData; + } + if (icon.Height < 48 && icon.Width < 48) + { + using var newImage = new Bitmap(48, 48); + newImage.MakeTransparent(); + using var graphics = Graphics.FromImage(newImage); + + graphics.DrawImage(icon, 4, 4, 40, 40); + + + return Tools.ImageToBase64(newImage, true); + } + else + { + return Tools.ImageToBase64(icon, true); + } + }); + } + + public override string GetIconData() => _data; + } + +} diff --git a/src/FocusVolumeControl/FocusVolumeControl.csproj b/src/FocusVolumeControl/FocusVolumeControl.csproj index 1f48d56..e124d61 100644 --- a/src/FocusVolumeControl/FocusVolumeControl.csproj +++ b/src/FocusVolumeControl/FocusVolumeControl.csproj @@ -55,6 +55,7 @@ + @@ -71,6 +72,7 @@ + diff --git a/src/FocusVolumeControl/UI/JavaIconExtractor.cs b/src/FocusVolumeControl/UI/JavaIconExtractor.cs new file mode 100644 index 0000000..2c3928c --- /dev/null +++ b/src/FocusVolumeControl/UI/JavaIconExtractor.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace FocusVolumeControl.UI +{ + internal class JavaIconExtractor + { + const int WM_GETICON = 0x7F; + const int WM_QUERYDRAGICON = 0x0037; + //const int ICON_SMALL = 0; //(16x16) + const int ICON_BIG = 1; //(32x32) + const int SMTO_ABORTIFHUNG = 0x3; + const int GCL_HICON = -14; + + [DllImport("User32.dll")] + static extern int SendMessageTimeout(IntPtr hWnd, int uMsg, int wParam, int lParam, int fuFlags, int uTimeout, out int lpdwResult); + [DllImport("User32.dll")] + static extern int GetClassLong(IntPtr hWnd, int index); + + [DllImport("user32.dll", EntryPoint = "DestroyIcon", SetLastError = true)] + public static extern int DestroyIcon(IntPtr hIcon); + + /// + /// Retrieves a big icon (32*32) of a window + /// + /// + /// + public static Bitmap? GetWindowBigIcon(IntPtr hWnd) + { + IntPtr hIcon = IntPtr.Zero; + try + { + int result; + SendMessageTimeout(hWnd, WM_GETICON, ICON_BIG, //big icon size + 0, SMTO_ABORTIFHUNG, 1000, out result); + + hIcon = new IntPtr(result); + if (hIcon == IntPtr.Zero) //some applications don't respond to sendmessage, we have to use GetClassLong in that case + { + result = GetClassLong(hWnd, GCL_HICON); //big icon size + hIcon = new IntPtr(result); + } + + if (hIcon == IntPtr.Zero) + { + SendMessageTimeout(hWnd, WM_QUERYDRAGICON, 0, 0, SMTO_ABORTIFHUNG, 1000, out result); + hIcon = new IntPtr(result); + } + + if (hIcon == IntPtr.Zero) + { + return null; + } + else + { + using var tmp = (Icon)Icon.FromHandle(hIcon).Clone(); + return tmp.ToBitmap(); + } + } + catch (Exception) + { + } + finally + { + if(hIcon != IntPtr.Zero) + { + DestroyIcon(hIcon); + } + } + return null; + } + + } +} diff --git a/src/SoundBrowser/MainWindow.xaml.cs b/src/SoundBrowser/MainWindow.xaml.cs index e4b7f81..8acf88a 100644 --- a/src/SoundBrowser/MainWindow.xaml.cs +++ b/src/SoundBrowser/MainWindow.xaml.cs @@ -57,7 +57,7 @@ public partial class MainWindow : Window foreach (var p in processes) { - var (displayName, _) = (new NameAndIconHelper()).GetProcessInfo(p); + var displayName = (new NameAndIconHelper()).GetProcessInfo(p); sb.AppendLine($"pid: {p.Id}"); sb.AppendLine($"\tprocessName: {p.ProcessName}"); @@ -112,9 +112,10 @@ public partial class MainWindow : Window sessionEnumerator.GetSession(s, out var session); session.GetProcessId(out var processId); + session.GetIconPath(out var path); var audioProcess = Process.GetProcessById(processId); - var (displayName, _) = (new NameAndIconHelper()).GetProcessInfo(audioProcess); + var displayName = (new NameAndIconHelper()).GetProcessInfo(audioProcess); sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}"); }