From b5bf17821c39398fac0c11e20c67e217ed73e428 Mon Sep 17 00:00:00 2001 From: Dan Clark Date: Sat, 16 Nov 2024 19:46:25 +0000 Subject: [PATCH] Stuff for tunesync --- Toolkit.Foundation/HandlerInitialization.cs | 10 +- .../IServiceCollectionExtensions.cs | 58 +++--- Toolkit.Foundation/ResponseEventArgs.cs | 2 +- Toolkit.UI.WinUI/Toolkit.UI.WinUI.csproj | 6 + Toolkit.UI.WinUI/WindowExtensions.cs | 97 ++++++++-- Toolkit.UI.WinUI/WindowStyle.cs | 9 - Toolkit.WinUI/DispatcherTimerFactory.cs | 2 +- Toolkit.WinUI/IServiceCollectionExtensions.cs | 1 + Toolkit.WinUI/IWindowRegistry.cs | 12 ++ Toolkit.WinUI/ImageSourceExtensions.cs | 25 +++ Toolkit.WinUI/Toolkit.WinUI.csproj | 1 + Toolkit.WinUI/WinUIDispatcher.cs | 2 +- Toolkit.WinUI/WindowRegistry.cs | 32 +++ Toolkit.Windows/EfficiencyMode.cs | 73 +++++++ Toolkit.Windows/IEfficiencyMode.cs | 9 + Toolkit.Windows/INotifyIcon.cs | 10 + Toolkit.Windows/IPointerMonitor.cs | 2 +- Toolkit.Windows/ITaskbar.cs | 2 +- .../{IWndProcMonitor.cs => IWndProc.cs} | 8 +- Toolkit.Windows/NativeMethods.txt | 26 ++- Toolkit.Windows/NotifyIcon.cs | 183 ++++++++++++++++++ Toolkit.Windows/NotifyIconInvokedEventArgs.cs | 3 + Toolkit.Windows/ProcessPriority.cs | 12 ++ Toolkit.Windows/QualityOfService.cs | 23 +++ Toolkit.Windows/WindowHelper.cs | 4 + .../{WndProcMonitor.cs => WndProc.cs} | 4 +- Toolkit.sln | 4 +- 27 files changed, 562 insertions(+), 58 deletions(-) delete mode 100644 Toolkit.UI.WinUI/WindowStyle.cs create mode 100644 Toolkit.WinUI/IWindowRegistry.cs create mode 100644 Toolkit.WinUI/ImageSourceExtensions.cs create mode 100644 Toolkit.WinUI/WindowRegistry.cs create mode 100644 Toolkit.Windows/EfficiencyMode.cs create mode 100644 Toolkit.Windows/IEfficiencyMode.cs create mode 100644 Toolkit.Windows/INotifyIcon.cs rename Toolkit.Windows/{IWndProcMonitor.cs => IWndProc.cs} (51%) create mode 100644 Toolkit.Windows/NotifyIcon.cs create mode 100644 Toolkit.Windows/NotifyIconInvokedEventArgs.cs create mode 100644 Toolkit.Windows/ProcessPriority.cs create mode 100644 Toolkit.Windows/QualityOfService.cs rename Toolkit.Windows/{WndProcMonitor.cs => WndProc.cs} (95%) diff --git a/Toolkit.Foundation/HandlerInitialization.cs b/Toolkit.Foundation/HandlerInitialization.cs index 885afe5..2748ad2 100644 --- a/Toolkit.Foundation/HandlerInitialization.cs +++ b/Toolkit.Foundation/HandlerInitialization.cs @@ -11,7 +11,15 @@ public class HandlerInitialization(IServiceProvid (provider, args) => args.Reply(provider.GetRequiredService().Handle(args.Message))); } -public class HandlerInitialization(string key, IServiceProvider provider) : +public class HandlerInitialization(IServiceProvider provider) : + IInitialization where THandler : class, IHandler + where TMessage : class +{ + public void Initialize() => WeakReferenceMessenger.Default.Register(provider, + (provider, args) => provider.GetRequiredService().Handle(args)); +} + +public class HandlerKeyedInitialization(string key, IServiceProvider provider) : IInitialization where THandler : class, IHandler where TMessage : class { diff --git a/Toolkit.Foundation/IServiceCollectionExtensions.cs b/Toolkit.Foundation/IServiceCollectionExtensions.cs index fe30f9a..85bc99d 100644 --- a/Toolkit.Foundation/IServiceCollectionExtensions.cs +++ b/Toolkit.Foundation/IServiceCollectionExtensions.cs @@ -4,6 +4,28 @@ namespace Toolkit.Foundation; public static class IServiceCollectionExtensions { + public static IServiceCollection AddAsyncHandler(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Transient) + where THandler : class, IAsyncHandler + where TMessage : class + { + services.Add(new ServiceDescriptor(typeof(THandler), typeof(THandler), lifetime)); + services.AddInitialization>(); + + return services; + } + + public static IServiceCollection AddAsyncHandler(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Transient) + where THandler : class, IAsyncHandler + where TMessage : class + { + services.Add(new ServiceDescriptor(typeof(THandler), typeof(THandler), lifetime)); + services.AddInitialization>(); + + return services; + } + public static IServiceCollection AddAsyncInitialization(this IServiceCollection services) where TInitialization : class, IAsyncInitialization @@ -49,28 +71,6 @@ public static class IServiceCollectionExtensions return services; } - public static IServiceCollection AddAsyncHandler(this IServiceCollection services, - ServiceLifetime lifetime = ServiceLifetime.Transient) - where THandler : class, IAsyncHandler - where TMessage : class - { - services.Add(new ServiceDescriptor(typeof(THandler), typeof(THandler), lifetime)); - services.AddInitialization>(); - - return services; - } - - public static IServiceCollection AddAsyncHandler(this IServiceCollection services, - ServiceLifetime lifetime = ServiceLifetime.Transient) - where THandler : class, IAsyncHandler - where TMessage : class - { - services.Add(new ServiceDescriptor(typeof(THandler), typeof(THandler), lifetime)); - services.AddInitialization>(); - - return services; - } - public static IServiceCollection AddHandler(this IServiceCollection services, string key) where THandler : class, IHandler @@ -85,7 +85,7 @@ public static class IServiceCollectionExtensions if (key is { Length: > 0}) { services.Add(new ServiceDescriptor(typeof(THandler), key, typeof(THandler), lifetime)); - services.AddInitialization>(key); + services.AddInitialization>(key); } else { @@ -96,6 +96,18 @@ public static class IServiceCollectionExtensions return services; } + public static IServiceCollection AddInitialization(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Transient) + where TInitialization : class, + IInitialization + where TInitializationImplementation : class, + TInitialization + { + services.Add(new ServiceDescriptor(typeof(TInitialization), typeof(TInitializationImplementation), lifetime)); + services.AddTransient(provider => provider.GetRequiredService()); + return services; + } + public static IServiceCollection AddInitialization(this IServiceCollection services) where TInitialization : class, IInitialization diff --git a/Toolkit.Foundation/ResponseEventArgs.cs b/Toolkit.Foundation/ResponseEventArgs.cs index f9ab71a..2db175e 100644 --- a/Toolkit.Foundation/ResponseEventArgs.cs +++ b/Toolkit.Foundation/ResponseEventArgs.cs @@ -5,5 +5,5 @@ namespace Toolkit.Foundation; public class ResponseEventArgs : RequestMessage { - public TMessage? Message { get; set; } + public required TMessage Message { get; set; } } \ No newline at end of file diff --git a/Toolkit.UI.WinUI/Toolkit.UI.WinUI.csproj b/Toolkit.UI.WinUI/Toolkit.UI.WinUI.csproj index fea5908..1e8e1cf 100644 --- a/Toolkit.UI.WinUI/Toolkit.UI.WinUI.csproj +++ b/Toolkit.UI.WinUI/Toolkit.UI.WinUI.csproj @@ -11,6 +11,8 @@ true 10.0.19041.41 + true + enable @@ -18,4 +20,8 @@ + + + + diff --git a/Toolkit.UI.WinUI/WindowExtensions.cs b/Toolkit.UI.WinUI/WindowExtensions.cs index 43431fc..ec5ddb7 100644 --- a/Toolkit.UI.WinUI/WindowExtensions.cs +++ b/Toolkit.UI.WinUI/WindowExtensions.cs @@ -1,25 +1,96 @@ +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; -using WinUIEx; +using Windows.Win32; +using Windows.Win32.Foundation; +using WinRT.Interop; +using Windows.Win32.Graphics.Gdi; +using System.Drawing; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; namespace Toolkit.UI.WinUI; public static partial class WindowExtensions { - public static void SetWindowStyle(this Window window, - WindowStyle style) - { - WinUIEx.WindowStyle windowStyle = window.GetWindowStyle(); + private static SUBCLASSPROC? SubClassDelegate; - switch (style) + public static void SetBorderless(this Window window, + bool value) + { + if (window.AppWindow is AppWindow appWindow && + appWindow.Presenter is OverlappedPresenter presenter) { - case WindowStyle.None: - windowStyle &= ~(WinUIEx.WindowStyle.Caption | - WinUIEx.WindowStyle.ThickFrame | - WinUIEx.WindowStyle.Border | - WinUIEx.WindowStyle.SysMenu); - break; + presenter.IsMaximizable = !value; + presenter.IsMinimizable = !value; + presenter.IsResizable = !value; + presenter.SetBorderAndTitleBar(!value, !value); + } + } + + public static void SetTransparency(this Window window, + bool value) + { + nint handle = WindowNative.GetWindowHandle(window); + if (handle == 0) return; + + HWND hWnd = new(handle); + if (value) + { + EnableTransparency(hWnd); + } + else + { + DisableTransparency(hWnd); + } + } + + private static unsafe void EnableTransparency(HWND hWnd) + { + SubClassDelegate = new SUBCLASSPROC(WindowSubClass); + _ = PInvoke.SetWindowSubclass(hWnd, SubClassDelegate, 0, 0); + + WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvoke.GetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + _ = PInvoke.SetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, + (int)(exStyle | WINDOW_EX_STYLE.WS_EX_LAYERED)); + + COLORREF blackColor = new COLORREF((uint)ToWin32(Color.Black)); + _ = PInvoke.SetLayeredWindowAttributes(hWnd, blackColor, 0, LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_COLORKEY | + LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA); + } + + private static unsafe void DisableTransparency(HWND hWnd) + { + if (SubClassDelegate != null) + { + _ = PInvoke.RemoveWindowSubclass(hWnd, SubClassDelegate, 0); + SubClassDelegate = null; } - window.SetWindowStyle(windowStyle); + WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvoke.GetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + _ = PInvoke.SetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)(exStyle & ~WINDOW_EX_STYLE.WS_EX_LAYERED)); + } + + private static int ToWin32(Color c) => c.B << 16 | c.G << 8 | c.R; + + private static unsafe LRESULT WindowSubClass(HWND hWnd, + uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData) + { + switch (uMsg) + { + case PInvoke.WM_ERASEBKGND: + { + RECT rect; + PInvoke.GetClientRect(hWnd, &rect); + + HBRUSH hBrush = PInvoke.CreateSolidBrush(new COLORREF((uint)ToWin32(Color.Black))); + _ = PInvoke.FillRect(new HDC((nint)wParam.Value), + &rect, hBrush); + _ = PInvoke.DeleteObject(new HGDIOBJ(hBrush)); + + return new LRESULT(1); + } + } + + return PInvoke.DefSubclassProc(hWnd, uMsg, wParam, lParam); } } \ No newline at end of file diff --git a/Toolkit.UI.WinUI/WindowStyle.cs b/Toolkit.UI.WinUI/WindowStyle.cs deleted file mode 100644 index a3792ed..0000000 --- a/Toolkit.UI.WinUI/WindowStyle.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Toolkit.UI.WinUI; - -public enum WindowStyle -{ - None, - SingleBorderWindow, - ThreeDBorderWindow, - ToolWindow -} \ No newline at end of file diff --git a/Toolkit.WinUI/DispatcherTimerFactory.cs b/Toolkit.WinUI/DispatcherTimerFactory.cs index 5a1f049..8d066d0 100644 --- a/Toolkit.WinUI/DispatcherTimerFactory.cs +++ b/Toolkit.WinUI/DispatcherTimerFactory.cs @@ -7,4 +7,4 @@ public class DispatcherTimerFactory : { public IDispatcherTimer Create(Action actionDelegate, TimeSpan interval) => new DispatcherTimer(actionDelegate, interval); -} +} \ No newline at end of file diff --git a/Toolkit.WinUI/IServiceCollectionExtensions.cs b/Toolkit.WinUI/IServiceCollectionExtensions.cs index 6f83ef8..f501cc5 100644 --- a/Toolkit.WinUI/IServiceCollectionExtensions.cs +++ b/Toolkit.WinUI/IServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ public static class IServiceCollectionExtensions { services.AddTransient(); services.AddTransient(); + services.AddSingleton(); services.AddTransient((Func>)(provider => new ProxyServiceCollection(services => diff --git a/Toolkit.WinUI/IWindowRegistry.cs b/Toolkit.WinUI/IWindowRegistry.cs new file mode 100644 index 0000000..f39afb1 --- /dev/null +++ b/Toolkit.WinUI/IWindowRegistry.cs @@ -0,0 +1,12 @@ +using Microsoft.UI.Xaml; +using System.Diagnostics.CodeAnalysis; + +namespace Toolkit.WinUI; + +public interface IWindowRegistry +{ + void Add(Window window); + + bool TryGet([DisallowNull] out TWindow? window) + where TWindow : Window; +} diff --git a/Toolkit.WinUI/ImageSourceExtensions.cs b/Toolkit.WinUI/ImageSourceExtensions.cs new file mode 100644 index 0000000..5af6655 --- /dev/null +++ b/Toolkit.WinUI/ImageSourceExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using System.Drawing; +using Windows.Storage; +using Windows.Win32; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Toolkit.WinUI; + +public static class ImageSourceExtensions +{ + public static Icon? ConvertToIcon(this Stream stream, uint dpi) + { + return ExtractIcon(dpi, stream); + } + + private static Icon? ExtractIcon(uint dpi, Stream stream) + { + Bitmap bitmap = (Bitmap)Image.FromStream(stream); + Icon icon = Icon.FromHandle(bitmap.GetHicon()); + + return new Icon(icon, new Size(PInvoke.GetSystemMetricsForDpi(SYSTEM_METRICS_INDEX.SM_CXICON, dpi), + PInvoke.GetSystemMetricsForDpi(SYSTEM_METRICS_INDEX.SM_CYICON, dpi))); + } +} diff --git a/Toolkit.WinUI/Toolkit.WinUI.csproj b/Toolkit.WinUI/Toolkit.WinUI.csproj index c6d27e3..7d84f5a 100644 --- a/Toolkit.WinUI/Toolkit.WinUI.csproj +++ b/Toolkit.WinUI/Toolkit.WinUI.csproj @@ -23,6 +23,7 @@ + diff --git a/Toolkit.WinUI/WinUIDispatcher.cs b/Toolkit.WinUI/WinUIDispatcher.cs index 7ae5f17..dd80281 100644 --- a/Toolkit.WinUI/WinUIDispatcher.cs +++ b/Toolkit.WinUI/WinUIDispatcher.cs @@ -11,4 +11,4 @@ public class WinUIDispatcher : DispatcherQueue.GetForCurrentThread().TryEnqueue(action.Invoke); return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/Toolkit.WinUI/WindowRegistry.cs b/Toolkit.WinUI/WindowRegistry.cs new file mode 100644 index 0000000..c779c03 --- /dev/null +++ b/Toolkit.WinUI/WindowRegistry.cs @@ -0,0 +1,32 @@ +using Microsoft.UI.Xaml; +using System.Diagnostics.CodeAnalysis; + +namespace Toolkit.WinUI; + +public class WindowRegistry : + IWindowRegistry +{ + private readonly List windows = []; + + public void Add(Window window) + { + if (!windows.Contains(window)) + { + void OnWindowClosed(object sender, WindowEventArgs args) + { + window.Closed -= OnWindowClosed; + windows.Remove(window); + } + + windows.Add(window); + window.Closed += OnWindowClosed; + } + } + + public bool TryGet([DisallowNull] out TWindow? window) + where TWindow : Window + { + window = windows.OfType().FirstOrDefault() ?? null; + return window is not null; + } +} diff --git a/Toolkit.Windows/EfficiencyMode.cs b/Toolkit.Windows/EfficiencyMode.cs new file mode 100644 index 0000000..2923e17 --- /dev/null +++ b/Toolkit.Windows/EfficiencyMode.cs @@ -0,0 +1,73 @@ +using Windows.Win32; +using Windows.Win32.System.Threading; + +namespace Toolkit.Windows; + +public class EfficiencyMode : + IEfficiencyMode +{ + public unsafe void SetProcessQualityOfServiceLevel(QualityOfService level) + { + PROCESS_POWER_THROTTLING_STATE powerThrottling = new PROCESS_POWER_THROTTLING_STATE + { + Version = PInvoke.PROCESS_POWER_THROTTLING_CURRENT_VERSION + }; + + switch (level) + { + case QualityOfService.Default: + powerThrottling.ControlMask = 0; + powerThrottling.StateMask = 0; + break; + + case QualityOfService.Eco when Environment.OSVersion.Version >= new Version(11, 0): + case QualityOfService.Low: + powerThrottling.ControlMask = PInvoke.PROCESS_POWER_THROTTLING_EXECUTION_SPEED; + powerThrottling.StateMask = PInvoke.PROCESS_POWER_THROTTLING_EXECUTION_SPEED; + break; + + case QualityOfService.High: + powerThrottling.ControlMask = PInvoke.PROCESS_POWER_THROTTLING_EXECUTION_SPEED; + powerThrottling.StateMask = 0; + break; + + default: + throw new NotImplementedException(); + } + + _ = PInvoke.SetProcessInformation( + hProcess: PInvoke.GetCurrentProcess(), + ProcessInformationClass: PROCESS_INFORMATION_CLASS.ProcessPowerThrottling, + ProcessInformation: &powerThrottling, + ProcessInformationSize: (uint)sizeof(PROCESS_POWER_THROTTLING_STATE)); + } + + public unsafe void SetProcessPriorityClass(ProcessPriority priorityClass) + { + PROCESS_CREATION_FLAGS flags = priorityClass switch + { + ProcessPriority.Default => PROCESS_CREATION_FLAGS.NORMAL_PRIORITY_CLASS, + ProcessPriority.Idle => PROCESS_CREATION_FLAGS.IDLE_PRIORITY_CLASS, + ProcessPriority.BelowNormal => PROCESS_CREATION_FLAGS.BELOW_NORMAL_PRIORITY_CLASS, + ProcessPriority.Normal => PROCESS_CREATION_FLAGS.NORMAL_PRIORITY_CLASS, + ProcessPriority.AboveNormal => PROCESS_CREATION_FLAGS.ABOVE_NORMAL_PRIORITY_CLASS, + ProcessPriority.High => PROCESS_CREATION_FLAGS.HIGH_PRIORITY_CLASS, + ProcessPriority.Realtime => PROCESS_CREATION_FLAGS.REALTIME_PRIORITY_CLASS, + _ => throw new NotImplementedException(), + }; + + _ = PInvoke.SetPriorityClass( + hProcess: PInvoke.GetCurrentProcess(), + dwPriorityClass: flags); + } + + public void SetEfficiencyMode(bool value) + { + QualityOfService ecoLevel = Environment.OSVersion.Version >= new Version(11, 0) + ? QualityOfService.Eco + : QualityOfService.Low; + + SetProcessQualityOfServiceLevel(value ? ecoLevel : QualityOfService.Default); + SetProcessPriorityClass(value ? ProcessPriority.Idle : ProcessPriority.Default); + } +} \ No newline at end of file diff --git a/Toolkit.Windows/IEfficiencyMode.cs b/Toolkit.Windows/IEfficiencyMode.cs new file mode 100644 index 0000000..b266311 --- /dev/null +++ b/Toolkit.Windows/IEfficiencyMode.cs @@ -0,0 +1,9 @@ +namespace Toolkit.Windows +{ + public interface IEfficiencyMode + { + void SetEfficiencyMode(bool value); + void SetProcessPriorityClass(ProcessPriority priorityClass); + void SetProcessQualityOfServiceLevel(QualityOfService level); + } +} \ No newline at end of file diff --git a/Toolkit.Windows/INotifyIcon.cs b/Toolkit.Windows/INotifyIcon.cs new file mode 100644 index 0000000..8c8695e --- /dev/null +++ b/Toolkit.Windows/INotifyIcon.cs @@ -0,0 +1,10 @@ +using Toolkit.Foundation; + +namespace Toolkit.Windows; + +public interface INotifyIcon : + IInitialization, + IDisposable +{ + void SetIcon(IntPtr iconHandle); +} diff --git a/Toolkit.Windows/IPointerMonitor.cs b/Toolkit.Windows/IPointerMonitor.cs index 0888c1d..739b7b2 100644 --- a/Toolkit.Windows/IPointerMonitor.cs +++ b/Toolkit.Windows/IPointerMonitor.cs @@ -4,4 +4,4 @@ namespace Toolkit.Windows; public interface IPointerMonitor : IInitialization, - IDisposable; + IDisposable; \ No newline at end of file diff --git a/Toolkit.Windows/ITaskbar.cs b/Toolkit.Windows/ITaskbar.cs index 093cc45..0c6e271 100644 --- a/Toolkit.Windows/ITaskbar.cs +++ b/Toolkit.Windows/ITaskbar.cs @@ -9,4 +9,4 @@ public interface ITaskbar : TaskbarState GetCurrentState(); IntPtr GetHandle(); -} +} \ No newline at end of file diff --git a/Toolkit.Windows/IWndProcMonitor.cs b/Toolkit.Windows/IWndProc.cs similarity index 51% rename from Toolkit.Windows/IWndProcMonitor.cs rename to Toolkit.Windows/IWndProc.cs index 70f11e8..646b111 100644 --- a/Toolkit.Windows/IWndProcMonitor.cs +++ b/Toolkit.Windows/IWndProc.cs @@ -2,6 +2,10 @@ namespace Toolkit.Windows; -public interface IWndProcMonitor : +public interface IWndProc : IInitialization, - IDisposable; + IDisposable +{ + + IntPtr Handle { get; } +} diff --git a/Toolkit.Windows/NativeMethods.txt b/Toolkit.Windows/NativeMethods.txt index c41c717..d7aac73 100644 --- a/Toolkit.Windows/NativeMethods.txt +++ b/Toolkit.Windows/NativeMethods.txt @@ -14,4 +14,28 @@ MonitorFromWindow RegisterWindowMessage GetDpiForWindow SetWindowPos -SHCreateShellItemArrayFromDataObject \ No newline at end of file +SHCreateShellItemArrayFromDataObject +Shell_NotifyIcon +GetSystemMetricsForDpi +GetSystemMetrics +GetCurrentProcess +SetProcessInformation +PROCESS_POWER_THROTTLING_STATE +PROCESS_POWER_THROTTLING_CURRENT_VERSION +PROCESS_POWER_THROTTLING_EXECUTION_SPEED +SetPriorityClass +WINDOW_STYLE +DefSubclassProc +SetLayeredWindowAttributes +WM_ERASEBKGND +SetWindowSubclass +GetClientRect +CreateSolidBrush +DwmSetWindowAttribute +DWM_WINDOW_CORNER_PREFERENCE +FillRect +SetLayeredWindowAttributes +DeleteObject +RemoveWindowSubclass +GetWindowLong +SetWindowLong \ No newline at end of file diff --git a/Toolkit.Windows/NotifyIcon.cs b/Toolkit.Windows/NotifyIcon.cs new file mode 100644 index 0000000..e640662 --- /dev/null +++ b/Toolkit.Windows/NotifyIcon.cs @@ -0,0 +1,183 @@ +using CommunityToolkit.Mvvm.Messaging; +using System.Runtime.InteropServices; +using Toolkit.Foundation; + +namespace Toolkit.Windows; + + +public partial class NotifyIcon(IWndProc wndProc, + IMessenger messenger) : + INotifyIcon, + IRecipient +{ + private const int CallbackMessage = 0x400; + private const uint IconVersion = 0x4; + + private readonly Lock notifyLock = new(); + private bool isDisposed; + private NotifyIconData notifyIconData; + + ~NotifyIcon() + { + Dispose(false); + } + + private enum NotifyIconBalloonType + { + None = 0x00, + Info = 0x01, + Warning = 0x02, + Error = 0x03, + User = 0x04, + NoSound = 0x10, + LargeIcon = 0x20, + RespectQuietTime = 0x80 + } + + private enum NotifyIconCommand : uint + { + Add = 0x0, + Delete = 0x2, + Modify = 0x1, + SetVersion = 0x4 + } + + [Flags] + private enum NotifyIconDataMember : uint + { + Message = 0x01, + Icon = 0x02, + Tip = 0x04, + State = 0x08, + Info = 0x10, + Realtime = 0x40, + UseLegacyToolTips = 0x80 + } + + private enum NotifyIconState : uint + { + Visible = 0x00, + Hidden = 0x01 + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Initialize() + { + messenger.RegisterAll(this); + CreateNotificationIcon(); + } + + public void Receive(WndProcEventArgs message) + { + if (message.Message == CallbackMessage) + { + switch (message.LParam) + { + case (uint)WndProcMessages.WM_LBUTTONUP: + messenger.Send(new NotifyIconInvokedEventArgs(PointerButton.Left)); + break; + + case (uint)WndProcMessages.WM_MBUTTONUP: + messenger.Send(new NotifyIconInvokedEventArgs(PointerButton.Middle)); + break; + + case (uint)WndProcMessages.WM_RBUTTONUP: + messenger.Send(new NotifyIconInvokedEventArgs(PointerButton.Right)); + break; + } + } + } + + public void SetIcon(IntPtr iconHandle) + { + lock (notifyLock) + { + notifyIconData.IconHandle = iconHandle; + WriteNotifyIconData(NotifyIconCommand.Modify, NotifyIconDataMember.Icon); + } + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr DefWindowProcW(IntPtr handle, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("shell32.dll", SetLastError = true)] + private static extern int Shell_NotifyIcon(NotifyIconCommand notifyCommand, ref NotifyIconData notifyIconData); + + private void CreateNotificationIcon() + { + lock (notifyLock) + { + notifyIconData = new NotifyIconData(); + + notifyIconData.cbSize = (uint)Marshal.SizeOf(notifyIconData); + notifyIconData.WindowHandle = wndProc.Handle; + notifyIconData.TaskbarIconId = 0x0; + notifyIconData.CallbackMessageId = CallbackMessage; + notifyIconData.VersionOrTimeout = IconVersion; + + notifyIconData.IconHandle = IntPtr.Zero; + + notifyIconData.IconState = NotifyIconState.Hidden; + notifyIconData.StateMask = NotifyIconState.Hidden; + + WriteNotifyIconData(NotifyIconCommand.Add, NotifyIconDataMember.Message | NotifyIconDataMember.Icon | NotifyIconDataMember.Tip); + } + } + + private void Dispose(bool disposing) + { + if (isDisposed || !disposing) return; + lock (notifyLock) + { + isDisposed = true; + + messenger.UnregisterAll(this); + RemoveNotificationIcon(); + } + } + + private void RemoveNotificationIcon() => WriteNotifyIconData(NotifyIconCommand.Delete, NotifyIconDataMember.Message); + + private void WriteNotifyIconData(NotifyIconCommand command, NotifyIconDataMember flags) + { + notifyIconData.ValidMembers = flags; + lock (notifyLock) + { + Shell_NotifyIcon(command, ref notifyIconData); + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct NotifyIconData + { + public uint cbSize; + public IntPtr WindowHandle; + public uint TaskbarIconId; + public NotifyIconDataMember ValidMembers; + public uint CallbackMessageId; + public IntPtr IconHandle; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string ToolTipText; + + public NotifyIconState IconState; + public NotifyIconState StateMask; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string BalloonText; + + public uint VersionOrTimeout; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string BalloonTitle; + + public NotifyIconBalloonType BalloonFlags; + public Guid TaskbarIconGuid; + public IntPtr CustomBalloonIconHandle; + } +} \ No newline at end of file diff --git a/Toolkit.Windows/NotifyIconInvokedEventArgs.cs b/Toolkit.Windows/NotifyIconInvokedEventArgs.cs new file mode 100644 index 0000000..9801e5b --- /dev/null +++ b/Toolkit.Windows/NotifyIconInvokedEventArgs.cs @@ -0,0 +1,3 @@ +namespace Toolkit.Windows; + +public record NotifyIconInvokedEventArgs(PointerButton PointerButton); diff --git a/Toolkit.Windows/ProcessPriority.cs b/Toolkit.Windows/ProcessPriority.cs new file mode 100644 index 0000000..e7af205 --- /dev/null +++ b/Toolkit.Windows/ProcessPriority.cs @@ -0,0 +1,12 @@ +namespace Toolkit.Windows; + +public enum ProcessPriority +{ + Default, + Idle, + BelowNormal, + Normal, + AboveNormal, + High, + Realtime +} diff --git a/Toolkit.Windows/QualityOfService.cs b/Toolkit.Windows/QualityOfService.cs new file mode 100644 index 0000000..8fcd3a0 --- /dev/null +++ b/Toolkit.Windows/QualityOfService.cs @@ -0,0 +1,23 @@ +using System.Runtime.Versioning; + +namespace Toolkit.Windows; + +[SupportedOSPlatform("windows8.0")] +public enum QualityOfService +{ + Default, + [SupportedOSPlatform("windows10.0.16299.0")] + High, + [SupportedOSPlatform("windows10.0.16299.0")] + Medium, + [SupportedOSPlatform("windows10.0.16299.0")] + Low, + [SupportedOSPlatform("windows11.0.22621.0")] + Utility, + [SupportedOSPlatform("windows11.0")] + Eco, + [SupportedOSPlatform("windows10.0.19041.0")] + Media, + [SupportedOSPlatform("windows10.0.19041.0")] + Deadline +} diff --git a/Toolkit.Windows/WindowHelper.cs b/Toolkit.Windows/WindowHelper.cs index abaff17..84e3ac5 100644 --- a/Toolkit.Windows/WindowHelper.cs +++ b/Toolkit.Windows/WindowHelper.cs @@ -7,6 +7,10 @@ namespace Toolkit.Windows; public class WindowHelper { + public static IntPtr GetHandle(string windowName) => PInvoke.FindWindow(windowName, null); + + public static uint GetDpi(IntPtr handle) => PInvoke.GetDpiForWindow((HWND)handle); + public static void BringToForeground(HWND handle) { if (TryGetBoundsUnsafe(handle, out RECT bounds)) diff --git a/Toolkit.Windows/WndProcMonitor.cs b/Toolkit.Windows/WndProc.cs similarity index 95% rename from Toolkit.Windows/WndProcMonitor.cs rename to Toolkit.Windows/WndProc.cs index 153ca7e..5b917a9 100644 --- a/Toolkit.Windows/WndProcMonitor.cs +++ b/Toolkit.Windows/WndProc.cs @@ -6,8 +6,8 @@ using Windows.Win32.UI.WindowsAndMessaging; namespace Toolkit.Windows; -public class WndProcMonitor(IMessenger messenger) : - IWndProcMonitor +public class WndProc(IMessenger messenger) : + IWndProc { private WNDPROC? handler; diff --git a/Toolkit.sln b/Toolkit.sln index 53dc88f..9dfaaa2 100644 --- a/Toolkit.sln +++ b/Toolkit.sln @@ -83,8 +83,8 @@ Global {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Debug|x64.Build.0 = Debug|x64 {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Debug|x86.ActiveCfg = Debug|x86 {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Debug|x86.Build.0 = Debug|x86 - {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|Any CPU.ActiveCfg = Release|x64 - {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|Any CPU.Build.0 = Release|x64 + {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|Any CPU.Build.0 = Release|Any CPU {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|x64.ActiveCfg = Release|x64 {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|x64.Build.0 = Release|x64 {08F06CCE-86F9-4885-84F0-B23B2E5A0813}.Release|x86.ActiveCfg = Release|x86