diff --git a/VdLabel/DesktopCatalog.xaml b/VdLabel/DesktopCatalog.xaml index 63feb99..4395d47 100644 --- a/VdLabel/DesktopCatalog.xaml +++ b/VdLabel/DesktopCatalog.xaml @@ -50,13 +50,32 @@ + + + + + + + + + + + + desktops = []; @@ -28,11 +31,13 @@ internal sealed partial class DesktopCatalogViewModel : ObservableObject, IDispo [ObservableProperty] private double height; - public DesktopCatalogViewModel(IVirualDesktopService virualDesktopService, ICommandService commandService, IConfigStore configStore) + public DesktopCatalogViewModel(IVirualDesktopService virualDesktopService, ICommandService commandService, IConfigStore configStore, IWindowMonitor windowMonitor, IWindowIconCache windowIconCache) { this.virualDesktopService = virualDesktopService; this.commandService = commandService; this.configStore = configStore; + this.windowMonitor = windowMonitor; + this.windowIconCache = windowIconCache; this.configStore.Saved += ConfigStore_Saved; this.commandService.BadgeResultsUpdated += CommandService_BadgeResultsUpdated; this.maxColumns = (int)(SystemParameters.PrimaryScreenWidth * 0.8 / 280); @@ -69,7 +74,11 @@ private async void Setup() var cached = this.commandService.GetBadgeResult(b.Id, c.Id); return cached ?? new ResolvedBadge(b.Label, b.Color); }); - return new DesktopViewModel(i + 1, c, commandLabel, wallpaperPath, pos, resolvedBadges, ToggleBadgeAsync); + var windowIcons = this.windowMonitor.GetDesktopWindows(c.Id) + .Select(this.windowIconCache.Get) + .OfType() + .ToArray(); + return new DesktopViewModel(i + 1, c, commandLabel, wallpaperPath, pos, resolvedBadges, windowIcons, ToggleBadgeAsync); }) .ToArray(); var currentDesktop = this.virualDesktopService.GetCurrent(); @@ -121,7 +130,7 @@ partial void OnLeftChanged(double value) => this.Left = (SystemParameters.PrimaryScreenWidth - this.Width) / 2; } -internal class DesktopViewModel(int index, DesktopConfig desktopConfig, string? commandLabel, string? wallpaperPath, Dock pos, IReadOnlyDictionary resolvedBadges, Func toggleBadge) +internal class DesktopViewModel(int index, DesktopConfig desktopConfig, string? commandLabel, string? wallpaperPath, Dock pos, IReadOnlyDictionary resolvedBadges, IReadOnlyList windowIcons, Func toggleBadge) { private readonly int index = index; private DesktopConfig desktopConfig = desktopConfig; @@ -141,6 +150,8 @@ internal class DesktopViewModel(int index, DesktopConfig desktopConfig, string? .Select(id => resolvedBadges[id]) .ToArray(); + public IReadOnlyList WindowIcons { get; } = windowIcons; + public IReadOnlyList BadgeMenuItems { get; } = resolvedBadges .Select(kvp => new BadgeMenuItem(kvp.Key, kvp.Value, desktopConfig.Id, desktopConfig.BadgeIds.Contains(kvp.Key), toggleBadge)) .ToArray(); diff --git a/VdLabel/Program.cs b/VdLabel/Program.cs index 44b1f51..6411b0a 100644 --- a/VdLabel/Program.cs +++ b/VdLabel/Program.cs @@ -15,12 +15,15 @@ .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddHostedService(sp => sp.GetRequiredService()) .AddHostedService(sp => sp.GetRequiredService()) .AddHostedService(sp => sp.GetRequiredService()) - .AddHostedService() + .AddHostedService(sp => sp.GetRequiredService()) .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton() .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton() .AddSingleton() diff --git a/VdLabel/WindowIconCache.cs b/VdLabel/WindowIconCache.cs new file mode 100644 index 0000000..15504e7 --- /dev/null +++ b/VdLabel/WindowIconCache.cs @@ -0,0 +1,43 @@ +using System.Collections.Concurrent; +using System.Drawing; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace VdLabel; + +interface IWindowIconCache +{ + ImageSource? Get(string processPath); +} + +sealed class WindowIconCache : IWindowIconCache +{ + private readonly ConcurrentDictionary> icons = new(StringComparer.OrdinalIgnoreCase); + + public ImageSource? Get(string processPath) + => this.icons.GetOrAdd(processPath, static path => new(() => Extract(path))).Value; + + private static ImageSource? Extract(string processPath) + { + try + { + using var icon = Icon.ExtractAssociatedIcon(processPath); + if (icon is null) + { + return null; + } + var source = Imaging.CreateBitmapSourceFromHIcon( + icon.Handle, + Int32Rect.Empty, + BitmapSizeOptions.FromWidthAndHeight(64, 64)); + source.Freeze(); + return source; + } + catch (Exception) + { + return null; + } + } +} diff --git a/VdLabel/WindowMonitor.cs b/VdLabel/WindowMonitor.cs index 3b3fad2..9af0cff 100644 --- a/VdLabel/WindowMonitor.cs +++ b/VdLabel/WindowMonitor.cs @@ -3,26 +3,32 @@ using PInvoke; using System.Text.RegularExpressions; using WindowsDesktop; -using static Windows.Win32.PInvoke; using static VdLabel.ProcessUtility; namespace VdLabel; -class WindowMonitor(ILogger logger, IConfigStore configStore) : BackgroundService + +class WindowMonitor(ILogger logger, IConfigStore configStore, App app) : BackgroundService, IWindowMonitor { private readonly ILogger logger = logger; private readonly IConfigStore configStore = configStore; + private readonly App app = app; private readonly Dictionary checkedWindows = []; private bool needReload = true; private TargetWindow[] targetWindows = []; + private Dictionary> desktopWindows = []; private record TargetWindow(Guid DesktopId, WindowMatchType MatchType, Regex Regex); + public IReadOnlyList GetDesktopWindows(Guid desktopId) + => this.desktopWindows.TryGetValue(desktopId, out var windows) ? windows : []; + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); this.logger.LogInformation("ウィンドウ監視開始"); await ReloadTargetProcess().ConfigureAwait(false); this.configStore.Saved += ConfigStore_Saved; + await this.app.WaitForStartupAsync().ConfigureAwait(false); while (!stoppingToken.IsCancellationRequested) { @@ -67,11 +73,8 @@ private void CheckWindows() { var now = DateTime.Now; this.logger.LogDebug("ウィンドウチェック開始"); - if (this.targetWindows.Length == 0) - { - return; - } var windows = new HashSet(); + var desktopWindows = new Dictionary>(); User32.EnumWindows((hWnd, lParam) => { windows.Add(hWnd); @@ -93,6 +96,27 @@ private void CheckWindows() return true; } + if (GetProcessPath(processId) is not { } path) + { + return true; + } + + if (VirtualDesktop.FromHwnd(hWnd) is not { } desktop) + { + return true; + } + if (!desktopWindows.TryGetValue(desktop.Id, out var processPaths)) + { + processPaths = []; + desktopWindows.Add(desktop.Id, processPaths); + } + processPaths.Add(path); + + if (this.targetWindows.Length == 0) + { + return true; + } + // ウィンドウタイトルが取得できない場合はスキップ if (GetWindowTitle(hWnd) is not { } windowTitle) { @@ -106,12 +130,6 @@ private void CheckWindows() } // ウィンドウが所属するプロセスのパスが取得できない場合はスキップ - if (GetProcessPath(processId) is not { } path) - { - this.checkedWindows[hWnd] = windowTitle; - return true; - } - // ウィンドウが所属するプロセスのコマンドラインが取得できない場合はスキップ if (GetCommandLine(processId) is not { } commandLine) { @@ -135,12 +153,12 @@ private void CheckWindows() VirtualDesktop.PinWindow(hWnd); } } - else if (VirtualDesktop.FromId(target.DesktopId) is { } desktop) + else if (VirtualDesktop.FromId(target.DesktopId) is { } targetDesktop) { - if (VirtualDesktop.FromHwnd(hWnd)?.Id != desktop.Id) + if (VirtualDesktop.FromHwnd(hWnd)?.Id != targetDesktop.Id) { - this.logger.LogDebug($"ウィンドウ検出: {windowTitle} to {desktop.Name}"); - VirtualDesktop.MoveToDesktop(hWnd, desktop); + this.logger.LogDebug($"ウィンドウ検出: {windowTitle} to {targetDesktop.Name}"); + VirtualDesktop.MoveToDesktop(hWnd, targetDesktop); } } @@ -159,6 +177,7 @@ private void CheckWindows() { this.checkedWindows.Remove(hWnd); } + this.desktopWindows = desktopWindows; this.logger.LogDebug($"ウィンドウチェック終了: {DateTime.Now - now}"); } @@ -171,3 +190,8 @@ private static string GetCheckText(WindowMatchType type, string path, string com _ => string.Empty, }; } + +interface IWindowMonitor +{ + IReadOnlyList GetDesktopWindows(Guid desktopId); +}