diff --git a/Morphic.Controls/HybridTrayIcon.cs b/Morphic.Controls/HybridTrayIcon.cs index 17bb609b..5fb1f2b0 100644 --- a/Morphic.Controls/HybridTrayIcon.cs +++ b/Morphic.Controls/HybridTrayIcon.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -31,228 +31,259 @@ namespace Morphic.Controls; /// public class HybridTrayIcon : IDisposable { - private System.Drawing.Icon? _icon = null; - private string? _text = null; - private bool _visible = false; - - // Used if a tray icon is desired instead of a next-to-tray taskbar button - private System.Windows.Forms.NotifyIcon? _notifyIcon = null; - - // Used if a next-to-tray button is desired instead of a tray icon - private Morphic.Controls.TrayButton.TrayButton? _trayButton = null; - - public enum TrayIconLocationOption - { - None, - NotificationTray, - NextToNotificationTray, - NotificationTrayAndNextToNotificationTray - } - // - private TrayIconLocationOption _trayIconLocation = TrayIconLocationOption.None; - - /// Raised when the button is clicked. - public event EventHandler? Click; - /// Raised when the button is right-clicked. - public event EventHandler? SecondaryClick; - - public HybridTrayIcon() - { - } - - public void Dispose() - { - _notifyIcon?.Dispose(); - _notifyIcon = null; - - _trayButton?.Dispose(); - _trayButton = null; - } - - /// The icon for the tray icon - public System.Drawing.Icon? Icon - { - get - { - return _icon; - } - set - { - _icon = value; - if (_notifyIcon is not null) - { - _notifyIcon.Icon = _icon; - } - if (_trayButton is not null) - { - _trayButton.Icon = _icon; - } - } - } - - /// Tooltip for the tray icon. - public string? Text - { - get - { - return _text; - } - set - { - _text = value; - if (_notifyIcon is not null) - { - _notifyIcon.Text = _text; - } - if (_trayButton is not null) - { - _trayButton.Text = _text; - } - } - } - - /// Show or hide the tray icon. - public bool Visible - { - get - { - return _visible; - } - set - { - _visible = value; - - if (_notifyIcon is not null) - { - _notifyIcon.Visible = _visible; - } - if (_trayButton is not null) - { - _trayButton.Visible = _visible; - } - } - } - - // - - public void SuppressTaskbarButtonResurfaceChecks(bool suppress) - { - _trayButton?.SuppressTaskbarButtonResurfaceChecks(suppress); - } - - // - - private void InitializeTrayIcon() - { - if (_notifyIcon is not null) - { - return; - } - - _notifyIcon = new System.Windows.Forms.NotifyIcon(); - _notifyIcon.Text = _text; - _notifyIcon.Icon = _icon; - // - _notifyIcon.MouseUp += (sender, args) => - { - if (args.Button == System.Windows.Forms.MouseButtons.Right) - { - this.SecondaryClick?.Invoke(this, args); - } - else if (args.Button == System.Windows.Forms.MouseButtons.Left) - { - this.Click?.Invoke(this, args); - } - }; - _notifyIcon.Visible = _visible; - } - - private void InitializeTrayButton() - { - if (_trayButton is not null) - { - return; - } - - _trayButton = new Morphic.Controls.TrayButton.TrayButton(); - _trayButton.Text = _text; - _trayButton.Icon = _icon; - // - _trayButton.MouseUp += (sender, args) => - { - if (args.Button == System.Windows.Forms.MouseButtons.Right) - { - this.SecondaryClick?.Invoke(this, args); - } - else if (args.Button == System.Windows.Forms.MouseButtons.Left) - { - this.Click?.Invoke(this, args); - } - }; - _trayButton.Visible = _visible; - } - - // - - public TrayIconLocationOption TrayIconLocation - { - get - { - return _trayIconLocation; - } - set - { - _trayIconLocation = value; - - // create notify icon if requested - switch (value) - { - case TrayIconLocationOption.NotificationTray: - case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: - if (_notifyIcon is null) - { - this.InitializeTrayIcon(); - } - break; - } - - // create tray button if requested - switch (value) - { - case TrayIconLocationOption.NextToNotificationTray: - case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: - if (_trayButton is null) - { - this.InitializeTrayButton(); - } - break; - } - - // destroy notify icon if no longer wanted - switch (value) - { - case TrayIconLocationOption.None: - case TrayIconLocationOption.NextToNotificationTray: - if (_notifyIcon is not null) - { - _notifyIcon.Dispose(); - _notifyIcon = null; - } - break; - } - - // destroy tray button if no longer wanted - switch (value) - { - case TrayIconLocationOption.None: - case TrayIconLocationOption.NotificationTray: - if (_trayButton is not null) - { - _trayButton.Dispose(); - _trayButton = null; - } - break; - } - } - } + private bool disposedValue; + + private System.Drawing.Icon? _icon = null; + private string? _text = null; + private bool _visible = false; + + // Used if a tray icon is desired instead of a next-to-tray taskbar button + private System.Windows.Forms.NotifyIcon? _notifyIcon = null; + + // Used if a next-to-tray button is desired instead of a tray icon + private Morphic.Controls.TrayButton.TrayButton? _trayButton = null; + + public enum TrayIconLocationOption + { + None, + NotificationTray, + NextToNotificationTray, + NotificationTrayAndNextToNotificationTray + } + // + private TrayIconLocationOption _trayIconLocation = TrayIconLocationOption.None; + + /// Raised when the button is clicked. + public event EventHandler? Click; + /// Raised when the button is right-clicked. + public event EventHandler? SecondaryClick; + + public HybridTrayIcon() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + _notifyIcon?.Dispose(); + _notifyIcon = null; + + _trayButton?.Dispose(); + _trayButton = null; + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrayButton() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// The icon for the tray icon + public System.Drawing.Icon? Icon + { + get + { + return _icon; + } + set + { + _icon = value; + if (_notifyIcon is not null) + { + _notifyIcon.Icon = _icon; + } + if (_trayButton is not null) + { + _trayButton.Icon = _icon; + } + } + } + + /// Tooltip for the tray icon. + public string? Text + { + get + { + return _text; + } + set + { + _text = value; + if (_notifyIcon is not null) + { + _notifyIcon.Text = _text; + } + if (_trayButton is not null) + { + _trayButton.Text = _text; + } + } + } + + /// Show or hide the tray icon. + public bool Visible + { + get + { + return _visible; + } + set + { + _visible = value; + + if (_notifyIcon is not null) + { + _notifyIcon.Visible = _visible; + } + if (_trayButton is not null) + { + _trayButton.Visible = _visible; + } + } + } + + // + + public void SuppressTaskbarButtonResurfaceChecks(bool suppress) + { + _trayButton?.SuppressTaskbarButtonResurfaceChecks(suppress); + } + + // + + private void InitializeTrayIcon() + { + if (_notifyIcon is not null) + { + return; + } + + _notifyIcon = new System.Windows.Forms.NotifyIcon(); + _notifyIcon.Text = _text; + _notifyIcon.Icon = _icon; + // + _notifyIcon.MouseUp += (sender, args) => + { + if (args.Button == System.Windows.Forms.MouseButtons.Right) + { + this.SecondaryClick?.Invoke(this, args); + } + else if (args.Button == System.Windows.Forms.MouseButtons.Left) + { + this.Click?.Invoke(this, args); + } + }; + _notifyIcon.Visible = _visible; + } + + private void InitializeTrayButton() + { + if (_trayButton is not null) + { + return; + } + + _trayButton = new Morphic.Controls.TrayButton.TrayButton(); + _trayButton.Text = _text; + _trayButton.Icon = _icon; + // + _trayButton.MouseUp += (sender, args) => + { + if (args.Button == System.Windows.Forms.MouseButtons.Right) + { + this.SecondaryClick?.Invoke(this, args); + } + else if (args.Button == System.Windows.Forms.MouseButtons.Left) + { + this.Click?.Invoke(this, args); + } + }; + _trayButton.Visible = _visible; + } + + // + + public TrayIconLocationOption TrayIconLocation + { + get + { + return _trayIconLocation; + } + set + { + _trayIconLocation = value; + + // create notify icon if requested + switch (value) + { + case TrayIconLocationOption.NotificationTray: + case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: + if (_notifyIcon is null) + { + this.InitializeTrayIcon(); + } + break; + } + + // create tray button if requested + switch (value) + { + case TrayIconLocationOption.NextToNotificationTray: + case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: + if (_trayButton is null) + { + this.InitializeTrayButton(); + } + break; + } + + // destroy notify icon if no longer wanted + switch (value) + { + case TrayIconLocationOption.None: + case TrayIconLocationOption.NextToNotificationTray: + if (_notifyIcon is not null) + { + _notifyIcon.Dispose(); + _notifyIcon = null; + } + break; + } + + // destroy tray button if no longer wanted + switch (value) + { + case TrayIconLocationOption.None: + case TrayIconLocationOption.NotificationTray: + if (_trayButton is not null) + { + _trayButton.Dispose(); + _trayButton = null; + } + break; + } + } + } } \ No newline at end of file diff --git a/Morphic.Controls/NativeMethods.txt b/Morphic.Controls/NativeMethods.txt index 0e53ba55..7b8235d4 100644 --- a/Morphic.Controls/NativeMethods.txt +++ b/Morphic.Controls/NativeMethods.txt @@ -13,8 +13,8 @@ DestroyWindow EndBufferedPaint EndPaint FillRect -FindWindowW -FindWindowExW +FindWindow +FindWindowEx GetDC GetDesktopWindow GetWindow diff --git a/Morphic.Controls/PInvokeExtensions.cs b/Morphic.Controls/PInvokeExtensions.cs index 5adef59d..7bdfab40 100644 --- a/Morphic.Controls/PInvokeExtensions.cs +++ b/Morphic.Controls/PInvokeExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -23,47 +23,304 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace Morphic.Controls; internal class PInvokeExtensions { - #region winuser - - internal static IntPtr GetWindowLongPtr_IntPtr(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex) - { - if (IntPtr.Size == 4) - { - return (nint)Windows.Win32.PInvoke.GetWindowLong(hWnd, nIndex); - } - else - { - return PInvokeExtensions.GetWindowLongPtr(hWnd.Value, nIndex); - } - } - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-redrawwindow - [DllImport("user32.dll")] - internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS flags); - - internal static IntPtr SetWindowLongPtr_IntPtr(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex, IntPtr dwNewLong) - { - if (IntPtr.Size == 4) - { - return (nint)Windows.Win32.PInvoke.SetWindowLong(hWnd, nIndex, (int)dwNewLong); - } - else - { - return PInvokeExtensions.SetWindowLongPtr(hWnd, nIndex, dwNewLong); - } - } - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex, IntPtr dwNewLong); - - #endregion winuser + #region commctrl + + internal const string TOOLTIPS_CLASS = "tooltips_class32"; + // + internal const byte TTS_ALWAYSTIP = 0x01; + //internal const byte TTS_NOPREFIX = 0x02; + //internal const byte TTS_BALLOON = 0x40; + internal const ushort TTF_SUBCLASS = 0x0010; + // + internal const ushort TTM_ADDTOOL = WM_USER + 50; + internal const ushort TTM_DELTOOL = WM_USER + 51; + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-tttoolinfow +#pragma warning disable CS0649 // NOTE: hinst and lParam may never be written to (and will remain as IntPtr.Zero) in this implementation + internal struct TOOLINFO + { + public uint cbSize; + public uint uFlags; + public IntPtr hwnd; + public UIntPtr uId; + public PInvoke.RECT rect; + public IntPtr hinst; + [MarshalAs(UnmanagedType.LPTStr)] + public string? lpszText; + public IntPtr lParam; + //public IntPtr reserved; // NOTE: this exists in the official declaration as a void pointer but adding it causes SendMessage to fail; pinvoke.net leaves it out and so do we + } +#pragma warning restore CS0649 // NOTE: hinst and lParam may never be written to (and will remain as IntPtr.Zero) in this implementation + + #endregion commctrl + + #region winuser + + internal const int CW_USEDEFAULT = unchecked((int)0x80000000); + + internal const ushort MK_LBUTTON = 0x0001; + internal const ushort MK_RBUTTON = 0x0002; + + internal const ushort WM_USER = 0x0400; + + // + + internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent + [Flags] + internal enum TRACKMOUSEEVENTFlags : uint + { + TME_CANCEL = 0x80000000, + TME_HOVER = 0x00000001, + TME_LEAVE = 0x00000002, + TME_NONCLIENT = 0x00000010, + TME_QUERY = 0x40000000 + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook + [Flags] + internal enum WinEventHookFlags : uint + { + WINEVENT_OUTOFCONTEXT = 0x0000, // Events are ASYNC + WINEVENT_SKIPOWNTHREAD = 0x0001, // Don't call back for events on installer's thread + WINEVENT_SKIPOWNPROCESS = 0x0002, // Don't call back for events on installer's process + WINEVENT_INCONTEXT = 0x0004, // Events are SYNC, this causes your dll to be injected into every process + } + + // https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants + public enum WinEventHookType : uint + { + EVENT_AIA_START = 0xA000, + EVENT_AIA_END = 0xAFFF, + EVENT_MIN = 0x00000001, + EVENT_MAX = 0x7FFFFFFF, + EVENT_OBJECT_ACCELERATORCHANGE = 0x8012, + EVENT_OBJECT_CLOAKED = 0x8017, + EVENT_OBJECT_CONTENTSCROLLED = 0x8015, + EVENT_OBJECT_CREATE = 0x8000, + EVENT_OBJECT_DEFACTIONCHANGE = 0x8011, + EVENT_OBJECT_DESCRIPTIONCHANGE = 0x800D, + EVENT_OBJECT_DESTROY = 0x8001, + EVENT_OBJECT_DRAGSTART = 0x8021, + EVENT_OBJECT_DRAGCANCEL = 0x8022, + EVENT_OBJECT_DRAGCOMPLETE = 0x8023, + EVENT_OBJECT_DRAGENTER = 0x8024, + EVENT_OBJECT_DRAGLEAVE = 0x8025, + EVENT_OBJECT_DRAGDROPPED = 0x8026, + EVENT_OBJECT_END = 0x80FF, + EVENT_OBJECT_FOCUS = 0x8005, + EVENT_OBJECT_HELPCHANGE = 0x8010, + EVENT_OBJECT_HIDE = 0x8003, + EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED = 0x8020, + EVENT_OBJECT_IME_HIDE = 0x8028, + EVENT_OBJECT_IME_SHOW = 0x8027, + EVENT_OBJECT_IME_CHANGE = 0x8029, + EVENT_OBJECT_INVOKED = 0x8013, + EVENT_OBJECT_LIVEREGIONCHANGED = 0x8019, + EVENT_OBJECT_LOCATIONCHANGE = 0x800B, + EVENT_OBJECT_NAMECHANGE = 0x800C, + EVENT_OBJECT_PARENTCHANGE = 0x800F, + EVENT_OBJECT_REORDER = 0x8004, + EVENT_OBJECT_SELECTION = 0x8006, + EVENT_OBJECT_SELECTIONADD = 0x8007, + EVENT_OBJECT_SELECTIONREMOVE = 0x8008, + EVENT_OBJECT_SELECTIONWITHIN = 0x8009, + EVENT_OBJECT_SHOW = 0x8002, + EVENT_OBJECT_STATECHANGE = 0x800A, + EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED = 0x8030, + EVENT_OBJECT_TEXTSELECTIONCHANGED = 0x8014, + EVENT_OBJECT_UNCLOAKED = 0x8018, + EVENT_OBJECT_VALUECHANGE = 0x800E, + EVENT_OEM_DEFINED_START = 0x0101, + EVENT_OEM_DEFINED_END = 0x01FF, + EVENT_SYSTEM_ALERT = 0x0002, + EVENT_SYSTEM_ARRANGMENTPREVIEW = 0x8016, + EVENT_SYSTEM_CAPTUREEND = 0x0009, + EVENT_SYSTEM_CAPTURESTART = 0x0008, + EVENT_SYSTEM_CONTEXTHELPEND = 0x000D, + EVENT_SYSTEM_CONTEXTHELPSTART = 0x000C, + EVENT_SYSTEM_DESKTOPSWITCH = 0x0020, + EVENT_SYSTEM_DIALOGEND = 0x0011, + EVENT_SYSTEM_DIALOGSTART = 0x0010, + EVENT_SYSTEM_DRAGDROPEND = 0x000F, + EVENT_SYSTEM_DRAGDROPSTART = 0x000E, + EVENT_SYSTEM_END = 0x00FF, + EVENT_SYSTEM_FOREGROUND = 0x0003, + EVENT_SYSTEM_MENUPOPUPEND = 0x0007, + EVENT_SYSTEM_MENUPOPUPSTART = 0x0006, + EVENT_SYSTEM_MENUEND = 0x0005, + EVENT_SYSTEM_MENUSTART = 0x0004, + EVENT_SYSTEM_MINIMIZEEND = 0x0017, + EVENT_SYSTEM_MINIMIZESTART = 0x0016, + EVENT_SYSTEM_MOVESIZEEND = 0x000B, + EVENT_SYSTEM_MOVESIZESTART = 0x000A, + EVENT_SYSTEM_SCROLLINGEND = 0x0013, + EVENT_SYSTEM_SCROLLINGSTART = 0x0012, + EVENT_SYSTEM_SOUND = 0x0001, + EVENT_SYSTEM_SWITCHEND = 0x0015, + EVENT_SYSTEM_SWITCHSTART = 0x0014, + EVENT_UIA_EVENTID_START = 0x4E00, + EVENT_UIA_EVENTID_END = 0x4EFF, + EVENT_UIA_PROPID_START = 0x7500, + EVENT_UIA_PROPID_END = 0x75FF + } + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent + [StructLayout(LayoutKind.Sequential)] + internal struct TRACKMOUSEEVENT + { + public uint cbSize; + public TRACKMOUSEEVENTFlags dwFlags; + public IntPtr hWnd; + public uint dwHoverTime; + + public static TRACKMOUSEEVENT CreateNew(TRACKMOUSEEVENTFlags dwFlags, IntPtr hWnd, uint dwHoverTime) + { + var result = new TRACKMOUSEEVENT() + { + cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)), + dwFlags = dwFlags, + hWnd = hWnd, + dwHoverTime = dwHoverTime + }; + return result; + } + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WNDCLASSEX + { + public uint cbSize; + public uint style; + public IntPtr lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + public string? lpszMenuName; + public string? lpszClassName; // NOTE: this member should be initialized (i.e. non-null) + public IntPtr hIconSm; + + public static WNDCLASSEX CreateNew() + { + var result = new WNDCLASSEX() + { + cbSize = (uint)Marshal.SizeOf(typeof(WNDCLASSEX)) + }; + return result; + } + } + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wineventproc + internal delegate void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc + internal delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CreateWindowEx( + PInvoke.User32.WindowStylesEx dwExStyle, + IntPtr lpClassName, + string? lpWindowName, + PInvoke.User32.WindowStyles dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclassnamew + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw + internal static IntPtr GetWindowLongPtr_IntPtr(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex) + { + if (IntPtr.Size == 4) + { + return (nint)Windows.Win32.PInvoke.GetWindowLong(hWnd, nIndex); + } + else + { + return PInvokeExtensions.GetWindowLongPtr(hWnd.Value, nIndex); + } + } + // + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex); + + // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints + // NOTE: this signature is the POINT option (in which cPoints must always be set to 1). + [DllImport("user32.dll", SetLastError = true)] + internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref PInvoke.POINT lpPoints, uint cPoints); + // + // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints + // NOTE: this signature is the RECT option (in which cPoints must always be set to 2). + [DllImport("user32.dll", SetLastError = true)] + internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref PInvoke.RECT lpPoints, uint cPoints); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-redrawwindow + [DllImport("user32.dll")] + internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS flags); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern ushort RegisterClassEx([In] ref WNDCLASSEX lpWndClass); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptrw + internal static IntPtr SetWindowLongPtr_IntPtr(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 4) + { + return (nint)Windows.Win32.PInvoke.SetWindowLong(hWnd, nIndex, (int)dwNewLong); + } + else + { + return PInvokeExtensions.SetWindowLongPtr(hWnd, nIndex, dwNewLong); + } + } + // + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptrw + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex, IntPtr dwNewLong); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook + [DllImport("user32.dll")] + internal static extern IntPtr SetWinEventHook(WinEventHookType eventMin, WinEventHookType eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, WinEventHookFlags dwFlags); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackmouseevent + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unhookwinevent + [DllImport("user32.dll")] + internal static extern bool UnhookWinEvent(IntPtr hWinEventHook); + + #endregion winuser } diff --git a/Morphic.Controls/TrayButton/TrayButton.cs b/Morphic.Controls/TrayButton/TrayButton.cs index 6fc53227..bb6ae915 100644 --- a/Morphic.Controls/TrayButton/TrayButton.cs +++ b/Morphic.Controls/TrayButton/TrayButton.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -27,185 +27,218 @@ namespace Morphic.Controls.TrayButton; public class TrayButton : IDisposable { - // NOTE: only one of the two tray button variants will be populated (i.e. based on the OS version) - // [we have chosen not to create a common interface between them, as the plan is to deprecate the Windows 10 variant once Windows 10 is no longer supported...and the Windows 11+ variant should be allowed to get a new API surface if/as needed] - Morphic.Controls.TrayButton.Windows10.TrayButton? _legacyTrayButton; - Morphic.Controls.TrayButton.Windows11.TrayButton? _trayButton; - - public event System.Windows.Forms.MouseEventHandler? MouseUp; - - public TrayButton() - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - // Windows 11 and newer (i.e. modern tray button) - _trayButton = new(); - _trayButton.MouseUp += (s, e) => - { - this.MouseUp?.Invoke(s, e); - }; - } - else - { - // Windows 10 (i.e. legacy tray button) - _legacyTrayButton = new(); - _legacyTrayButton.MouseUp += (s, e) => - { - this.MouseUp?.Invoke(s, e); - }; - } - } - - public void Dispose() - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _trayButton?.Dispose(); - } - else - { - _legacyTrayButton?.Dispose(); - } - } - - public System.Drawing.Bitmap? Bitmap - { - get - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - return _trayButton!.Bitmap; - } - else //if (.IsWindows10() == true) - { - var icon = _legacyTrayButton!.Icon; - return (icon is not null) ? icon!.ToBitmap() : null; - } - } - set - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _trayButton!.Bitmap = value; - } - else //if (.IsWindows10() == true) - { - if (value is not null) + private bool disposedValue; + + // NOTE: only one of the two tray button variants will be populated (i.e. based on the OS version) + // [we have chosen not to create a common interface between them, as the plan is to deprecate the Windows 10 variant once Windows 10 is no longer supported...and the Windows 11+ variant should be allowed to get a new API surface if/as needed] + Morphic.Controls.TrayButton.Windows10.TrayButton? _legacyTrayButton; + Morphic.Controls.TrayButton.Windows11.TrayButton? _trayButton; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + public TrayButton() + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + // Windows 11 and newer (i.e. modern tray button) + _trayButton = new(); + _trayButton.MouseUp += (s, e) => + { + this.MouseUp?.Invoke(s, e); + }; + } + else + { + // Windows 10 (i.e. legacy tray button) + _legacyTrayButton = new(); + _legacyTrayButton.MouseUp += (s, e) => + { + this.MouseUp?.Invoke(s, e); + }; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton?.Dispose(); + } + else + { + _legacyTrayButton?.Dispose(); + } + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrayButton() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public System.Drawing.Bitmap? Bitmap + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + return _trayButton!.Bitmap; + } + else //if (.IsWindows10() == true) + { + var icon = _legacyTrayButton!.Icon; + return (icon is not null) ? icon!.ToBitmap() : null; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Bitmap = value; + } + else //if (.IsWindows10() == true) + { + if (value is not null) + { + var bitmapAsIconHandlePointer = value.GetHicon(); + try { - var bitmapAsIconHandlePointer = value.GetHicon(); - try - { - _legacyTrayButton!.Icon = (System.Drawing.Icon)(System.Drawing.Icon.FromHandle(bitmapAsIconHandlePointer).Clone()); - } - finally - { - _ = Windows.Win32.PInvoke.DestroyIcon((Windows.Win32.UI.WindowsAndMessaging.HICON)bitmapAsIconHandlePointer); - } + _legacyTrayButton!.Icon = (System.Drawing.Icon)(System.Drawing.Icon.FromHandle(bitmapAsIconHandlePointer).Clone()); } - else + finally { - _legacyTrayButton!.Icon = null; + _ = Windows.Win32.PInvoke.DestroyIcon((Windows.Win32.UI.WindowsAndMessaging.HICON)bitmapAsIconHandlePointer); } - } - } - } - - public System.Drawing.Icon? Icon - { - get - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - if (_trayButton!.Bitmap is not null) + } + else + { + _legacyTrayButton!.Icon = null; + } + } + } + } + + public System.Drawing.Icon? Icon + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + if (_trayButton!.Bitmap is not null) + { + var bitmapAsIconHandlePointer = _trayButton!.Bitmap!.GetHicon(); + try { - var bitmapAsIconHandlePointer = _trayButton!.Bitmap!.GetHicon(); - try - { - return (System.Drawing.Icon)(System.Drawing.Icon.FromHandle(bitmapAsIconHandlePointer).Clone()); - } - finally - { - Windows.Win32.PInvoke.DestroyIcon((Windows.Win32.UI.WindowsAndMessaging.HICON)bitmapAsIconHandlePointer); - } + return (System.Drawing.Icon)(System.Drawing.Icon.FromHandle(bitmapAsIconHandlePointer).Clone()); } - else + finally { - return null; + Windows.Win32.PInvoke.DestroyIcon((Windows.Win32.UI.WindowsAndMessaging.HICON)bitmapAsIconHandlePointer); } - } - else //if (.IsWindows10() == true) - { - return _legacyTrayButton!.Icon; - } - } - set - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _trayButton!.Bitmap = (value is not null) ? value!.ToBitmap() : null; - } - else //if (.IsWindows10() == true) - { - _legacyTrayButton!.Icon = value; - } - } - } - - public string? Text - { - get - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - return _trayButton!.Text; - } - else //if (.IsWindows10() == true) - { - return _legacyTrayButton!.Text; - } - } - set - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _trayButton!.Text = value; - } - else //if (.IsWindows10() == true) - { - _legacyTrayButton!.Text = value; - } - } - } - - public bool Visible - { - get - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - return _trayButton!.Visible; - } - else //if (.IsWindows10() == true) - { - return _legacyTrayButton!.Visible; - } - } - set - { - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _trayButton!.Visible = value; - } - else //if (.IsWindows10() == true) - { - _legacyTrayButton!.Visible = value; - } - } - } - - public void SuppressTaskbarButtonResurfaceChecks(bool suppress) - { - _trayButton?.SuppressTaskbarButtonResurfaceChecks(suppress); - } + } + else + { + return null; + } + } + else //if (.IsWindows10() == true) + { + return _legacyTrayButton!.Icon; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Bitmap = (value is not null) ? value!.ToBitmap() : null; + } + else //if (.IsWindows10() == true) + { + _legacyTrayButton!.Icon = value; + } + } + } + + public string? Text + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + return _trayButton!.Text; + } + else //if (.IsWindows10() == true) + { + return _legacyTrayButton!.Text; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Text = value; + } + else //if (.IsWindows10() == true) + { + _legacyTrayButton!.Text = value; + } + } + } + + public bool Visible + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + return _trayButton!.Visible; + } + else //if (.IsWindows10() == true) + { + return _legacyTrayButton!.Visible; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Visible = value; + } + else //if (.IsWindows10() == true) + { + _legacyTrayButton!.Visible = value; + } + } + } + + public void SuppressTaskbarButtonResurfaceChecks(bool suppress) + { + _trayButton?.SuppressTaskbarButtonResurfaceChecks(suppress); + } } diff --git a/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs b/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs index b0056bd3..6b4cdd30 100644 --- a/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs +++ b/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -28,586 +28,602 @@ namespace Morphic.Controls.TrayButton.Windows10; internal class LegacyWindowsApi { - #region Win32 error codes - public const uint ERROR_SUCCESS = 0; - #endregion - - #region Window Positioning - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - - public PInvoke.RECT ToPInvokeRect() - { - return new PInvoke.RECT() { left = this.Left, top = this.Top, right = this.Right, bottom = this.Bottom }; - } - - /// - /// Creates a win32 RECT from a .NET Rect. - /// - /// The rectangle. - public RECT(System.Windows.Rect rect) - { - this.Left = (int)rect.Left; - this.Top = (int)rect.Top; - this.Right = (int)rect.Right; - this.Bottom = (int)rect.Bottom; - } - - public static RECT Empty - { - get - { - return new RECT(new System.Windows.Rect(0, 0, 0, 0)); - } - } - - public bool HasNonZeroWidthOrHeight() - { - return ((this.Left == this.Right) || (this.Top == this.Bottom)); - } - - public bool IsInside(RECT rect) - { - return ((this.Left >= rect.Left) && (this.Right <= rect.Right) && (this.Top >= rect.Top) && (this.Bottom <= rect.Bottom)); - } - - public bool Intersects(RECT rect) - { - bool overlapsHorizontally = false; - bool overlapsVertically = false; - - // horizontal check - if ((this.Right > rect.Left) && (this.Left < rect.Right)) - { - // partially or fully overlaps horizontally - overlapsHorizontally = true; - } - - // vertical check - if ((this.Bottom > rect.Top) && (this.Top < rect.Bottom)) - { - // partially or fully overlaps vertically - overlapsVertically = true; - } - - if ((overlapsHorizontally == true) && (overlapsVertically == true)) - { - return true; - } - - // if we could not find overlap, then return false - return false; - } - - public static bool operator ==(RECT lhs, RECT rhs) - { - if ((lhs.Left == rhs.Left) && - (lhs.Top == rhs.Top) && - (lhs.Right == rhs.Right) && - (lhs.Bottom == rhs.Bottom)) - { - return true; - } - else - { - return false; - } - } - - public static bool operator !=(RECT lhs, RECT rhs) - { - if ((lhs.Left != rhs.Left) || - (lhs.Top != rhs.Top) || - (lhs.Right != rhs.Right) || - (lhs.Bottom != rhs.Bottom)) - { - return true; - } - else - { - return false; - } - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct POINT - { - public int X; - public int Y; - - public POINT(int x, int y) - { - this.X = x; - this.Y = y; - } - - public POINT(System.Windows.Point pt) : this((int)pt.X, (int)pt.Y) - { - } - - public System.Windows.Point ToPoint() - { - return new System.Windows.Point(this.X, this.Y); - } - } - - [DllImport("user32.dll")] - public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); - - [DllImport("user32.dll")] - internal static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); - - #endregion - - #region Window Creation and Management - - [DllImport("gdi32.dll")] - internal static extern IntPtr CreateCompatibleDC(IntPtr hdc); - - [DllImport("gdi32.dll")] - internal static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO pbmi, uint usage, out IntPtr ppvBits, IntPtr hSection, uint offset); - - //[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - //internal static extern IntPtr CreateWindowEx( - // WindowStylesEx dwExStyle, - // IntPtr lpClassName, - // string? lpWindowName, - // WindowStyles dwStyle, - // int x, - // int y, - // int nWidth, - // int nHeight, - // IntPtr hWndParent, - // IntPtr hMenu, - // IntPtr hInstance, - // IntPtr lpParam); - - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern IntPtr CreateWindowEx( - WindowStylesEx dwExStyle, - string lpClassName, - string? lpWindowName, - WindowStyles dwStyle, - int x, - int y, - int nWidth, - int nHeight, - IntPtr hWndParent, - IntPtr hMenu, - IntPtr hInstance, - IntPtr lpParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - [DllImport("gdi32.dll")] - internal static extern bool DeleteDC(IntPtr hdc); - - [DllImport("gdi32.dll")] - internal static extern bool DeleteObject(IntPtr ho); - - [DllImport("user32.dll")] - internal static extern bool DestroyWindow(IntPtr hWnd); - - internal delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string? lpszWindow); - - [DllImport("user32.dll")] - internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); - - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); - // - internal enum Cursors - { - IDC_APPSTARTING = 32650, - IDC_ARROW = 32512, - IDC_CROSS = 32515, - IDC_HAND = 32649, - IDC_HELP = 32651, - IDC_IBEAM = 32513, - IDC_ICON = 32641, - IDC_NO = 32648, - IDC_SIZE = 32640, - IDC_SIZEALL = 32646, - IDC_SIZENESW = 32643, - IDC_SIZENS = 32645, - IDC_SIZENWSE = 32642, - IDC_SIZEWE = 32644, - IDC_UPARROW = 32516, - IDC_WAIT = 32514, - } - - [DllImport("user32.dll")] - internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags); - - // source for values: http://www.pinvoke.net/default.aspx/Enums/RedrawWindowFlags.html - internal enum RedrawWindowFlags : uint - { - // invalidation flags - RDW_ERASE = 0x4, - RDW_FRAME = 0x400, - RDW_INTERNALPAINT = 0x2, - RDW_INVALIDATE = 0x1, - // validation flags - RDW_NOERASE = 0x20, - RDW_NOFRAME = 0x800, - RDW_NOINTERNALPAINT = 0x10, - RDW_VALIDATE = 0x8, - // repainting flags - RDW_ERASENOW = 0x200, - RDW_UPDATENOW = 0x100, - // misc. control flags - RDW_ALLCHILDREN = 0x80, - RDW_NOCHILDREN = 0x40 - } - - [DllImport("gdi32.dll")] - internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); - - [DllImport("user32.dll")] - internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags); - - internal enum SetWindowPosFlags : uint - { - SWP_ASYNCWINDOWPOS = 0x4000, - SWP_DEFERERASE = 0x2000, - SWP_DRAWFRAME = 0x0020, - SWP_FRAMECHANGED = 0x0020, - SWP_HIDEWINDOW = 0x0080, - SWP_NOACTIVATE = 0x0010, - SWP_NOCOPYBITS = 0x0100, - SWP_NOMOVE = 0x0002, - SWP_NOOWNERZORDER = 0x0200, - SWP_NOREDRAW = 0x0008, - SWP_NOREPOSITION = 0x0200, - SWP_NOSENDCHANGING = 0x0400, - SWP_NOSIZE = 0x0001, - SWP_NOZORDER = 0x0004, - SWP_SHOWWINDOW = 0x0040 - } - - [DllImport("user32.dll")] - internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); - - [StructLayout(LayoutKind.Sequential)] - internal struct TRACKMOUSEEVENT - { - public uint cbSize; - public TMEFlags dwFlags; - public IntPtr hWnd; - public uint dwHoverTime; - - public TRACKMOUSEEVENT(TMEFlags dwFlags, IntPtr hWnd, uint dwHoverTime) - { - this.cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)); - this.dwFlags = dwFlags; - this.hWnd = hWnd; - this.dwHoverTime = dwHoverTime; - } - } - - // WinUser.h (Windows 10 1809 SDK) - internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; - - [Flags] - internal enum TMEFlags : uint - { - TME_CANCEL = 0x80000000, - TME_HOVER = 0x00000001, - TME_LEAVE = 0x00000002, - TME_NONCLIENT = 0x00000010, - TME_QUERY = 0x40000000 - } - - [StructLayout(LayoutKind.Sequential)] - internal struct BITMAPINFO - { - public BITMAPINFOHEADER bmiHeader; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // NOTE: in other implementations, this was represented as a uint instead (with 256 elements instead of 1 element) - public RGBQUAD[] bmiColors; - } - - [StructLayout(LayoutKind.Sequential)] - public struct BITMAPINFOHEADER - { - public uint biSize; - public int biWidth; - public int biHeight; - public ushort biPlanes; - public ushort biBitCount; - public BitmapCompressionType biCompression; - public uint biSizeImage; - public int biXPelsPerMeter; - public int biYPelsPerMeter; - public uint biClrUsed; - public uint biClrImportant; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct RGBQUAD - { - byte rgbBlue; - byte rgbGreen; - byte rgbRed; - byte rgbReserved; - } - - // wingdi.h (Windows 10 1809 SDK) - internal enum BitmapCompressionType : uint - { - BI_RGB = 0, - BI_RLE8 = 1, - BI_RLE4 = 2, - BI_BITFIELDS = 3, - BI_JPEG = 4, - BI_PNG = 5 - } - - internal static readonly IntPtr HWND_TOP = new IntPtr(0); - //internal static readonly IntPtr HWND_BOTTOM = new IntPtr(1); - //internal static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); - //internal static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); - - internal const int MA_NOACTIVATEANDEAT = 4; - - // - - internal const uint MK_LBUTTON = 0x0001; - internal const uint MK_RBUTTON = 0x0002; - - internal const uint S_OK = 0; - - // WinUser.h (Windows 10 1809 SDK) - public enum WindowMessage : uint - { - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-create - WM_CREATE = 0x0001, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-destroy - WM_DESTROY = 0x0002, - - // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-displaychange - WM_DISPLAYCHANGE = 0x007E, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-erasebkgnd - WM_ERASEBKGND = 0x0014, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown - WM_LBUTTONDOWN = 0x0201, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup - WM_LBUTTONUP = 0x0202, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate - WM_MOUSEACTIVATE = 0x0021, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseleave - WM_MOUSELEAVE = 0x02A3, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove - WM_MOUSEMOVE = 0x0200, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest - WM_NCHITTEST = 0x0084, - - // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-ncpaint - WM_NCPAINT = 0x0085, - - // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-paint - WM_PAINT = 0x000F, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged - WM_WINDOWPOSCHANGED = 0x0047, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanging - WM_WINDOWPOSCHANGING = 0x0046, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttondown - WM_RBUTTONDOWN = 0x0204, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttonup - WM_RBUTTONUP = 0x0205, - - // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-setcursor - WM_SETCURSOR = 0x0020, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-size - WM_SIZE = 0x0005, - } - - [Flags] - internal enum WindowStyles : uint - { - WS_BORDER = 0x00800000, - WS_CAPTION = 0x00C00000, - WS_CHILD = 0x40000000, - WS_CHILDWINDOW = 0x40000000, - WS_CLIPCHILDREN = 0x02000000, - WS_CLIPSIBLINGS = 0x04000000, - WS_DISABLED = 0x08000000, - WS_DLGFRAME = 0x00400000, - WS_GROUP = 0x00020000, - WS_HSCROLL = 0x00100000, - WS_ICONIC = 0x20000000, - WS_MAXIMIZE = 0x01000000, - WS_MAXIMIZEBOX = 0x00010000, - WS_MINIMIZE = 0x20000000, - WS_MINIMIZEBOX = 0x00020000, - WS_OVERLAPPED = 0x00000000, - WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, - WS_POPUP = 0x80000000, - WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, - WS_SIZEBOX = 0x00040000, - WS_SYSMENU = 0x00080000, - WS_TABSTOP = 0x00010000, - WS_THICKFRAME = 0x00040000, - WS_TILED = 0x00000000, - WS_TILEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, - WS_VISIBLE = 0x10000000, - WS_VSCROLL = 0x00200000 - } - - [Flags] - internal enum WindowStylesEx : uint - { - WS_EX_ACCEPTFILES = 0x00000010, - WS_EX_APPWINDOW = 0x00040000, - WS_EX_CLIENTEDGE = 0x00000200, - WS_EX_COMPOSITED = 0x02000000, - WS_EX_CONTEXTHELP = 0x00000400, - WS_EX_CONTROLPARENT = 0x00010000, - WS_EX_DLGMODALFRAME = 0x00000001, - WS_EX_LAYERED = 0x00080000, - WS_EX_LAYOUTRTL = 0x00400000, - WS_EX_LEFT = 0x00000000, - WS_EX_LEFTSCROLLBAR = 0x00004000, - WS_EX_LTRREADING = 0x00000000, - WS_EX_MDICHILD = 0x00000040, - WS_EX_NOACTIVATE = 0x08000000, - WS_EX_NOINHERITLAYOUT = 0x00100000, - WS_EX_NOPARENTNOTIFY = 0x00000004, - WS_EX_NOREDIRECTIONBITMAP = 0x00200000, - WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, - WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, - WS_EX_RIGHT = 0x00001000, - WS_EX_RIGHTSCROLLBAR = 0x00000000, - WS_EX_RTLREADING = 0x00002000, - WS_EX_STATICEDGE = 0x00020000, - WS_EX_TOOLWINDOW = 0x00000080, - WS_EX_TOPMOST = 0x00000008, - WS_EX_TRANSPARENT = 0x00000020, - WS_EX_WINDOWEDGE = 0x00000100 - } - - internal const uint TTF_SUBCLASS = 0x0010; - - internal const uint TTS_ALWAYSTIP = 0x01; - //internal const uint TTS_NOPREFIX = 0x02; - //internal const uint TTS_BALLOON = 0x40; - - #endregion - - #region Window Painting - - // NOTE: per pinvoke.net, this function is called "GdiAlphaBlend" even though the Microsoft documentation calls it AlphaBlend - [DllImport("gdi32.dll", EntryPoint = "GdiAlphaBlend")] - internal static extern bool AlphaBlend(IntPtr hdcDest, int xOriginDest, int yOriginDest, int wDest, int hDest, IntPtr hdcSrc, int xOriginSrc, int yOriginSrc, int wSrc, int hSrc, BLENDFUNCTION ftn); - - [StructLayout(LayoutKind.Sequential)] - public struct BLENDFUNCTION - { - public byte BlendOp; - public byte BlendFlags; - public byte SourceConstantAlpha; - public byte AlphaFormat; - } - - internal const byte AC_SRC_OVER = 0x00; - //internal const byte AC_SRC_ALPHA = 0x01; - - internal const uint DIB_RGB_COLORS = 0; - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-beginbufferedpaint - [DllImport("uxtheme.dll")] - internal static extern IntPtr BeginBufferedPaint(IntPtr hdcTarget, [In] ref RECT prcTarget, BP_BUFFERFORMAT dwFormat, IntPtr pPaintParams, out IntPtr phdc); - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/ne-uxtheme-bp_bufferformat - internal enum BP_BUFFERFORMAT : uint - { - BPBF_COMPATIBLEBITMAP, - BPBF_DIB, - BPBF_TOPDOWNDIB, - BPBF_TOPDOWNMONODIB - } - - [DllImport("user32.dll")] - internal static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintclear - [DllImport("uxtheme.dll")] - internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, ref RECT prc); - // - [DllImport("uxtheme.dll")] - internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, IntPtr prc); - - [DllImport("uxtheme.dll")] - internal static extern int BufferedPaintInit(); - - [DllImport("uxtheme.dll")] - internal static extern int BufferedPaintUnInit(); - - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawiconex - [DllImport("user32.dll")] - internal static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, DrawIconFlags diFlags); - - [Flags] - internal enum DrawIconFlags : uint - { - DI_COMPAT = 0x0004, - DI_DEFAULTSIZE = 0x0008, - DI_IMAGE = 0x0002, - DI_MASK = 0x0001, - DI_NOMIRROR = 0x0010, - DI_NORMAL = DI_IMAGE | DI_MASK // 0x0003 - } - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-endbufferedpaint - [DllImport("uxtheme.dll")] - internal static extern uint EndBufferedPaint(IntPtr hBufferedPaint, bool fUpdateTarget); - - [DllImport("user32.dll")] - internal static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint); - - // - - [StructLayout(LayoutKind.Sequential)] - internal struct PAINTSTRUCT - { - public IntPtr hdc; - public bool fErase; - public RECT rcPaint; - public bool fRestore; - public bool fIncUpdate; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - public byte[] rgbReserved; - } - #endregion + #region Win32 error codes + + public const uint ERROR_SUCCESS = 0; + + #endregion + + #region Window Positioning + +#pragma warning disable CS0660 // Code should override Object.Equals(object o) when defining == and != operations, but the legacy struct RECT does not. +#pragma warning disable CS0661 // Code should override Object.GetHashCode() when defining == and != operations, but the legacy struct RECT does not. + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public PInvoke.RECT ToPInvokeRect() + { + return new PInvoke.RECT() { left = this.Left, top = this.Top, right = this.Right, bottom = this.Bottom }; + } + + /// + /// Creates a win32 RECT from a .NET Rect. + /// + /// The rectangle. + public RECT(System.Windows.Rect rect) + { + this.Left = (int)rect.Left; + this.Top = (int)rect.Top; + this.Right = (int)rect.Right; + this.Bottom = (int)rect.Bottom; + } + + public static RECT Empty + { + get + { + return new RECT(new System.Windows.Rect(0, 0, 0, 0)); + } + } + + public bool HasNonZeroWidthOrHeight() + { + return ((this.Left == this.Right) || (this.Top == this.Bottom)); + } + + public bool IsInside(RECT rect) + { + return ((this.Left >= rect.Left) && (this.Right <= rect.Right) && (this.Top >= rect.Top) && (this.Bottom <= rect.Bottom)); + } + + public bool Intersects(RECT rect) + { + bool overlapsHorizontally = false; + bool overlapsVertically = false; + + // horizontal check + if ((this.Right > rect.Left) && (this.Left < rect.Right)) + { + // partially or fully overlaps horizontally + overlapsHorizontally = true; + } + + // vertical check + if ((this.Bottom > rect.Top) && (this.Top < rect.Bottom)) + { + // partially or fully overlaps vertically + overlapsVertically = true; + } + + if ((overlapsHorizontally == true) && (overlapsVertically == true)) + { + return true; + } + + // if we could not find overlap, then return false + return false; + } + + public static bool operator ==(RECT lhs, RECT rhs) + { + if ((lhs.Left == rhs.Left) && + (lhs.Top == rhs.Top) && + (lhs.Right == rhs.Right) && + (lhs.Bottom == rhs.Bottom)) + { + return true; + } + else + { + return false; + } + } + + public static bool operator !=(RECT lhs, RECT rhs) + { + if ((lhs.Left != rhs.Left) || + (lhs.Top != rhs.Top) || + (lhs.Right != rhs.Right) || + (lhs.Bottom != rhs.Bottom)) + { + return true; + } + else + { + return false; + } + } + } +#pragma warning restore CS0660 // Code should override Object.Equals(object o) when defining == and != operations, but the legacy struct RECT does not. +#pragma warning restore CS0661 // Code should override Object.GetHashCode() when defining == and != operations, but the legacy struct RECT does not. + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + this.X = x; + this.Y = y; + } + + public POINT(System.Windows.Point pt) : this((int)pt.X, (int)pt.Y) + { + } + + public System.Windows.Point ToPoint() + { + return new System.Windows.Point(this.X, this.Y); + } + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msllhookstruct + [StructLayout(LayoutKind.Sequential)] + internal struct MSLLHOOKSTRUCT + { + public PInvoke.POINT pt; + // NOTE: the mouseData DWORD is apparently used as a signed integer (rather than as a uint) + public int mouseData; + public uint flags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [DllImport("user32.dll")] + internal static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll")] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + + #endregion + + #region Window Creation and Management + + [DllImport("gdi32.dll")] + internal static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + internal static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO pbmi, uint usage, out IntPtr ppvBits, IntPtr hSection, uint offset); + + //[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + //internal static extern IntPtr CreateWindowEx( + // WindowStylesEx dwExStyle, + // IntPtr lpClassName, + // string? lpWindowName, + // WindowStyles dwStyle, + // int x, + // int y, + // int nWidth, + // int nHeight, + // IntPtr hWndParent, + // IntPtr hMenu, + // IntPtr hInstance, + // IntPtr lpParam); + // + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr CreateWindowEx( + WindowStylesEx dwExStyle, + string lpClassName, + string? lpWindowName, + WindowStyles dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("gdi32.dll")] + internal static extern bool DeleteDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + internal static extern bool DeleteObject(IntPtr ho); + + [DllImport("user32.dll")] + internal static extern bool DestroyWindow(IntPtr hWnd); + + internal delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string? lpszWindow); + + //[DllImport("user32.dll")] + //internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); + // + internal enum Cursors + { + IDC_APPSTARTING = 32650, + IDC_ARROW = 32512, + IDC_CROSS = 32515, + IDC_HAND = 32649, + IDC_HELP = 32651, + IDC_IBEAM = 32513, + IDC_ICON = 32641, + IDC_NO = 32648, + IDC_SIZE = 32640, + IDC_SIZEALL = 32646, + IDC_SIZENESW = 32643, + IDC_SIZENS = 32645, + IDC_SIZENWSE = 32642, + IDC_SIZEWE = 32644, + IDC_UPARROW = 32516, + IDC_WAIT = 32514, + } + + [DllImport("user32.dll")] + internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags); + + // source for values: http://www.pinvoke.net/default.aspx/Enums/RedrawWindowFlags.html + internal enum RedrawWindowFlags : uint + { + // invalidation flags + RDW_ERASE = 0x4, + RDW_FRAME = 0x400, + RDW_INTERNALPAINT = 0x2, + RDW_INVALIDATE = 0x1, + // validation flags + RDW_NOERASE = 0x20, + RDW_NOFRAME = 0x800, + RDW_NOINTERNALPAINT = 0x10, + RDW_VALIDATE = 0x8, + // repainting flags + RDW_ERASENOW = 0x200, + RDW_UPDATENOW = 0x100, + // misc. control flags + RDW_ALLCHILDREN = 0x80, + RDW_NOCHILDREN = 0x40 + } + + [DllImport("gdi32.dll")] + internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); + + [DllImport("user32.dll")] + internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags); + + internal enum SetWindowPosFlags : uint + { + SWP_ASYNCWINDOWPOS = 0x4000, + SWP_DEFERERASE = 0x2000, + SWP_DRAWFRAME = 0x0020, + SWP_FRAMECHANGED = 0x0020, + SWP_HIDEWINDOW = 0x0080, + SWP_NOACTIVATE = 0x0010, + SWP_NOCOPYBITS = 0x0100, + SWP_NOMOVE = 0x0002, + SWP_NOOWNERZORDER = 0x0200, + SWP_NOREDRAW = 0x0008, + SWP_NOREPOSITION = 0x0200, + SWP_NOSENDCHANGING = 0x0400, + SWP_NOSIZE = 0x0001, + SWP_NOZORDER = 0x0004, + SWP_SHOWWINDOW = 0x0040 + } + + [DllImport("user32.dll")] + internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); + + [StructLayout(LayoutKind.Sequential)] + internal struct TRACKMOUSEEVENT + { + public uint cbSize; + public TMEFlags dwFlags; + public IntPtr hWnd; + public uint dwHoverTime; + + public TRACKMOUSEEVENT(TMEFlags dwFlags, IntPtr hWnd, uint dwHoverTime) + { + this.cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)); + this.dwFlags = dwFlags; + this.hWnd = hWnd; + this.dwHoverTime = dwHoverTime; + } + } + + // WinUser.h (Windows 10 1809 SDK) + internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; + + [Flags] + internal enum TMEFlags : uint + { + TME_CANCEL = 0x80000000, + TME_HOVER = 0x00000001, + TME_LEAVE = 0x00000002, + TME_NONCLIENT = 0x00000010, + TME_QUERY = 0x40000000 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct BITMAPINFO + { + public BITMAPINFOHEADER bmiHeader; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // NOTE: in other implementations, this was represented as a uint instead (with 256 elements instead of 1 element) + public RGBQUAD[] bmiColors; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BITMAPINFOHEADER + { + public uint biSize; + public int biWidth; + public int biHeight; + public ushort biPlanes; + public ushort biBitCount; + public BitmapCompressionType biCompression; + public uint biSizeImage; + public int biXPelsPerMeter; + public int biYPelsPerMeter; + public uint biClrUsed; + public uint biClrImportant; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct RGBQUAD + { + byte rgbBlue; + byte rgbGreen; + byte rgbRed; + byte rgbReserved; + } + + // wingdi.h (Windows 10 1809 SDK) + internal enum BitmapCompressionType : uint + { + BI_RGB = 0, + BI_RLE8 = 1, + BI_RLE4 = 2, + BI_BITFIELDS = 3, + BI_JPEG = 4, + BI_PNG = 5 + } + + internal static readonly IntPtr HWND_TOP = new IntPtr(0); + //internal static readonly IntPtr HWND_BOTTOM = new IntPtr(1); + //internal static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + //internal static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); + + internal const int MA_NOACTIVATEANDEAT = 4; + + // + + internal const uint MK_LBUTTON = 0x0001; + internal const uint MK_RBUTTON = 0x0002; + + internal const uint S_OK = 0; + + // WinUser.h (Windows 10 1809 SDK) + public enum WindowMessage : uint + { + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-create + WM_CREATE = 0x0001, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-destroy + WM_DESTROY = 0x0002, + + // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-displaychange + WM_DISPLAYCHANGE = 0x007E, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-erasebkgnd + WM_ERASEBKGND = 0x0014, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown + WM_LBUTTONDOWN = 0x0201, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup + WM_LBUTTONUP = 0x0202, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate + WM_MOUSEACTIVATE = 0x0021, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseleave + WM_MOUSELEAVE = 0x02A3, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove + WM_MOUSEMOVE = 0x0200, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest + WM_NCHITTEST = 0x0084, + + // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-ncpaint + WM_NCPAINT = 0x0085, + + // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-paint + WM_PAINT = 0x000F, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged + WM_WINDOWPOSCHANGED = 0x0047, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanging + WM_WINDOWPOSCHANGING = 0x0046, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttondown + WM_RBUTTONDOWN = 0x0204, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttonup + WM_RBUTTONUP = 0x0205, + + // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-setcursor + WM_SETCURSOR = 0x0020, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-size + WM_SIZE = 0x0005, + } + + [Flags] + internal enum WindowStyles : uint + { + WS_BORDER = 0x00800000, + WS_CAPTION = 0x00C00000, + WS_CHILD = 0x40000000, + WS_CHILDWINDOW = 0x40000000, + WS_CLIPCHILDREN = 0x02000000, + WS_CLIPSIBLINGS = 0x04000000, + WS_DISABLED = 0x08000000, + WS_DLGFRAME = 0x00400000, + WS_GROUP = 0x00020000, + WS_HSCROLL = 0x00100000, + WS_ICONIC = 0x20000000, + WS_MAXIMIZE = 0x01000000, + WS_MAXIMIZEBOX = 0x00010000, + WS_MINIMIZE = 0x20000000, + WS_MINIMIZEBOX = 0x00020000, + WS_OVERLAPPED = 0x00000000, + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_POPUP = 0x80000000, + WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, + WS_SIZEBOX = 0x00040000, + WS_SYSMENU = 0x00080000, + WS_TABSTOP = 0x00010000, + WS_THICKFRAME = 0x00040000, + WS_TILED = 0x00000000, + WS_TILEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_VISIBLE = 0x10000000, + WS_VSCROLL = 0x00200000 + } + + [Flags] + internal enum WindowStylesEx : uint + { + WS_EX_ACCEPTFILES = 0x00000010, + WS_EX_APPWINDOW = 0x00040000, + WS_EX_CLIENTEDGE = 0x00000200, + WS_EX_COMPOSITED = 0x02000000, + WS_EX_CONTEXTHELP = 0x00000400, + WS_EX_CONTROLPARENT = 0x00010000, + WS_EX_DLGMODALFRAME = 0x00000001, + WS_EX_LAYERED = 0x00080000, + WS_EX_LAYOUTRTL = 0x00400000, + WS_EX_LEFT = 0x00000000, + WS_EX_LEFTSCROLLBAR = 0x00004000, + WS_EX_LTRREADING = 0x00000000, + WS_EX_MDICHILD = 0x00000040, + WS_EX_NOACTIVATE = 0x08000000, + WS_EX_NOINHERITLAYOUT = 0x00100000, + WS_EX_NOPARENTNOTIFY = 0x00000004, + WS_EX_NOREDIRECTIONBITMAP = 0x00200000, + WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, + WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, + WS_EX_RIGHT = 0x00001000, + WS_EX_RIGHTSCROLLBAR = 0x00000000, + WS_EX_RTLREADING = 0x00002000, + WS_EX_STATICEDGE = 0x00020000, + WS_EX_TOOLWINDOW = 0x00000080, + WS_EX_TOPMOST = 0x00000008, + WS_EX_TRANSPARENT = 0x00000020, + WS_EX_WINDOWEDGE = 0x00000100 + } + + #endregion + + #region Window Painting + + // NOTE: per pinvoke.net, this function is called "GdiAlphaBlend" even though the Microsoft documentation calls it AlphaBlend + [DllImport("gdi32.dll", EntryPoint = "GdiAlphaBlend")] + internal static extern bool AlphaBlend(IntPtr hdcDest, int xOriginDest, int yOriginDest, int wDest, int hDest, IntPtr hdcSrc, int xOriginSrc, int yOriginSrc, int wSrc, int hSrc, BLENDFUNCTION ftn); + + [StructLayout(LayoutKind.Sequential)] + public struct BLENDFUNCTION + { + public byte BlendOp; + public byte BlendFlags; + public byte SourceConstantAlpha; + public byte AlphaFormat; + } + + internal const byte AC_SRC_OVER = 0x00; + //internal const byte AC_SRC_ALPHA = 0x01; + + internal const uint DIB_RGB_COLORS = 0; + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-beginbufferedpaint + [DllImport("uxtheme.dll")] + internal static extern IntPtr BeginBufferedPaint(IntPtr hdcTarget, [In] ref RECT prcTarget, BP_BUFFERFORMAT dwFormat, IntPtr pPaintParams, out IntPtr phdc); + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/ne-uxtheme-bp_bufferformat + internal enum BP_BUFFERFORMAT : uint + { + BPBF_COMPATIBLEBITMAP, + BPBF_DIB, + BPBF_TOPDOWNDIB, + BPBF_TOPDOWNMONODIB + } + + [DllImport("user32.dll")] + internal static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintclear + [DllImport("uxtheme.dll")] + internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, ref RECT prc); + // + [DllImport("uxtheme.dll")] + internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, IntPtr prc); + + [DllImport("uxtheme.dll")] + internal static extern int BufferedPaintInit(); + + [DllImport("uxtheme.dll")] + internal static extern int BufferedPaintUnInit(); + + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawiconex + [DllImport("user32.dll")] + internal static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, DrawIconFlags diFlags); + + [Flags] + internal enum DrawIconFlags : uint + { + DI_COMPAT = 0x0004, + DI_DEFAULTSIZE = 0x0008, + DI_IMAGE = 0x0002, + DI_MASK = 0x0001, + DI_NOMIRROR = 0x0010, + DI_NORMAL = DI_IMAGE | DI_MASK // 0x0003 + } + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-endbufferedpaint + [DllImport("uxtheme.dll")] + internal static extern uint EndBufferedPaint(IntPtr hBufferedPaint, bool fUpdateTarget); + + [DllImport("user32.dll")] + internal static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint); + + // + + [StructLayout(LayoutKind.Sequential)] + internal struct PAINTSTRUCT + { + public IntPtr hdc; + public bool fErase; + public RECT rcPaint; + public bool fRestore; + public bool fIncUpdate; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public byte[] rgbReserved; + } + + #endregion } diff --git a/Morphic.Controls/TrayButton/Windows10/TrayButton.cs b/Morphic.Controls/TrayButton/Windows10/TrayButton.cs index 8d276526..5fb0e8a4 100644 --- a/Morphic.Controls/TrayButton/Windows10/TrayButton.cs +++ b/Morphic.Controls/TrayButton/Windows10/TrayButton.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -35,1438 +35,1469 @@ namespace Morphic.Controls.TrayButton.Windows10; internal class TrayButton : IDisposable { - private System.Drawing.Icon? _icon = null; - private string? _text = null; - private bool _visible = false; - - private TrayButtonNativeWindow? _nativeWindow = null; - - //private bool _highContrastModeIsOn_Cached = false; - - public event System.Windows.Forms.MouseEventHandler? MouseUp; - - internal TrayButton() - { - } - - public void Dispose() - { - this.DestroyNativeWindow(); - } - - /// The icon for the tray button - public System.Drawing.Icon? Icon - { - get - { - return _icon; - } - set - { - _icon = value; - - _nativeWindow?.SetIcon(_icon); - } - } - - /// Tooltip for the tray button. - public string? Text - { - get - { - return _text; - } - set - { - _text = value; - - _nativeWindow?.SetText(_text); - } - } - - /// Show or hide the tray button. - public bool Visible - { - get - { - return _visible; - } - set - { - _visible = value; - - if (_visible == true) - { - if (_nativeWindow is null) + private bool disposedValue; + + private System.Drawing.Icon? _icon = null; + private string? _text = null; + private bool _visible = false; + + private TrayButtonNativeWindow? _nativeWindow = null; + + //private bool _highContrastModeIsOn_Cached = false; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + internal TrayButton() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + this.DestroyManagedNativeWindow(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrayButton() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// The icon for the tray button + public System.Drawing.Icon? Icon + { + get + { + return _icon; + } + set + { + _icon = value; + + _nativeWindow?.SetIcon(_icon); + } + } + + /// Tooltip for the tray button. + public string? Text + { + get + { + return _text; + } + set + { + _text = value; + + _nativeWindow?.SetText(_text); + } + } + + /// Show or hide the tray button. + public bool Visible + { + get + { + return _visible; + } + set + { + _visible = value; + + if (_visible == true) + { + if (_nativeWindow is null) + { + CreateNativeWindow(); + } + } + else //if (_visible == false) + { + if (_nativeWindow is not null) + { + this.DestroyManagedNativeWindow(); + } + } + } + } + + // NOTE: this throws an exception if it fails to create the native window + private void CreateNativeWindow() + { + // if the tray button window already exists; it cannot be created again + if (_nativeWindow is not null) + { + throw new InvalidOperationException(); + } + + // find the window handle of the Windows taskbar + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) + { + // could not find taskbar + throw new Exception("Could not find taskbar"); + } + + /* TODO: consider cached the current DPI of the taskbar (to track, in case the taskbar DPI changes in the future); we currently calculate the icon size based on + * the height/width of the window, so this check may not be necessary */ + + //// cache the current high contrast on/off state (to track) + //_highContrastModeIsOn_Cached = IsHighContrastModeOn(); + + // create the native window + var nativeWindow = new TrayButtonNativeWindow(this); + + // initialize the native window; note that we have separated "initialize" into a separate function so that our constructor doesn't throw exceptions on failure + try + { + nativeWindow.Initialize(taskbarHandle); + } + catch (System.ComponentModel.Win32Exception/* ex*/) + { + // TODO: consider what exceptions we could get here, how to handle them and how to bubble them up to our caller, etc. + throw; + } + catch (InvalidOperationException) + { + throw; + } + + // set the icon for the native window + nativeWindow.SetIcon(_icon); + // set the (tooltip) text for the native window + nativeWindow.SetText(_text); + + // store the reference to our new native window + _nativeWindow = nativeWindow; + } + + private void DestroyManagedNativeWindow() + { + _nativeWindow?.Dispose(); + _nativeWindow = null; + } + + //private bool IsHighContrastModeOn() + //{ + // var highContrastIsOn = (Spi.Instance.GetHighContrast() & Spi.HighContrastOptions.HCF_HIGHCONTRASTON) != 0; + // return highContrastIsOn; + //} + + #region Tray Button (Native Window) + + private class TrayButtonNativeWindow : System.Windows.Forms.NativeWindow, IDisposable + { + private TrayButton _owner; + + private IntPtr _tooltipWindowHandle = IntPtr.Zero; + private IntPtr _iconHandle = IntPtr.Zero; + + private string? _tooltipText = null; + private bool _tooltipInfoAdded = false; + + private System.Threading.Timer? _trayButtonPositionCheckupTimer; + private int _trayButtonPositionCheckupTimerCounter = 0; + + [Flags] + private enum TrayButtonVisualStateFlags + { + None = 0, + Hover = 1, + LeftButtonPressed = 2, + RightButtonPressed = 4 + } + private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; + + private Morphic.Controls.TrayButton.Windows10.WindowsNative.MouseWindowMessageHook? _mouseHook = null; + + internal TrayButtonNativeWindow(TrayButton owner) + { + _owner = owner; + } + + public void Initialize(IntPtr taskbarHandle) + { + const string nativeWindowClassName = "Morphic-TrayButton"; + + // register our custom native window class + var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new PInvokeExtensions.WndProc(this.WndProcCallback)); + var lpWndClass = new PInvokeExtensions.WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(typeof(PInvokeExtensions.WNDCLASSEX)), + lpfnWndProc = pointerToWndProcCallback, + lpszClassName = nativeWindowClassName, + hCursor = LegacyWindowsApi.LoadCursor(IntPtr.Zero, (int)LegacyWindowsApi.Cursors.IDC_ARROW) + }; + + var registerClassResult = PInvokeExtensions.RegisterClassEx(ref lpWndClass); + if (registerClassResult == 0) + { + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + } + + var windowParams = new System.Windows.Forms.CreateParams(); + windowParams.ExStyle = (int)LegacyWindowsApi.WindowStylesEx.WS_EX_TOOLWINDOW; + /* NOTE: as we want to be able to ensure that we're referencing the exact class we just registered, we pass the RegisterClassEx results into the + * CreateWindow function (and we encode that result as a ushort here in a proprietary way) */ + windowParams.ClassName = registerClassResult.ToString(); // nativeWindowClassName; + //windowParams.Caption = nativeWindowClassName; + windowParams.Style = (int)(LegacyWindowsApi.WindowStyles.WS_VISIBLE | LegacyWindowsApi.WindowStyles.WS_CHILD | LegacyWindowsApi.WindowStyles.WS_CLIPSIBLINGS | LegacyWindowsApi.WindowStyles.WS_TABSTOP); + windowParams.X = 0; + windowParams.Y = 0; + windowParams.Width = 32; + windowParams.Height = 40; + windowParams.Parent = taskbarHandle; + // + // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException, or System.ComponentModel.Win32Exception + this.CreateHandle(windowParams); + + // create the tooltip window (although we won't provide it with any actual text until/unless the text is set + this.CreateTooltipWindow(); + + // subscribe to display settings changes (so that we know when the screen resolution changes, so that we can reposition our button) + Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + + // if the user is using Windows 11, create a mouse message hook (so we can capture the mousemove and click events over our taskbar icon) + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _mouseHook = new Morphic.Controls.TrayButton.Windows10.WindowsNative.MouseWindowMessageHook(); + _mouseHook.WndProcEvent += _mouseHook_WndProcEvent; + } + + // position the tray button in its initial position + // NOTE: the button has no icon at this point; if we want to move this logic to the Icon set routine, + // that's reasonable, but we'd need to think through any side-effects (and we'd need to do this here anyway + // if an icon had already been set prior to .Initialize being called) + //if (_iconHandle != IntPtr.Zero) + //{ + this.PositionTrayButton(); + //} + } + + // NOTE: this function is somewhat redundant and is provided to support Windows 11; we should refactor all of this code to handle window messages centrally + private void _mouseHook_WndProcEvent(object? sender, Morphic.Controls.TrayButton.Windows10.WindowsNative.MouseWindowMessageHook.WndProcEventArgs e) + { + // TODO: we should ensure that calls are queued and then called from a sequential thread (ideally a UI dispatch thread) + switch ((LegacyWindowsApi.WindowMessage)e.Message) + { + case LegacyWindowsApi.WindowMessage.WM_LBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + break; + case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); { - CreateNativeWindow(); + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, e.X, e.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); } - } - else //if (_visible == false) - { - if (_nativeWindow is not null) + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSELEAVE: + // the cursor has left our tray button's window area; remove the hover state from our visual state + _visualState &= ~TrayButtonVisualStateFlags.Hover; + // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here + // (and then we check them again when the mouse moves back over the button) + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: + // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) + // + // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because + // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during + // mousemove so that we can re-enable the pressed state if/where appropriate. + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0)) { - DestroyNativeWindow(); + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); } - } - } - } - - // NOTE: this throws an exception if it fails to create the native window - private void CreateNativeWindow() - { - // if the tray button window already exists; it cannot be created again - if (_nativeWindow is not null) - { - throw new InvalidOperationException(); - } - - // find the window handle of the Windows taskbar - var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) - { - // could not find taskbar - throw new Exception("Could not find taskbar"); - } - - /* TODO: consider cached the current DPI of the taskbar (to track, in case the taskbar DPI changes in the future); we currently calculate the icon size based on - * the height/width of the window, so this check may not be necessary */ - - //// cache the current high contrast on/off state (to track) - //_highContrastModeIsOn_Cached = IsHighContrastModeOn(); - - // create the native window - var nativeWindow = new TrayButtonNativeWindow(this); - - // initialize the native window; note that we have separated "initialize" into a separate function so that our constructor doesn't throw exceptions on failure - try - { - nativeWindow.Initialize(taskbarHandle); - } - catch (System.ComponentModel.Win32Exception/* ex*/) - { - // TODO: consider what exceptions we could get here, how to handle them and how to bubble them up to our caller, etc. - throw; - } - catch (InvalidOperationException) - { - throw; - } - - // set the icon for the native window - nativeWindow.SetIcon(_icon); - // set the (tooltip) text for the native window - nativeWindow.SetText(_text); - - // store the reference to our new native window - _nativeWindow = nativeWindow; - } - - private void DestroyNativeWindow() - { - _nativeWindow?.Dispose(); - _nativeWindow = null; - } - - //private bool IsHighContrastModeOn() - //{ - // var highContrastIsOn = (Spi.Instance.GetHighContrast() & Spi.HighContrastOptions.HCF_HIGHCONTRASTON) != 0; - // return highContrastIsOn; - //} - - #region Tray Button (Native Window) - - private class TrayButtonNativeWindow : System.Windows.Forms.NativeWindow, IDisposable - { - private TrayButton _owner; - - private IntPtr _tooltipWindowHandle = IntPtr.Zero; - private IntPtr _iconHandle = IntPtr.Zero; - - private string? _tooltipText = null; - private bool _tooltipInfoAdded = false; - - private System.Threading.Timer? _trayButtonPositionCheckupTimer; - private int _trayButtonPositionCheckupTimerCounter = 0; - - [Flags] - private enum TrayButtonVisualStateFlags - { - None = 0, - Hover = 1, - LeftButtonPressed = 2, - RightButtonPressed = 4 - } - private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; - - private Morphic.WindowsNative.WindowMessageHooks.MouseWindowMessageHook? _mouseHook = null; - - internal TrayButtonNativeWindow(TrayButton owner) - { - _owner = owner; - } - - public void Initialize(IntPtr taskbarHandle) - { - const string nativeWindowClassName = "Morphic-TrayButton"; - - // register our custom native window class - var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new WindowsApi.WndProc(this.WndProcCallback)); - var lpWndClass = new WindowsApi.WNDCLASSEX - { - cbSize = (uint)Marshal.SizeOf(typeof(WindowsApi.WNDCLASSEX)), - lpfnWndProc = pointerToWndProcCallback, - lpszClassName = nativeWindowClassName, - hCursor = LegacyWindowsApi.LoadCursor(IntPtr.Zero, (int)LegacyWindowsApi.Cursors.IDC_ARROW) - }; - - var registerClassResult = WindowsApi.RegisterClassEx(ref lpWndClass); - if (registerClassResult == 0) - { - throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); - } - - var windowParams = new System.Windows.Forms.CreateParams(); - windowParams.ExStyle = (int)LegacyWindowsApi.WindowStylesEx.WS_EX_TOOLWINDOW; - /* NOTE: as we want to be able to ensure that we're referencing the exact class we just registered, we pass the RegisterClassEx results into the - * CreateWindow function (and we encode that result as a ushort here in a proprietary way) */ - windowParams.ClassName = registerClassResult.ToString(); // nativeWindowClassName; - //windowParams.Caption = nativeWindowClassName; - windowParams.Style = (int)(LegacyWindowsApi.WindowStyles.WS_VISIBLE | LegacyWindowsApi.WindowStyles.WS_CHILD | LegacyWindowsApi.WindowStyles.WS_CLIPSIBLINGS | LegacyWindowsApi.WindowStyles.WS_TABSTOP); - windowParams.X = 0; - windowParams.Y = 0; - windowParams.Width = 32; - windowParams.Height = 40; - windowParams.Parent = taskbarHandle; - // - // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException, or System.ComponentModel.Win32Exception - this.CreateHandle(windowParams); - - // create the tooltip window (although we won't provide it with any actual text until/unless the text is set - this.CreateTooltipWindow(); - - // subscribe to display settings changes (so that we know when the screen resolution changes, so that we can reposition our button) - Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; - - // if the user is using Windows 11, create a mouse message hook (so we can capture the mousemove and click events over our taskbar icon) - if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _mouseHook = new Morphic.WindowsNative.WindowMessageHooks.MouseWindowMessageHook(); - _mouseHook.WndProcEvent += _mouseHook_WndProcEvent; - } - - // position the tray button in its initial position - // NOTE: the button has no icon at this point; if we want to move this logic to the Icon set routine, - // that's reasonable, but we'd need to think through any side-effects (and we'd need to do this here anyway - // if an icon had already been set prior to .Initialize being called) - //if (_iconHandle != IntPtr.Zero) - //{ - this.PositionTrayButton(); - //} - } - - // NOTE: this function is somewhat redundant and is provided to support Windows 11; we should refactor all of this code to handle window messages centrally - private void _mouseHook_WndProcEvent(object? sender, Morphic.WindowsNative.WindowMessageHooks.MouseWindowMessageHook.WndProcEventArgs e) - { - // TODO: we should ensure that calls are queued and then called from a sequential thread (ideally a UI dispatch thread) - switch ((LegacyWindowsApi.WindowMessage)e.Message) - { - case LegacyWindowsApi.WindowMessage.WM_LBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - break; - case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - { - var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, e.X, e.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - break; - case LegacyWindowsApi.WindowMessage.WM_MOUSELEAVE: - // the cursor has left our tray button's window area; remove the hover state from our visual state - _visualState &= ~TrayButtonVisualStateFlags.Hover; - // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here - // (and then we check them again when the mouse moves back over the button) - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - break; - case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: - // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) - // - // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because - // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during - // mousemove so that we can re-enable the pressed state if/where appropriate. - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0)) - { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - } - if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0)) - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - } - // - break; - case LegacyWindowsApi.WindowMessage.WM_RBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - break; - case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - { - var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, e.X, e.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - break; - } - } - - internal void SetText(string? text) - { - _tooltipText = text; - this.UpdateTooltipTextAndTracking(); - } - - private void CreateTooltipWindow() - { - if (_tooltipWindowHandle != IntPtr.Zero) - { - // tooltip window already exists - return; - } - - _tooltipWindowHandle = LegacyWindowsApi.CreateWindowEx( - 0 /* no styles */, - WindowsApi.TOOLTIPS_CLASS, - null, - LegacyWindowsApi.WindowStyles.WS_POPUP | (LegacyWindowsApi.WindowStyles)WindowsApi.TTS_ALWAYSTIP, - WindowsApi.CW_USEDEFAULT, - WindowsApi.CW_USEDEFAULT, - WindowsApi.CW_USEDEFAULT, - WindowsApi.CW_USEDEFAULT, - this.Handle, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - if (_tooltipWindowHandle == IntPtr.Zero) - { - Debug.Assert(false, "Could not create tooltip window"); - } - - this.UpdateTooltipTextAndTracking(); - } - - private void DestroyTooltipWindow() - { - // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. - _tooltipText = null; - this.UpdateTooltipTextAndTracking(); - - LegacyWindowsApi.DestroyWindow(_tooltipWindowHandle); - _tooltipWindowHandle = IntPtr.Zero; - } - - private void UpdateTooltipTextAndTracking() - { - if (_tooltipWindowHandle == IntPtr.Zero) - { - // tooltip window does not exist; failed; abort - Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); - return; - } - - PInvoke.RECT trayButtonClientRect; - var getClientRectSuccess = PInvoke.User32.GetClientRect(this.Handle, out trayButtonClientRect); - if (getClientRectSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip"); - return; - } - - var toolinfo = new WindowsApi.TOOLINFO(); - toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); - toolinfo.hwnd = this.Handle; - toolinfo.uFlags = LegacyWindowsApi.TTF_SUBCLASS; - toolinfo.lpszText = _tooltipText; - toolinfo.uId = unchecked((nuint)(nint)this.Handle); // unique identifier (for adding/deleting the tooltip) - toolinfo.rect = trayButtonClientRect; - // - var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); - try - { - Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); - if (toolinfo.lpszText is not null) + if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0)) { - if (_tooltipInfoAdded == false) - { - _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)WindowsApi.TTM_ADDTOOL, 0, pointerToToolinfo); - _tooltipInfoAdded = true; - } - else - { - // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) - _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)WindowsApi.TTM_DELTOOL, 0, pointerToToolinfo); - _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)WindowsApi.TTM_ADDTOOL, 0, pointerToToolinfo); - } + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); } - else + // + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); { - // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed - _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)WindowsApi.TTM_DELTOOL, 0, pointerToToolinfo); - _tooltipInfoAdded = false; + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, e.X, e.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); } - } - finally - { - Marshal.FreeHGlobal(pointerToToolinfo); - } - } - - // NOTE: intial creation events are captured by this callback, but afterwards window messages are captured by WndProc instead - private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - switch ((LegacyWindowsApi.WindowMessage)msg) - { - case LegacyWindowsApi.WindowMessage.WM_CREATE: - if (LegacyWindowsApi.BufferedPaintInit() != LegacyWindowsApi.S_OK) - { - // failed; abort - Debug.Assert(false, "Could not initialize buffered paint"); - return new IntPtr(-1); // abort window creation process - } - break; - default: - break; - } - - // pass all non-handled messages through to DefWindowProc - return LegacyWindowsApi.DefWindowProc(hWnd, msg, wParam, lParam); - } - - // NOTE: the built-in CreateHandle function couldn't handle our custom class, so we have overridden CreateHandle and are calling CreateWindowEx ourselves - public override void CreateHandle(System.Windows.Forms.CreateParams cp) - { - // NOTE: if cp.ClassName is a string parseable as a (UInt16) number, convert that value to an IntPtr; otherwise capture a pointer to the string - IntPtr classNameAsIntPtr; - bool mustReleaseClassNameAsIntPtr = false; - // - ushort classNameAsUInt16 = 0; - if (ushort.TryParse(cp.ClassName, out classNameAsUInt16) == true) - { - classNameAsIntPtr = (IntPtr)classNameAsUInt16; - mustReleaseClassNameAsIntPtr = false; - } - else - { - classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); - mustReleaseClassNameAsIntPtr = true; - } - - // TODO: in some circumstances, it is possible that we are unable to create our window; consider creating a retry mechanism (dealing with async) or notify our caller - try - { - var handle = WindowsApi.CreateWindowEx( - (PInvoke.User32.WindowStylesEx)cp.ExStyle, - classNameAsIntPtr, - cp.Caption, - (PInvoke.User32.WindowStyles)cp.Style, - cp.X, - cp.Y, - cp.Width, - cp.Height, - cp.Parent, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero - ); - - // NOTE: in our testing, handle was sometimes IntPtr.Zero here (in which case the tray icon button's window will not exist) - if (handle == IntPtr.Zero) + break; + } + } + + internal void SetText(string? text) + { + _tooltipText = text; + this.UpdateTooltipTextAndTracking(); + } + + private void CreateTooltipWindow() + { + if (_tooltipWindowHandle != IntPtr.Zero) + { + // tooltip window already exists + return; + } + + _tooltipWindowHandle = LegacyWindowsApi.CreateWindowEx( + 0 /* no styles */, + PInvokeExtensions.TOOLTIPS_CLASS, + null, + LegacyWindowsApi.WindowStyles.WS_POPUP | (LegacyWindowsApi.WindowStyles)PInvokeExtensions.TTS_ALWAYSTIP, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + this.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (_tooltipWindowHandle == IntPtr.Zero) + { + Debug.Assert(false, "Could not create tooltip window"); + } + + this.UpdateTooltipTextAndTracking(); + } + + private void DestroyTooltipWindow() + { + // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. + _tooltipText = null; + this.UpdateTooltipTextAndTracking(); + + LegacyWindowsApi.DestroyWindow(_tooltipWindowHandle); + _tooltipWindowHandle = IntPtr.Zero; + } + + private void UpdateTooltipTextAndTracking() + { + if (_tooltipWindowHandle == IntPtr.Zero) + { + // tooltip window does not exist; failed; abort + Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); + return; + } + + PInvoke.RECT trayButtonClientRect; + var getClientRectSuccess = PInvoke.User32.GetClientRect(this.Handle, out trayButtonClientRect); + if (getClientRectSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip"); + return; + } + + var toolinfo = new PInvokeExtensions.TOOLINFO(); + toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); + toolinfo.hwnd = this.Handle; + toolinfo.uFlags = PInvokeExtensions.TTF_SUBCLASS; + toolinfo.lpszText = _tooltipText; + toolinfo.uId = unchecked((nuint)(nint)this.Handle); // unique identifier (for adding/deleting the tooltip) + toolinfo.rect = trayButtonClientRect; + // + var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); + try + { + Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); + if (toolinfo.lpszText is not null) + { + if (_tooltipInfoAdded == false) { - Debug.Assert(false, "Could not create tray button window handle"); + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_ADDTOOL, 0, pointerToToolinfo); + _tooltipInfoAdded = true; } - - this.AssignHandle(handle); - } - finally - { - if (mustReleaseClassNameAsIntPtr == true) + else { - Marshal.FreeHGlobal(classNameAsIntPtr); + // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_DELTOOL, 0, pointerToToolinfo); + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_ADDTOOL, 0, pointerToToolinfo); } - } - } - - public void Dispose() - { - // TODO: if we are the topmost/leftmost next-to-tray-icon button, we should expand the task button container so it takes up our now-unoccupied space - - if (_mouseHook is not null) - { - _mouseHook.Dispose(); - } - - Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; - - this.DestroyTooltipWindow(); - this.DestroyHandle(); - } - - protected override void WndProc(ref System.Windows.Forms.Message m) - { - var uMsg = (uint)m.Msg; - - IntPtr? result = null; - - switch ((LegacyWindowsApi.WindowMessage)uMsg) - { - case LegacyWindowsApi.WindowMessage.WM_DESTROY: - /* TODO: trace to see if WM_DESTROY is actually called here; if not, then we should place the uninit in dispose instead; we might also consider - * not using BufferedPaintInit/UnInit at all (although that _might_ slow down our buffered painting execution a tiny bit) */ - LegacyWindowsApi.BufferedPaintUnInit(); - break; - case LegacyWindowsApi.WindowMessage.WM_DISPLAYCHANGE: - // screen resolution has changed: reposition the tray button - // NOTE: m.wParam contains bit depth - // NOTE: m.lParam contains the resolutions of the screen (horizontal resolution in low-order word; vertical resolution in high-order word) - this.PositionTrayButton(); - break; - case LegacyWindowsApi.WindowMessage.WM_ERASEBKGND: - // we will handle erasing the background, so return a non-zero value here - result = new IntPtr(1); - break; - case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - { - var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); - if (hitPoint is null) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - break; - } - var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_MOUSEACTIVATE: - // do not activate our window (and discard this message) - result = new IntPtr(LegacyWindowsApi.MA_NOACTIVATEANDEAT); - break; - case LegacyWindowsApi.WindowMessage.WM_MOUSELEAVE: - // the cursor has left our tray button's window area; remove the hover state from our visual state - _visualState &= ~TrayButtonVisualStateFlags.Hover; - // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here - // (and then we check them again when the mouse moves back over the button) - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: - // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) - // - // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because - // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during - // mousemove so that we can re-enable the pressed state if/where appropriate. - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & LegacyWindowsApi.MK_LBUTTON) != 0)) - { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - } - if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & LegacyWindowsApi.MK_RBUTTON) != 0)) - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - } - // - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_NCHITTEST: - var hitTestX = (short)((m.LParam.ToInt64() >> 0) & 0xFFFF); - var hitTestY = (short)((m.LParam.ToInt64() >> 16) & 0xFFFF); - // - LegacyWindowsApi.RECT trayButtonRectInScreenCoordinates; - if (LegacyWindowsApi.GetWindowRect(this.Handle, out trayButtonRectInScreenCoordinates) == false) - { - // fail; abort - Debug.Assert(false, "Could not get rect of tray button in screen coordinates"); - return; - } - // - if ((hitTestX >= trayButtonRectInScreenCoordinates.Left) && (hitTestX < trayButtonRectInScreenCoordinates.Right) && - (hitTestY >= trayButtonRectInScreenCoordinates.Top) && (hitTestY < trayButtonRectInScreenCoordinates.Bottom)) - { - // inside client area - result = new IntPtr(1); // HTCLIENT - } - else - { - // nowhere - // TODO: determine if there is another response we should be returning instead; the documentation is not clear in this regard - result = new IntPtr(0); // HTNOWHERE - } - break; - case LegacyWindowsApi.WindowMessage.WM_NCPAINT: - // no non-client (frame) area to paint - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_PAINT: - this.Paint(m.HWnd); - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - { - var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); - if (hitPoint is null) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - break; - } - var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_SETCURSOR: - // wParam: window handle - // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event - var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); - var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); - switch ((LegacyWindowsApi.WindowMessage)mouseMsg) - { - case LegacyWindowsApi.WindowMessage.WM_LBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - result = new IntPtr(1); - break; - case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: - result = new IntPtr(1); - break; - case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: - // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter") then do so now - if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) - { - // track mousehover (for tooltips) and mouseleave (to remove hover effect) - var eventTrack = new LegacyWindowsApi.TRACKMOUSEEVENT(LegacyWindowsApi.TMEFlags.TME_LEAVE, this.Handle, LegacyWindowsApi.HOVER_DEFAULT); - var trackMouseEventSuccess = LegacyWindowsApi.TrackMouseEvent(ref eventTrack); - if (trackMouseEventSuccess == false) - { - // failed - Debug.Assert(false, "Could not set up tracking of tray button window area"); - return; - } - - _visualState |= TrayButtonVisualStateFlags.Hover; - - this.RequestRedraw(); - } - result = new IntPtr(1); - break; - case LegacyWindowsApi.WindowMessage.WM_RBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - result = new IntPtr(1); - break; - case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: - result = new IntPtr(1); - break; - default: - //Debug.WriteLine("UNHANDLED SETCURSOR Mouse Message: " + mouseMsg.ToString()); - break; - } - break; - case LegacyWindowsApi.WindowMessage.WM_SIZE: - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_WINDOWPOSCHANGED: - result = new IntPtr(0); - break; - case LegacyWindowsApi.WindowMessage.WM_WINDOWPOSCHANGING: - // in this implementation, we don't do anything with this message; nothing to do here - result = new IntPtr(0); - break; - default: - // unhandled message; this will be passed onto DefWindowProc instead - break; - } - - if (result.HasValue == true) - { - m.Result = result.Value; - } - else - { - m.Result = LegacyWindowsApi.DefWindowProc(m.HWnd, (uint)m.Msg, m.WParam, m.LParam); - } - } - - private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) - { - // start a timer which will verify that the button is positioned properly (and will give up after a certain number of attempts) - var checkupInterval = new TimeSpan(0, 0, 0, 0, 250); - _trayButtonPositionCheckupTimerCounter = 40; // count down for 10 seconds (0.250 x 40) - _trayButtonPositionCheckupTimer = new System.Threading.Timer(TrayButtonPositionCheckup, null, checkupInterval, checkupInterval); - } - private void TrayButtonPositionCheckup(object? state) - { - if (_trayButtonPositionCheckupTimerCounter <= 0) - { - _trayButtonPositionCheckupTimer?.Dispose(); - _trayButtonPositionCheckupTimer = null; - return; - } - // - _trayButtonPositionCheckupTimerCounter = Math.Max(_trayButtonPositionCheckupTimerCounter - 1, 0); - - // check the current and desired positions of the notify tray icon - var calculateResult = this.CalculateCurrentAndTargetRectOfTrayButton(); - if (calculateResult is not null) - { - if (calculateResult.Value.changeToRect is not null) + } + else + { + // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_DELTOOL, 0, pointerToToolinfo); + _tooltipInfoAdded = false; + } + } + finally + { + Marshal.FreeHGlobal(pointerToToolinfo); + } + } + + // NOTE: intial creation events are captured by this callback, but afterwards window messages are captured by WndProc instead + private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch ((LegacyWindowsApi.WindowMessage)msg) + { + case LegacyWindowsApi.WindowMessage.WM_CREATE: + if (LegacyWindowsApi.BufferedPaintInit() != LegacyWindowsApi.S_OK) { - this.PositionTrayButton(); + // failed; abort + Debug.Assert(false, "Could not initialize buffered paint"); + return new IntPtr(-1); // abort window creation process } - } - } - - private LegacyWindowsApi.POINT? ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) - { - var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); - var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); - // convert x and y to screen coordinates - var hitPoint = new PInvoke.POINT { x = x, y = y }; - - // NOTE: the instructions for MapWindowPoints instruct us to call SetLastError before calling MapWindowPoints to ensure that we can distinguish a result of 0 from an error if the last win32 error wasn't set (because it wasn't an error) - Marshal.SetLastPInvokeError(0); - // - // NOTE: the PInvoke implementation of MapWindowPoints did not support passing in a POINT struct, so we manually declared the function - var mapWindowPointsResult = WindowsApi.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); - if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != 0) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - return null; - } - - var result = new LegacyWindowsApi.POINT(hitPoint.x, hitPoint.y); - return result; - } - private void Paint(IntPtr hWnd) - { - LegacyWindowsApi.PAINTSTRUCT ps = new LegacyWindowsApi.PAINTSTRUCT(); - IntPtr paintDc = LegacyWindowsApi.BeginPaint(hWnd, out ps); - try - { - IntPtr bufferedPaintDc; - // NOTE: ps.rcPaint was an empty rect in our intiail tests, so we are using a manually-created clientRect (from GetClientRect) here instead - var paintBufferHandle = LegacyWindowsApi.BeginBufferedPaint(ps.hdc, ref ps.rcPaint, LegacyWindowsApi.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, IntPtr.Zero, out bufferedPaintDc); - try + break; + default: + break; + } + + // pass all non-handled messages through to DefWindowProc + return LegacyWindowsApi.DefWindowProc(hWnd, msg, wParam, lParam); + } + + // NOTE: the built-in CreateHandle function couldn't handle our custom class, so we have overridden CreateHandle and are calling CreateWindowEx ourselves + public override void CreateHandle(System.Windows.Forms.CreateParams cp) + { + // NOTE: if cp.ClassName is a string parseable as a (UInt16) number, convert that value to an IntPtr; otherwise capture a pointer to the string + IntPtr classNameAsIntPtr; + bool mustReleaseClassNameAsIntPtr = false; + // + ushort classNameAsUInt16 = 0; + if (ushort.TryParse(cp.ClassName, out classNameAsUInt16) == true) + { + classNameAsIntPtr = (IntPtr)classNameAsUInt16; + mustReleaseClassNameAsIntPtr = false; + } + else + { + classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); + mustReleaseClassNameAsIntPtr = true; + } + + // TODO: in some circumstances, it is possible that we are unable to create our window; consider creating a retry mechanism (dealing with async) or notify our caller + try + { + var handle = PInvokeExtensions.CreateWindowEx( + (PInvoke.User32.WindowStylesEx)cp.ExStyle, + classNameAsIntPtr, + cp.Caption, + (PInvoke.User32.WindowStyles)cp.Style, + cp.X, + cp.Y, + cp.Width, + cp.Height, + cp.Parent, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + + // NOTE: in our testing, handle was sometimes IntPtr.Zero here (in which case the tray icon button's window will not exist) + if (handle == IntPtr.Zero) + { + Debug.Assert(false, "Could not create tray button window handle"); + } + + this.AssignHandle(handle); + } + finally + { + if (mustReleaseClassNameAsIntPtr == true) + { + Marshal.FreeHGlobal(classNameAsIntPtr); + } + } + } + + public void Dispose() + { + // TODO: if we are the topmost/leftmost next-to-tray-icon button, we should expand the task button container so it takes up our now-unoccupied space + + if (_mouseHook is not null) + { + _mouseHook.Dispose(); + } + + Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; + + this.DestroyTooltipWindow(); + this.DestroyHandle(); + } + + protected override void WndProc(ref System.Windows.Forms.Message m) + { + var uMsg = (uint)m.Msg; + + IntPtr? result = null; + + switch ((LegacyWindowsApi.WindowMessage)uMsg) + { + case LegacyWindowsApi.WindowMessage.WM_DESTROY: + /* TODO: trace to see if WM_DESTROY is actually called here; if not, then we should place the uninit in dispose instead; we might also consider + * not using BufferedPaintInit/UnInit at all (although that _might_ slow down our buffered painting execution a tiny bit) */ + LegacyWindowsApi.BufferedPaintUnInit(); + break; + case LegacyWindowsApi.WindowMessage.WM_DISPLAYCHANGE: + // screen resolution has changed: reposition the tray button + // NOTE: m.wParam contains bit depth + // NOTE: m.lParam contains the resolutions of the screen (horizontal resolution in low-order word; vertical resolution in high-order word) + this.PositionTrayButton(); + break; + case LegacyWindowsApi.WindowMessage.WM_ERASEBKGND: + // we will handle erasing the background, so return a non-zero value here + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); { - if (ps.rcPaint == LegacyWindowsApi.RECT.Empty) - { - // no rectangle; nothing to do - return; - } - - // clear our buffer background (to ARGB(0,0,0,0)) - var bufferedPaintClearSuccess = LegacyWindowsApi.BufferedPaintClear(paintBufferHandle, ref ps.rcPaint); - if (bufferedPaintClearSuccess != LegacyWindowsApi.S_OK) - { - // failed; abort - Debug.Assert(false, "Could not clear tray button's background"); - return; - } - - // if the user has pressed (mousedown) on our tray button or is hovering over it, highlight the tray button now - Double highlightOpacity = 0.0; - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || - ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) - { - highlightOpacity = 0.25; - } - else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) - { - highlightOpacity = 0.1; - } - // - if (highlightOpacity > 0.0) - { - this.DrawHighlightBackground(bufferedPaintDc, ps.rcPaint, System.Drawing.Color.White, highlightOpacity); - } - - // calculate the size and position of our icon - int iconWidthAndHeight = this.CalculateWidthAndHeightForIcon(ps.rcPaint); - // - var xLeft = ((ps.rcPaint.Right - ps.rcPaint.Left) - iconWidthAndHeight) / 2; - var yTop = ((ps.rcPaint.Bottom - ps.rcPaint.Top) - iconWidthAndHeight) / 2; - - if (_iconHandle != IntPtr.Zero && iconWidthAndHeight > 0) - { - var drawIconSuccess = LegacyWindowsApi.DrawIconEx(bufferedPaintDc, xLeft, yTop, _iconHandle, iconWidthAndHeight, iconWidthAndHeight, 0 /* not animated */, IntPtr.Zero /* no triple-buffering */, LegacyWindowsApi.DrawIconFlags.DI_NORMAL); - if (drawIconSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not draw tray button's icon"); - return; - } - } + var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (hitPoint is null) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + break; + } + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); } - finally + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEACTIVATE: + // do not activate our window (and discard this message) + result = new IntPtr(LegacyWindowsApi.MA_NOACTIVATEANDEAT); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSELEAVE: + // the cursor has left our tray button's window area; remove the hover state from our visual state + _visualState &= ~TrayButtonVisualStateFlags.Hover; + // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here + // (and then we check them again when the mouse moves back over the button) + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: + // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) + // + // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because + // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during + // mousemove so that we can re-enable the pressed state if/where appropriate. + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & LegacyWindowsApi.MK_LBUTTON) != 0)) { - LegacyWindowsApi.EndBufferedPaint(paintBufferHandle, true); + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); } - } - finally - { - LegacyWindowsApi.EndPaint(hWnd, ref ps); - } - } - - private int CalculateWidthAndHeightForIcon(LegacyWindowsApi.RECT rect) - { - int result; - // NOTE: we currently measure the size of our icon by measuring the size of the rectangle - // NOTE: we use the larger of the two dimensions (height vs width) to determine our icon size; we may reconsider this in the future if we support non-square icons - int largerDimensionLenth; - if (rect.Bottom - rect.Top > rect.Right - rect.Left) - { - largerDimensionLenth = rect.Bottom - rect.Top; - } - else - { - largerDimensionLenth = rect.Right - rect.Left; - } - // - if (largerDimensionLenth >= 48) - { - result = 32; - } - else if (largerDimensionLenth >= 36) - { - result = 24; - } - else if (largerDimensionLenth >= 30) - { - result = 20; - } - else if (largerDimensionLenth >= 24) - { - result = 16; - } - else - { - result = 0; - } - - return result; - } - - private void DrawHighlightBackground(IntPtr hdc, LegacyWindowsApi.RECT rect, System.Drawing.Color color, Double opacity) - { - // GDI doesn't have a concept of semi-transparent pixels - the only function that honours them is AlphaBlend. - // Create a bitmap containing a single pixel - and then use AlphaBlend to stretch it to the size of the rect. - - // set up the 1x1 pixel bitmap's configuration - var pixelBitmapInfo = new LegacyWindowsApi.BITMAPINFO(); - pixelBitmapInfo.bmiHeader = new LegacyWindowsApi.BITMAPINFOHEADER() - { - biWidth = 1, - biHeight = 1, - biPlanes = 1, // must be 1 - biBitCount = 32, // maximum of 2^32 colors - biCompression = LegacyWindowsApi.BitmapCompressionType.BI_RGB, - biSizeImage = 0, - biClrUsed = 0, - biClrImportant = 0 - }; - pixelBitmapInfo.bmiHeader.biSize = (uint)Marshal.SizeOf(pixelBitmapInfo.bmiHeader); - pixelBitmapInfo.bmiColors = new LegacyWindowsApi.RGBQUAD[1]; - - // calculate the pixel color as a uint32 (in AARRGGBB order) - uint pixelColor = ( - (((uint)color.A) << 24) | // NOTE: we ignore the alpha value in our call to AlphaBlend - (((uint)color.R) << 16) | - (((uint)color.G) << 8) | - (((uint)color.B) << 0)); - - // create the memory device context for the pixel - var pixelDc = LegacyWindowsApi.CreateCompatibleDC(hdc); - if (pixelDc == IntPtr.Zero) - { - // failed; abort - Debug.Assert(false, "Could not create device context for highlight pixel."); - return; - } - try - { - IntPtr pixelDibBitValues; - var pixelDibHandle = LegacyWindowsApi.CreateDIBSection(pixelDc, ref pixelBitmapInfo, LegacyWindowsApi.DIB_RGB_COLORS, out pixelDibBitValues, IntPtr.Zero, 0); - if (pixelDibHandle == IntPtr.Zero) + if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & LegacyWindowsApi.MK_RBUTTON) != 0)) { - // failed; abort - Debug.Assert(false, "Could not create DIB for highlight pixel."); - return; + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); } // - try + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_NCHITTEST: + var hitTestX = (short)((m.LParam.ToInt64() >> 0) & 0xFFFF); + var hitTestY = (short)((m.LParam.ToInt64() >> 16) & 0xFFFF); + // + LegacyWindowsApi.RECT trayButtonRectInScreenCoordinates; + if (LegacyWindowsApi.GetWindowRect(this.Handle, out trayButtonRectInScreenCoordinates) == false) { - var selectedBitmapHandle = LegacyWindowsApi.SelectObject(pixelDc, pixelDibHandle); - if (selectedBitmapHandle == IntPtr.Zero) - { - // failed; abort - Debug.Assert(false, "Could not select object into the pixel device context."); - return; - } - try - { - // write over the single pixel's value (with the passed-in pixel) - Marshal.WriteIntPtr(pixelDibBitValues, new IntPtr(pixelColor)); - - // draw the highlight (stretching the pixel to the full rectangle size) - LegacyWindowsApi.BLENDFUNCTION blendFunction = new LegacyWindowsApi.BLENDFUNCTION() - { - BlendOp = (byte)LegacyWindowsApi.AC_SRC_OVER, - BlendFlags = 0, // must be zero - SourceConstantAlpha = (byte)(opacity * 255), // the requested opacity level - AlphaFormat = 0 - }; - var RESULT_TO_USE = LegacyWindowsApi.AlphaBlend(hdc, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, pixelDc, 0, 0, 1, 1, blendFunction); - } - finally - { - _ = LegacyWindowsApi.SelectObject(pixelDc, selectedBitmapHandle); - } + // fail; abort + Debug.Assert(false, "Could not get rect of tray button in screen coordinates"); + return; } - finally + // + if ((hitTestX >= trayButtonRectInScreenCoordinates.Left) && (hitTestX < trayButtonRectInScreenCoordinates.Right) && + (hitTestY >= trayButtonRectInScreenCoordinates.Top) && (hitTestY < trayButtonRectInScreenCoordinates.Bottom)) { - _ = LegacyWindowsApi.DeleteObject(pixelDibHandle); + // inside client area + result = new IntPtr(1); // HTCLIENT } - } - finally - { - _ = LegacyWindowsApi.DeleteDC(pixelDc); - } - } - - public void SetIcon(System.Drawing.Icon? icon) - { - if (icon is not null) - { - _iconHandle = icon.Handle; - } - else - { - _iconHandle = IntPtr.Zero; - } - - // TODO: if we support non-square icons, then reposition the tray button based on the new dimensions of the icon (in case it's wider/narrower) - //this.PositionTrayButton(); - - // trigger a redraw - this.RequestRedraw(); - } - - private void PositionTrayButton() - { - var trayButtonRects = CalculateCurrentAndTargetRectOfTrayButton(); - if (trayButtonRects is null) - { - // fail; abort - Debug.Assert(false, "Could not calculate current and/or new rects for tray button"); - return; - } - // - var currentRect = trayButtonRects.Value.currentRect; - var changeToRect = trayButtonRects.Value.changeToRect; - var taskbarOrientation = trayButtonRects.Value.orientation; - - if (_mouseHook is not null) - { - // update our tracking region to track the new position (unless we haven't moved, in which case continue to track our current position) - if (changeToRect is not null) + else { - _mouseHook.UpdateTrackingRegion(changeToRect.Value.ToPInvokeRect()); + // nowhere + // TODO: determine if there is another response we should be returning instead; the documentation is not clear in this regard + result = new IntPtr(0); // HTNOWHERE } - else if (currentRect is not null) + break; + case LegacyWindowsApi.WindowMessage.WM_NCPAINT: + // no non-client (frame) area to paint + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_PAINT: + this.Paint(m.HWnd); + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); { - _mouseHook.UpdateTrackingRegion(currentRect.Value.ToPInvokeRect()); + var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (hitPoint is null) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + break; + } + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); } - else + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_SETCURSOR: + // wParam: window handle + // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event + var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); + var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); + switch ((LegacyWindowsApi.WindowMessage)mouseMsg) { - Debug.Assert(false, "Could not determine current RECT of tray button"); + case LegacyWindowsApi.WindowMessage.WM_LBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: + // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter") then do so now + if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) + { + // track mousehover (for tooltips) and mouseleave (to remove hover effect) + var eventTrack = new LegacyWindowsApi.TRACKMOUSEEVENT(LegacyWindowsApi.TMEFlags.TME_LEAVE, this.Handle, LegacyWindowsApi.HOVER_DEFAULT); + var trackMouseEventSuccess = LegacyWindowsApi.TrackMouseEvent(ref eventTrack); + if (trackMouseEventSuccess == false) + { + // failed + Debug.Assert(false, "Could not set up tracking of tray button window area"); + return; + } + + _visualState |= TrayButtonVisualStateFlags.Hover; + + this.RequestRedraw(); + } + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: + result = new IntPtr(1); + break; + default: + //Debug.WriteLine("UNHANDLED SETCURSOR Mouse Message: " + mouseMsg.ToString()); + break; } - } - - // if changeToRect is more leftmost/topmost than the task button container's right side, then shrink the task button container appropriately - LegacyWindowsApi.RECT? newTaskButtonContainerRect = null; - if (changeToRect is not null) - { - var taskbarTripletHandles = this.GetTaskbarTripletHandles(); - var taskbarTripletRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); - if (taskbarTripletRects is null) + break; + case LegacyWindowsApi.WindowMessage.WM_SIZE: + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_WINDOWPOSCHANGED: + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_WINDOWPOSCHANGING: + // in this implementation, we don't do anything with this message; nothing to do here + result = new IntPtr(0); + break; + default: + // unhandled message; this will be passed onto DefWindowProc instead + break; + } + + if (result.HasValue == true) + { + m.Result = result.Value; + } + else + { + m.Result = LegacyWindowsApi.DefWindowProc(m.HWnd, (uint)m.Msg, m.WParam, m.LParam); + } + } + + private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) + { + // start a timer which will verify that the button is positioned properly (and will give up after a certain number of attempts) + var checkupInterval = new TimeSpan(0, 0, 0, 0, 250); + _trayButtonPositionCheckupTimerCounter = 40; // count down for 10 seconds (0.250 x 40) + _trayButtonPositionCheckupTimer = new System.Threading.Timer(TrayButtonPositionCheckup, null, checkupInterval, checkupInterval); + } + private void TrayButtonPositionCheckup(object? state) + { + if (_trayButtonPositionCheckupTimerCounter <= 0) + { + _trayButtonPositionCheckupTimer?.Dispose(); + _trayButtonPositionCheckupTimer = null; + return; + } + // + _trayButtonPositionCheckupTimerCounter = Math.Max(_trayButtonPositionCheckupTimerCounter - 1, 0); + + // check the current and desired positions of the notify tray icon + var calculateResult = this.CalculateCurrentAndTargetRectOfTrayButton(); + if (calculateResult is not null) + { + if (calculateResult.Value.changeToRect is not null) + { + this.PositionTrayButton(); + } + } + } + + private LegacyWindowsApi.POINT? ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) + { + var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); + var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); + // convert x and y to screen coordinates + var hitPoint = new PInvoke.POINT { x = x, y = y }; + + // NOTE: the instructions for MapWindowPoints instruct us to call SetLastError before calling MapWindowPoints to ensure that we can distinguish a result of 0 from an error if the last win32 error wasn't set (because it wasn't an error) + Marshal.SetLastPInvokeError(0); + // + // NOTE: the PInvoke implementation of MapWindowPoints did not support passing in a POINT struct, so we manually declared the function + var mapWindowPointsResult = PInvokeExtensions.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); + if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != 0) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + return null; + } + + var result = new LegacyWindowsApi.POINT(hitPoint.x, hitPoint.y); + return result; + } + private void Paint(IntPtr hWnd) + { + LegacyWindowsApi.PAINTSTRUCT ps = new LegacyWindowsApi.PAINTSTRUCT(); + IntPtr paintDc = LegacyWindowsApi.BeginPaint(hWnd, out ps); + try + { + IntPtr bufferedPaintDc; + // NOTE: ps.rcPaint was an empty rect in our intiail tests, so we are using a manually-created clientRect (from GetClientRect) here instead + var paintBufferHandle = LegacyWindowsApi.BeginBufferedPaint(ps.hdc, ref ps.rcPaint, LegacyWindowsApi.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, IntPtr.Zero, out bufferedPaintDc); + try + { + if (ps.rcPaint == LegacyWindowsApi.RECT.Empty) { - // failed; abort - Debug.Assert(false, "could not get rects of taskbar or its important children"); - return; + // no rectangle; nothing to do + return; } - var taskButtonContainerRect = taskbarTripletRects.Value.TaskButtonContainerRect; - if ((taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) && (taskButtonContainerRect.Right > changeToRect.Value.Left)) + // clear our buffer background (to ARGB(0,0,0,0)) + var bufferedPaintClearSuccess = LegacyWindowsApi.BufferedPaintClear(paintBufferHandle, ref ps.rcPaint); + if (bufferedPaintClearSuccess != LegacyWindowsApi.S_OK) { - newTaskButtonContainerRect = new LegacyWindowsApi.RECT(new System.Windows.Rect( - taskButtonContainerRect.Left, - taskButtonContainerRect.Top, - Math.Max(taskButtonContainerRect.Right - taskButtonContainerRect.Left - (taskButtonContainerRect.Right - changeToRect.Value.Left), 0), - taskButtonContainerRect.Bottom - taskButtonContainerRect.Top - )); + // failed; abort + Debug.Assert(false, "Could not clear tray button's background"); + return; } - else if ((taskbarOrientation == System.Windows.Forms.Orientation.Vertical) && taskButtonContainerRect.Bottom > changeToRect.Value.Top) + + // if the user has pressed (mousedown) on our tray button or is hovering over it, highlight the tray button now + Double highlightOpacity = 0.0; + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || + ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) { - newTaskButtonContainerRect = new LegacyWindowsApi.RECT(new System.Windows.Rect( - taskButtonContainerRect.Left, - taskButtonContainerRect.Top, - taskButtonContainerRect.Right - taskButtonContainerRect.Left, - taskButtonContainerRect.Bottom - taskButtonContainerRect.Top - Math.Max(taskButtonContainerRect.Bottom - changeToRect.Value.Top, 0) - )); + highlightOpacity = 0.25; } - } - // - if (newTaskButtonContainerRect is not null) - { - var taskButtonContainerHandle = TrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); - - // shrink the task button container - // NOTE: this is a blocking call, waiting until the task button container is resized; we do this intentionally so that we see its updated size synchronously - var repositionTaskButtonContainerSuccess = LegacyWindowsApi.SetWindowPos( - taskButtonContainerHandle, - IntPtr.Zero, - newTaskButtonContainerRect.Value.Left, - newTaskButtonContainerRect.Value.Top, - newTaskButtonContainerRect.Value.Right - newTaskButtonContainerRect.Value.Left, - newTaskButtonContainerRect.Value.Bottom - newTaskButtonContainerRect.Value.Top, - LegacyWindowsApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | - LegacyWindowsApi.SetWindowPosFlags.SWP_NOMOVE /* retain the current x and y position, out of an abundance of caution */ | - LegacyWindowsApi.SetWindowPosFlags.SWP_NOZORDER /* retain the current Z order (ignoring the hWndInsertAfter parameter) */ - ); - - if (repositionTaskButtonContainerSuccess == false) + else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) { - // failed; abort - Debug.Assert(false, "Could not resize taskbar's task button container"); - return; + highlightOpacity = 0.1; } - } - - // if our button needs to move (either because we don't know the old RECT or because the new RECT is different), do so now - if (changeToRect is not null) - { - if (currentRect.HasValue == false || (currentRect.Value != changeToRect.Value)) + // + if (highlightOpacity > 0.0) { - var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); - - // convert our tray button's position from desktop coordinates to "child" coordinates within the taskbar - PInvoke.RECT childRect = new() { left = changeToRect.Value.Left, top = changeToRect.Value.Top, right = changeToRect.Value.Right, bottom = changeToRect.Value.Bottom }; - var mapWindowPointsResult = WindowsApi.MapWindowPoints(IntPtr.Zero /* use screen coordinates */, taskbarHandle, ref childRect, 2 /* 2 indicates that lpPoints is a RECT */); - if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != LegacyWindowsApi.ERROR_SUCCESS) - { - // failed; abort - Debug.Assert(false, "Could not map tray button RECT points to taskbar window handle"); - return; - } - - var repositionTrayButtonSuccess = LegacyWindowsApi.SetWindowPos( - this.Handle, - LegacyWindowsApi.HWND_TOP, - childRect.left, - childRect.top, - childRect.right - childRect.left, - childRect.bottom - childRect.top, - LegacyWindowsApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | - LegacyWindowsApi.SetWindowPosFlags.SWP_SHOWWINDOW /* display the tray button */ - ); - - if (repositionTrayButtonSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not reposition and/or resize tray button"); - return; - } + this.DrawHighlightBackground(bufferedPaintDc, ps.rcPaint, System.Drawing.Color.White, highlightOpacity); } - // as we have moved/resized, request a repaint - this.RequestRedraw(); + // calculate the size and position of our icon + int iconWidthAndHeight = this.CalculateWidthAndHeightForIcon(ps.rcPaint); + // + var xLeft = ((ps.rcPaint.Right - ps.rcPaint.Left) - iconWidthAndHeight) / 2; + var yTop = ((ps.rcPaint.Bottom - ps.rcPaint.Top) - iconWidthAndHeight) / 2; - // if we have tooltip text, update its tracking rectangle - if (_tooltipText is not null) + if (_iconHandle != IntPtr.Zero && iconWidthAndHeight > 0) { - UpdateTooltipTextAndTracking(); + var drawIconSuccess = LegacyWindowsApi.DrawIconEx(bufferedPaintDc, xLeft, yTop, _iconHandle, iconWidthAndHeight, iconWidthAndHeight, 0 /* not animated */, IntPtr.Zero /* no triple-buffering */, LegacyWindowsApi.DrawIconFlags.DI_NORMAL); + if (drawIconSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not draw tray button's icon"); + return; + } } - } - } - - private (IntPtr TaskbarHandle, IntPtr TaskButtonContainerHandle, IntPtr NotifyTrayHandle) GetTaskbarTripletHandles() - { - var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); - var taskButtonContainerHandle = TrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); - var notifyTrayHandle = TrayButtonNativeWindow.FindWindowsTaskbarNotificationTrayHandle(); - - return (taskbarHandle, taskButtonContainerHandle, notifyTrayHandle); - } - - private (LegacyWindowsApi.RECT TaskbarRect, LegacyWindowsApi.RECT TaskButtonContainerRect, LegacyWindowsApi.RECT NotifyTrayRect)? GetTaskbarTripletRects(IntPtr taskbarHandle, IntPtr taskButtonContainerHandle, IntPtr notifyTrayHandle) - { - // find the taskbar and its rect - LegacyWindowsApi.RECT taskbarRect = new LegacyWindowsApi.RECT(); - if (LegacyWindowsApi.GetWindowRect(taskbarHandle, out taskbarRect) == false) - { + } + finally + { + LegacyWindowsApi.EndBufferedPaint(paintBufferHandle, true); + } + } + finally + { + LegacyWindowsApi.EndPaint(hWnd, ref ps); + } + } + + private int CalculateWidthAndHeightForIcon(LegacyWindowsApi.RECT rect) + { + int result; + // NOTE: we currently measure the size of our icon by measuring the size of the rectangle + // NOTE: we use the larger of the two dimensions (height vs width) to determine our icon size; we may reconsider this in the future if we support non-square icons + int largerDimensionLenth; + if (rect.Bottom - rect.Top > rect.Right - rect.Left) + { + largerDimensionLenth = rect.Bottom - rect.Top; + } + else + { + largerDimensionLenth = rect.Right - rect.Left; + } + // + if (largerDimensionLenth >= 48) + { + result = 32; + } + else if (largerDimensionLenth >= 36) + { + result = 24; + } + else if (largerDimensionLenth >= 30) + { + result = 20; + } + else if (largerDimensionLenth >= 24) + { + result = 16; + } + else + { + result = 0; + } + + return result; + } + + private void DrawHighlightBackground(IntPtr hdc, LegacyWindowsApi.RECT rect, System.Drawing.Color color, Double opacity) + { + // GDI doesn't have a concept of semi-transparent pixels - the only function that honours them is AlphaBlend. + // Create a bitmap containing a single pixel - and then use AlphaBlend to stretch it to the size of the rect. + + // set up the 1x1 pixel bitmap's configuration + var pixelBitmapInfo = new LegacyWindowsApi.BITMAPINFO(); + pixelBitmapInfo.bmiHeader = new LegacyWindowsApi.BITMAPINFOHEADER() + { + biWidth = 1, + biHeight = 1, + biPlanes = 1, // must be 1 + biBitCount = 32, // maximum of 2^32 colors + biCompression = LegacyWindowsApi.BitmapCompressionType.BI_RGB, + biSizeImage = 0, + biClrUsed = 0, + biClrImportant = 0 + }; + pixelBitmapInfo.bmiHeader.biSize = (uint)Marshal.SizeOf(pixelBitmapInfo.bmiHeader); + pixelBitmapInfo.bmiColors = new LegacyWindowsApi.RGBQUAD[1]; + + // calculate the pixel color as a uint32 (in AARRGGBB order) + uint pixelColor = ( + (((uint)color.A) << 24) | // NOTE: we ignore the alpha value in our call to AlphaBlend + (((uint)color.R) << 16) | + (((uint)color.G) << 8) | + (((uint)color.B) << 0)); + + // create the memory device context for the pixel + var pixelDc = LegacyWindowsApi.CreateCompatibleDC(hdc); + if (pixelDc == IntPtr.Zero) + { + // failed; abort + Debug.Assert(false, "Could not create device context for highlight pixel."); + return; + } + try + { + IntPtr pixelDibBitValues; + var pixelDibHandle = LegacyWindowsApi.CreateDIBSection(pixelDc, ref pixelBitmapInfo, LegacyWindowsApi.DIB_RGB_COLORS, out pixelDibBitValues, IntPtr.Zero, 0); + if (pixelDibHandle == IntPtr.Zero) + { // failed; abort - Debug.Assert(false, "Could not obtain window handle to taskbar."); - return null; - } - - // find the window handles and rects of the task button container and the notify tray (which are children inside of the taskbar) - // - LegacyWindowsApi.RECT taskButtonContainerRect = new LegacyWindowsApi.RECT(); - if (LegacyWindowsApi.GetWindowRect(taskButtonContainerHandle, out taskButtonContainerRect) == false) - { - // failed; abort - Debug.Assert(false, "Could not obtain window handle to taskbar's task button list container."); - return null; - } - // - LegacyWindowsApi.RECT notifyTrayRect = new LegacyWindowsApi.RECT(); - if (LegacyWindowsApi.GetWindowRect(notifyTrayHandle, out notifyTrayRect) == false) - { - // failed; abort - Debug.Assert(false, "Could not obtain window handle to taskbar's notify tray."); - return null; - } - - return (taskbarRect, taskButtonContainerRect, notifyTrayRect); - } - - private (LegacyWindowsApi.RECT availableAreaRect, List childRects) CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(IntPtr taskbarHandle, System.Windows.Forms.Orientation taskbarOrientation, LegacyWindowsApi.RECT taskbarRect, LegacyWindowsApi.RECT taskButtonContainerRect, LegacyWindowsApi.RECT notifyTrayRect) - { - // calculate the total "free area" rectangle (the area between the task button container and the notify tray where we want to place our tray button) - LegacyWindowsApi.RECT freeAreaAvailableRect; - if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) - { - freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(taskButtonContainerRect.Right, taskbarRect.Top, Math.Max(notifyTrayRect.Left - taskButtonContainerRect.Right, 0), Math.Max(taskbarRect.Bottom - taskbarRect.Top, 0))); - } - else - { - freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(taskbarRect.Left, taskButtonContainerRect.Bottom, Math.Max(taskbarRect.Right - taskbarRect.Left, 0), Math.Max(notifyTrayRect.Top - taskButtonContainerRect.Bottom, 0))); - } - - // capture a list of all child windows within the taskbar; we'll use this list to enumerate the rects of all the taskbar's children - var taskbarChildHandles = TrayButtonNativeWindow.EnumerateChildWindows(taskbarHandle); - // - // find the rects of all windows within the taskbar; we need this information so that we do not overlap any other accessory windows which are trying to sit in the same area as us - var taskbarChildHandlesWithRects = new Dictionary(); - foreach (var taskbarChildHandle in taskbarChildHandles) - { - LegacyWindowsApi.RECT taskbarChildRect = new LegacyWindowsApi.RECT(); - if (LegacyWindowsApi.GetWindowRect(taskbarChildHandle, out taskbarChildRect) == true) + Debug.Assert(false, "Could not create DIB for highlight pixel."); + return; + } + // + try + { + var selectedBitmapHandle = LegacyWindowsApi.SelectObject(pixelDc, pixelDibHandle); + if (selectedBitmapHandle == IntPtr.Zero) { - taskbarChildHandlesWithRects.Add(taskbarChildHandle, taskbarChildRect); + // failed; abort + Debug.Assert(false, "Could not select object into the pixel device context."); + return; } - else + try { - Debug.Assert(false, "Could not capture RECTs of all taskbar child windows"); + // write over the single pixel's value (with the passed-in pixel) + Marshal.WriteIntPtr(pixelDibBitValues, new IntPtr(pixelColor)); + + // draw the highlight (stretching the pixel to the full rectangle size) + LegacyWindowsApi.BLENDFUNCTION blendFunction = new LegacyWindowsApi.BLENDFUNCTION() + { + BlendOp = (byte)LegacyWindowsApi.AC_SRC_OVER, + BlendFlags = 0, // must be zero + SourceConstantAlpha = (byte)(opacity * 255), // the requested opacity level + AlphaFormat = 0 + }; + var RESULT_TO_USE = LegacyWindowsApi.AlphaBlend(hdc, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, pixelDc, 0, 0, 1, 1, blendFunction); } - } - - // remove any child rects which are contained inside the task button container (so that we eliminate any subchildren from our calculations) - foreach (var taskbarChildHandle in taskbarChildHandles) - { - if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) + finally + { + _ = LegacyWindowsApi.SelectObject(pixelDc, selectedBitmapHandle); + } + } + finally + { + _ = LegacyWindowsApi.DeleteObject(pixelDibHandle); + } + } + finally + { + _ = LegacyWindowsApi.DeleteDC(pixelDc); + } + } + + public void SetIcon(System.Drawing.Icon? icon) + { + if (icon is not null) + { + _iconHandle = icon.Handle; + } + else + { + _iconHandle = IntPtr.Zero; + } + + // TODO: if we support non-square icons, then reposition the tray button based on the new dimensions of the icon (in case it's wider/narrower) + //this.PositionTrayButton(); + + // trigger a redraw + this.RequestRedraw(); + } + + private void PositionTrayButton() + { + var trayButtonRects = CalculateCurrentAndTargetRectOfTrayButton(); + if (trayButtonRects is null) + { + // fail; abort + Debug.Assert(false, "Could not calculate current and/or new rects for tray button"); + return; + } + // + var currentRect = trayButtonRects.Value.currentRect; + var changeToRect = trayButtonRects.Value.changeToRect; + var taskbarOrientation = trayButtonRects.Value.orientation; + + if (_mouseHook is not null) + { + // update our tracking region to track the new position (unless we haven't moved, in which case continue to track our current position) + if (changeToRect is not null) + { + _mouseHook.UpdateTrackingRegion(changeToRect.Value.ToPInvokeRect()); + } + else if (currentRect is not null) + { + _mouseHook.UpdateTrackingRegion(currentRect.Value.ToPInvokeRect()); + } + else + { + Debug.Assert(false, "Could not determine current RECT of tray button"); + } + } + + // if changeToRect is more leftmost/topmost than the task button container's right side, then shrink the task button container appropriately + LegacyWindowsApi.RECT? newTaskButtonContainerRect = null; + if (changeToRect is not null) + { + var taskbarTripletHandles = this.GetTaskbarTripletHandles(); + var taskbarTripletRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); + if (taskbarTripletRects is null) + { + // failed; abort + Debug.Assert(false, "could not get rects of taskbar or its important children"); + return; + } + var taskButtonContainerRect = taskbarTripletRects.Value.TaskButtonContainerRect; + + if ((taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) && (taskButtonContainerRect.Right > changeToRect.Value.Left)) + { + newTaskButtonContainerRect = new LegacyWindowsApi.RECT(new System.Windows.Rect( + taskButtonContainerRect.Left, + taskButtonContainerRect.Top, + Math.Max(taskButtonContainerRect.Right - taskButtonContainerRect.Left - (taskButtonContainerRect.Right - changeToRect.Value.Left), 0), + taskButtonContainerRect.Bottom - taskButtonContainerRect.Top + )); + } + else if ((taskbarOrientation == System.Windows.Forms.Orientation.Vertical) && taskButtonContainerRect.Bottom > changeToRect.Value.Top) + { + newTaskButtonContainerRect = new LegacyWindowsApi.RECT(new System.Windows.Rect( + taskButtonContainerRect.Left, + taskButtonContainerRect.Top, + taskButtonContainerRect.Right - taskButtonContainerRect.Left, + taskButtonContainerRect.Bottom - taskButtonContainerRect.Top - Math.Max(taskButtonContainerRect.Bottom - changeToRect.Value.Top, 0) + )); + } + } + // + if (newTaskButtonContainerRect is not null) + { + var taskButtonContainerHandle = TrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); + + // shrink the task button container + // NOTE: this is a blocking call, waiting until the task button container is resized; we do this intentionally so that we see its updated size synchronously + var repositionTaskButtonContainerSuccess = LegacyWindowsApi.SetWindowPos( + taskButtonContainerHandle, + IntPtr.Zero, + newTaskButtonContainerRect.Value.Left, + newTaskButtonContainerRect.Value.Top, + newTaskButtonContainerRect.Value.Right - newTaskButtonContainerRect.Value.Left, + newTaskButtonContainerRect.Value.Bottom - newTaskButtonContainerRect.Value.Top, + LegacyWindowsApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | + LegacyWindowsApi.SetWindowPosFlags.SWP_NOMOVE /* retain the current x and y position, out of an abundance of caution */ | + LegacyWindowsApi.SetWindowPosFlags.SWP_NOZORDER /* retain the current Z order (ignoring the hWndInsertAfter parameter) */ + ); + + if (repositionTaskButtonContainerSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not resize taskbar's task button container"); + return; + } + } + + // if our button needs to move (either because we don't know the old RECT or because the new RECT is different), do so now + if (changeToRect is not null) + { + if (currentRect.HasValue == false || (currentRect.Value != changeToRect.Value)) + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + + // convert our tray button's position from desktop coordinates to "child" coordinates within the taskbar + PInvoke.RECT childRect = new() { left = changeToRect.Value.Left, top = changeToRect.Value.Top, right = changeToRect.Value.Right, bottom = changeToRect.Value.Bottom }; + var mapWindowPointsResult = PInvokeExtensions.MapWindowPoints(IntPtr.Zero /* use screen coordinates */, taskbarHandle, ref childRect, 2 /* 2 indicates that lpPoints is a RECT */); + if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != LegacyWindowsApi.ERROR_SUCCESS) { - var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; - if (taskbarChildRect.IsInside(taskButtonContainerRect)) - { - taskbarChildHandlesWithRects.Remove(taskbarChildHandle); - } + // failed; abort + Debug.Assert(false, "Could not map tray button RECT points to taskbar window handle"); + return; } - } - // remove our own (tray button) window handle from the list (so that we don't see our current screen rect as "taken" in the list of occupied RECTs) - taskbarChildHandlesWithRects.Remove(this.Handle); + var repositionTrayButtonSuccess = LegacyWindowsApi.SetWindowPos( + this.Handle, + LegacyWindowsApi.HWND_TOP, + childRect.left, + childRect.top, + childRect.right - childRect.left, + childRect.bottom - childRect.top, + LegacyWindowsApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | + LegacyWindowsApi.SetWindowPosFlags.SWP_SHOWWINDOW /* display the tray button */ + ); - // create a list of children which are located between the task button container and the notify tray (i.e. windows which are occupying the same region we want to - // occupy...so we can try to avoid overlapping) - List freeAreaChildRects = new List(); - foreach (var taskbarChildHandle in taskbarChildHandles) - { - if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) + if (repositionTrayButtonSuccess == false) { - var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; - if ((taskbarChildRect.IsInside(freeAreaAvailableRect) == true) && - (taskbarChildRect.HasNonZeroWidthOrHeight() == false)) - { - freeAreaChildRects.Add(taskbarChildRect); - } + // failed; abort + Debug.Assert(false, "Could not reposition and/or resize tray button"); + return; } - } - - return (freeAreaAvailableRect, freeAreaChildRects); - } - - // NOTE: this function returns a newPosition IF the tray button should be moved - private (LegacyWindowsApi.RECT? currentRect, LegacyWindowsApi.RECT? changeToRect, System.Windows.Forms.Orientation orientation)? CalculateCurrentAndTargetRectOfTrayButton() - { - // NOTE: there are scenarios we must deal with where there may be multiple potential "taskbar button" icons to the left of the notification tray; in those scenarios, we must: - // 1. Position ourself to the left of the other icon-button(s) (or in an empty space in between them) - // 2. Reposition our icon when the other icon-button(s) are removed from the taskbar (e.g. when their host applications close them) - // 3. If we detect that we and another application are writing on top of each other (or repositioning the taskbar button container on top of our icon), then we must fail - // gracefully and let our host application know so it can warn the user, place the icon in the notification tray instead, etc. - - // To position the tray button, we need to find three windows: - // 1. the taskbar itself - // 2. the section of the taskbar which holds the taskbar buttons (i.e. to the right of the start button and find/cortana/taskview buttons, but to the left of the notification tray) */ - // 3. the notification tray - // - // We will then resize the section of the taskbar that holds the taskbar buttons so that we can place our tray button to its right (i.e. to the left of the notification tray). - - var taskbarTripletHandles = this.GetTaskbarTripletHandles(); - var taskbarHandle = taskbarTripletHandles.TaskbarHandle; - - var taskbarRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); - if (taskbarRects is null) - { - return null; - } - var taskbarRect = taskbarRects.Value.TaskbarRect; - var taskButtonContainerRect = taskbarRects.Value.TaskButtonContainerRect; - var notifyTrayRect = taskbarRects.Value.NotifyTrayRect; - - // determine the taskbar's orientation - System.Windows.Forms.Orientation taskbarOrientation; - if ((taskbarRect.Right - taskbarRect.Left) > (taskbarRect.Bottom - taskbarRect.Top)) - { - taskbarOrientation = System.Windows.Forms.Orientation.Horizontal; - } - else - { - taskbarOrientation = System.Windows.Forms.Orientation.Vertical; - } - - // calculate all of the free rects between the task button container and notify tray - var calculateEmptyRectsResult = this.CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(taskbarHandle, taskbarOrientation, taskbarRect, taskButtonContainerRect, notifyTrayRect); - var freeAreaChildRects = calculateEmptyRectsResult.childRects; - var freeAreaAvailableRect = calculateEmptyRectsResult.availableAreaRect; - - /* determine the rect for our tray button; based on our current positioning strategy, this will either be its existing position or the leftmost/topmost "next to tray" position. - * If we are determining the leftmost/topmost "next to tray" position, we will find the available space between the task button container and the notification tray (or any - * already-present controls that are already left/top of the notification tray); if there is not enough free space available in that area then we will shrink the task button - * container to make room. */ - // - /* NOTE: there are some deficiencies to our current positioning strategy. Of note... - * 1. In some circumstances, it might be possible that we are leaving "holes" of available space between the task button container and the notification tray; but if that - * happens, it might be something beyond our control (as other apps may have created that space). One concern is if we shrink our icon (in which case we should in theory - * shrink the space to our top/left) - * 2. If other apps draw their next-to-tray buttons after us and are not watching for conflicts then they could draw over us; a mitigation measure in that instance might be to - * use a timer to check that our tray button is not obscured and then remedy the situation; if we got into a "fight" over real estate that appeared to never terminate then - * we could destroy our icon and raise an event letting the application know it should choose an alternate strategy (such as a notification tray icon) instead. - * 3. If a more-rightmost/bottommost icon's application is closed while we are running, the taskbar could be resized to obscure us; we might need a timer (or we might need to - * capture the appropriate window message) to discover this scenario. - * In summary there is no standardized system (other than perhaps the "(dock) toolbar in taskbar" mechanism); if we find that we encounter problems in the field with our current - * strategy, we may want to consider rebuilding this functionality via the "toolbar in taskbar" mechanism. See HP Support Assistant for an example of another application - * which is doing what we are trying to do with the next-to-tray button strategy */ - - // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) - int trayButtonHeight; - int trayButtonWidth; - if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) - { - trayButtonHeight = taskbarRect.Bottom - taskbarRect.Top; - trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); - } - else - { - trayButtonWidth = taskbarRect.Right - taskbarRect.Left; - trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); - } - - // get our current rect (in case we can just reuse the current position...and also to make sure it doesn't need to be resized) - LegacyWindowsApi.RECT currentRectAsNonNullable; - LegacyWindowsApi.RECT? currentRect = null; - LegacyWindowsApi.RECT? currentRectForResult = null; - if (LegacyWindowsApi.GetWindowRect(this.Handle, out currentRectAsNonNullable) == true) - { - currentRect = currentRectAsNonNullable; - currentRectForResult = currentRectAsNonNullable; - } - - // if the current position of our window isn't the right size for our icon, then set it to NULL so we don't try to reuse it. - if ((currentRect is not null) && - ((currentRect.Value.Right - currentRect.Value.Left != trayButtonWidth) || (currentRect.Value.Bottom - currentRect.Value.Top != trayButtonHeight))) - { - currentRect = null; - } - - // calculate the new rect for our tray button's window - LegacyWindowsApi.RECT? newRect = null; - - // if the space occupied by our already-existing rect is not overlapped by anyone else and is in the free area, keep using the same space - if ((currentRect is not null) && (currentRect.Value.Intersects(freeAreaAvailableRect) == true)) - { - // by default, assume that our currentRect is still available (i.e. not overlapped) - bool currentRectIsNotOverlapped = true; - - // make sure we do not overlap another control in the free area - foreach (var freeAreaChildRect in freeAreaChildRects) + } + + // as we have moved/resized, request a repaint + this.RequestRedraw(); + + // if we have tooltip text, update its tracking rectangle + if (_tooltipText is not null) + { + UpdateTooltipTextAndTracking(); + } + } + } + + private (IntPtr TaskbarHandle, IntPtr TaskButtonContainerHandle, IntPtr NotifyTrayHandle) GetTaskbarTripletHandles() + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + var taskButtonContainerHandle = TrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); + var notifyTrayHandle = TrayButtonNativeWindow.FindWindowsTaskbarNotificationTrayHandle(); + + return (taskbarHandle, taskButtonContainerHandle, notifyTrayHandle); + } + + private (LegacyWindowsApi.RECT TaskbarRect, LegacyWindowsApi.RECT TaskButtonContainerRect, LegacyWindowsApi.RECT NotifyTrayRect)? GetTaskbarTripletRects(IntPtr taskbarHandle, IntPtr taskButtonContainerHandle, IntPtr notifyTrayHandle) + { + // find the taskbar and its rect + LegacyWindowsApi.RECT taskbarRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(taskbarHandle, out taskbarRect) == false) + { + // failed; abort + Debug.Assert(false, "Could not obtain window handle to taskbar."); + return null; + } + + // find the window handles and rects of the task button container and the notify tray (which are children inside of the taskbar) + // + LegacyWindowsApi.RECT taskButtonContainerRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(taskButtonContainerHandle, out taskButtonContainerRect) == false) + { + // failed; abort + Debug.Assert(false, "Could not obtain window handle to taskbar's task button list container."); + return null; + } + // + LegacyWindowsApi.RECT notifyTrayRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(notifyTrayHandle, out notifyTrayRect) == false) + { + // failed; abort + Debug.Assert(false, "Could not obtain window handle to taskbar's notify tray."); + return null; + } + + return (taskbarRect, taskButtonContainerRect, notifyTrayRect); + } + + private (LegacyWindowsApi.RECT availableAreaRect, List childRects) CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(IntPtr taskbarHandle, System.Windows.Forms.Orientation taskbarOrientation, LegacyWindowsApi.RECT taskbarRect, LegacyWindowsApi.RECT taskButtonContainerRect, LegacyWindowsApi.RECT notifyTrayRect) + { + // calculate the total "free area" rectangle (the area between the task button container and the notify tray where we want to place our tray button) + LegacyWindowsApi.RECT freeAreaAvailableRect; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(taskButtonContainerRect.Right, taskbarRect.Top, Math.Max(notifyTrayRect.Left - taskButtonContainerRect.Right, 0), Math.Max(taskbarRect.Bottom - taskbarRect.Top, 0))); + } + else + { + freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(taskbarRect.Left, taskButtonContainerRect.Bottom, Math.Max(taskbarRect.Right - taskbarRect.Left, 0), Math.Max(notifyTrayRect.Top - taskButtonContainerRect.Bottom, 0))); + } + + // capture a list of all child windows within the taskbar; we'll use this list to enumerate the rects of all the taskbar's children + var taskbarChildHandles = TrayButtonNativeWindow.EnumerateChildWindows(taskbarHandle); + // + // find the rects of all windows within the taskbar; we need this information so that we do not overlap any other accessory windows which are trying to sit in the same area as us + var taskbarChildHandlesWithRects = new Dictionary(); + foreach (var taskbarChildHandle in taskbarChildHandles) + { + LegacyWindowsApi.RECT taskbarChildRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(taskbarChildHandle, out taskbarChildRect) == true) + { + taskbarChildHandlesWithRects.Add(taskbarChildHandle, taskbarChildRect); + } + else + { + Debug.Assert(false, "Could not capture RECTs of all taskbar child windows"); + } + } + + // remove any child rects which are contained inside the task button container (so that we eliminate any subchildren from our calculations) + foreach (var taskbarChildHandle in taskbarChildHandles) + { + if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) + { + var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; + if (taskbarChildRect.IsInside(taskButtonContainerRect)) { - if (currentRect.Value.Intersects(freeAreaChildRect) == true) - { - // overlap conflict - currentRectIsNotOverlapped = false; - break; - } + taskbarChildHandlesWithRects.Remove(taskbarChildHandle); } - - if (currentRectIsNotOverlapped == true) + } + } + + // remove our own (tray button) window handle from the list (so that we don't see our current screen rect as "taken" in the list of occupied RECTs) + taskbarChildHandlesWithRects.Remove(this.Handle); + + // create a list of children which are located between the task button container and the notify tray (i.e. windows which are occupying the same region we want to + // occupy...so we can try to avoid overlapping) + List freeAreaChildRects = new List(); + foreach (var taskbarChildHandle in taskbarChildHandles) + { + if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) + { + var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; + if ((taskbarChildRect.IsInside(freeAreaAvailableRect) == true) && + (taskbarChildRect.HasNonZeroWidthOrHeight() == false)) { - // set "newRect" (the variable for where we will now place our tray button) to the same position we were already at - newRect = currentRect; + freeAreaChildRects.Add(taskbarChildRect); } - } - - // if our current (already-used-by-us) rect was not available, choose the leftmost/topmost space available - if (newRect is null) - { - if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + } + } + + return (freeAreaAvailableRect, freeAreaChildRects); + } + + // NOTE: this function returns a newPosition IF the tray button should be moved + private (LegacyWindowsApi.RECT? currentRect, LegacyWindowsApi.RECT? changeToRect, System.Windows.Forms.Orientation orientation)? CalculateCurrentAndTargetRectOfTrayButton() + { + // NOTE: there are scenarios we must deal with where there may be multiple potential "taskbar button" icons to the left of the notification tray; in those scenarios, we must: + // 1. Position ourself to the left of the other icon-button(s) (or in an empty space in between them) + // 2. Reposition our icon when the other icon-button(s) are removed from the taskbar (e.g. when their host applications close them) + // 3. If we detect that we and another application are writing on top of each other (or repositioning the taskbar button container on top of our icon), then we must fail + // gracefully and let our host application know so it can warn the user, place the icon in the notification tray instead, etc. + + // To position the tray button, we need to find three windows: + // 1. the taskbar itself + // 2. the section of the taskbar which holds the taskbar buttons (i.e. to the right of the start button and find/cortana/taskview buttons, but to the left of the notification tray) */ + // 3. the notification tray + // + // We will then resize the section of the taskbar that holds the taskbar buttons so that we can place our tray button to its right (i.e. to the left of the notification tray). + + var taskbarTripletHandles = this.GetTaskbarTripletHandles(); + var taskbarHandle = taskbarTripletHandles.TaskbarHandle; + + var taskbarRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); + if (taskbarRects is null) + { + return null; + } + var taskbarRect = taskbarRects.Value.TaskbarRect; + var taskButtonContainerRect = taskbarRects.Value.TaskButtonContainerRect; + var notifyTrayRect = taskbarRects.Value.NotifyTrayRect; + + // determine the taskbar's orientation + System.Windows.Forms.Orientation taskbarOrientation; + if ((taskbarRect.Right - taskbarRect.Left) > (taskbarRect.Bottom - taskbarRect.Top)) + { + taskbarOrientation = System.Windows.Forms.Orientation.Horizontal; + } + else + { + taskbarOrientation = System.Windows.Forms.Orientation.Vertical; + } + + // calculate all of the free rects between the task button container and notify tray + var calculateEmptyRectsResult = this.CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(taskbarHandle, taskbarOrientation, taskbarRect, taskButtonContainerRect, notifyTrayRect); + var freeAreaChildRects = calculateEmptyRectsResult.childRects; + var freeAreaAvailableRect = calculateEmptyRectsResult.availableAreaRect; + + /* determine the rect for our tray button; based on our current positioning strategy, this will either be its existing position or the leftmost/topmost "next to tray" position. + * If we are determining the leftmost/topmost "next to tray" position, we will find the available space between the task button container and the notification tray (or any + * already-present controls that are already left/top of the notification tray); if there is not enough free space available in that area then we will shrink the task button + * container to make room. */ + // + /* NOTE: there are some deficiencies to our current positioning strategy. Of note... + * 1. In some circumstances, it might be possible that we are leaving "holes" of available space between the task button container and the notification tray; but if that + * happens, it might be something beyond our control (as other apps may have created that space). One concern is if we shrink our icon (in which case we should in theory + * shrink the space to our top/left) + * 2. If other apps draw their next-to-tray buttons after us and are not watching for conflicts then they could draw over us; a mitigation measure in that instance might be to + * use a timer to check that our tray button is not obscured and then remedy the situation; if we got into a "fight" over real estate that appeared to never terminate then + * we could destroy our icon and raise an event letting the application know it should choose an alternate strategy (such as a notification tray icon) instead. + * 3. If a more-rightmost/bottommost icon's application is closed while we are running, the taskbar could be resized to obscure us; we might need a timer (or we might need to + * capture the appropriate window message) to discover this scenario. + * In summary there is no standardized system (other than perhaps the "(dock) toolbar in taskbar" mechanism); if we find that we encounter problems in the field with our current + * strategy, we may want to consider rebuilding this functionality via the "toolbar in taskbar" mechanism. See HP Support Assistant for an example of another application + * which is doing what we are trying to do with the next-to-tray button strategy */ + + // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) + int trayButtonHeight; + int trayButtonWidth; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + trayButtonHeight = taskbarRect.Bottom - taskbarRect.Top; + trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); + } + else + { + trayButtonWidth = taskbarRect.Right - taskbarRect.Left; + trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); + } + + // get our current rect (in case we can just reuse the current position...and also to make sure it doesn't need to be resized) + LegacyWindowsApi.RECT currentRectAsNonNullable; + LegacyWindowsApi.RECT? currentRect = null; + LegacyWindowsApi.RECT? currentRectForResult = null; + if (LegacyWindowsApi.GetWindowRect(this.Handle, out currentRectAsNonNullable) == true) + { + currentRect = currentRectAsNonNullable; + currentRectForResult = currentRectAsNonNullable; + } + + // if the current position of our window isn't the right size for our icon, then set it to NULL so we don't try to reuse it. + if ((currentRect is not null) && + ((currentRect.Value.Right - currentRect.Value.Left != trayButtonWidth) || (currentRect.Value.Bottom - currentRect.Value.Top != trayButtonHeight))) + { + currentRect = null; + } + + // calculate the new rect for our tray button's window + LegacyWindowsApi.RECT? newRect = null; + + // if the space occupied by our already-existing rect is not overlapped by anyone else and is in the free area, keep using the same space + if ((currentRect is not null) && (currentRect.Value.Intersects(freeAreaAvailableRect) == true)) + { + // by default, assume that our currentRect is still available (i.e. not overlapped) + bool currentRectIsNotOverlapped = true; + + // make sure we do not overlap another control in the free area + foreach (var freeAreaChildRect in freeAreaChildRects) + { + if (currentRect.Value.Intersects(freeAreaChildRect) == true) { - // horizontal taskbar: find the leftmost rect in the available space (which we'll then carve the "rightmost" section out of) - LegacyWindowsApi.RECT leftmostRect = freeAreaAvailableRect; - - foreach (var freeAreaChildRect in freeAreaChildRects) - { - if (freeAreaChildRect.Left < leftmostRect.Right) - { - leftmostRect.Right = freeAreaChildRect.Left; - } - } - - // choose the rightmost space in the leftmostRect area; expand our tray button towards the left if/as necessary - newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(leftmostRect.Right - trayButtonWidth, leftmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + // overlap conflict + currentRectIsNotOverlapped = false; + break; } - else + } + + if (currentRectIsNotOverlapped == true) + { + // set "newRect" (the variable for where we will now place our tray button) to the same position we were already at + newRect = currentRect; + } + } + + // if our current (already-used-by-us) rect was not available, choose the leftmost/topmost space available + if (newRect is null) + { + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + // horizontal taskbar: find the leftmost rect in the available space (which we'll then carve the "rightmost" section out of) + LegacyWindowsApi.RECT leftmostRect = freeAreaAvailableRect; + + foreach (var freeAreaChildRect in freeAreaChildRects) { - // vertical taskbar: find the topmost rect in the available space (which we'll then carve the "bottommost" section out of) - LegacyWindowsApi.RECT topmostRect = freeAreaAvailableRect; - - foreach (var freeAreaChildRect in freeAreaChildRects) - { - if (freeAreaChildRect.Top < topmostRect.Bottom) - { - topmostRect.Bottom = freeAreaChildRect.Top; - } - } - - // choose the bottommost space in the topmostRect area; expand our tray button towards the top if/as necessary - newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(topmostRect.Right - trayButtonWidth, topmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + if (freeAreaChildRect.Left < leftmostRect.Right) + { + leftmostRect.Right = freeAreaChildRect.Left; + } } - } - - LegacyWindowsApi.RECT? changeToRect = null; - if (newRect != currentRectForResult) - { - changeToRect = newRect; - } - - return (currentRectForResult, changeToRect, taskbarOrientation); - } - - private bool RequestRedraw() - { - return LegacyWindowsApi.RedrawWindow( - this.Handle, - IntPtr.Zero, - IntPtr.Zero, - LegacyWindowsApi.RedrawWindowFlags.RDW_ERASE | LegacyWindowsApi.RedrawWindowFlags.RDW_INVALIDATE | LegacyWindowsApi.RedrawWindowFlags.RDW_ALLCHILDREN - ); - } - - internal static List EnumerateChildWindows(IntPtr parentHwnd) - { - var result = new List(); - - // create an unmanaged pointer to our list (using a GC-managed handle) - GCHandle resultGCHandle = GCHandle.Alloc(result, GCHandleType.Normal); - // convert our GCHandle into an IntPtr (which we will unconvert back to a GCHandler in the EnumChildWindows callback) - IntPtr resultGCHandleAsIntPtr = GCHandle.ToIntPtr(resultGCHandle); - - try - { - var enumFunction = new LegacyWindowsApi.EnumWindowsProc(TrayButtonNativeWindow.EnumerateChildWindowsCallback); - LegacyWindowsApi.EnumChildWindows(parentHwnd, enumFunction, resultGCHandleAsIntPtr); - - } - finally - { - if (resultGCHandle.IsAllocated) + + // choose the rightmost space in the leftmostRect area; expand our tray button towards the left if/as necessary + newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(leftmostRect.Right - trayButtonWidth, leftmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + } + else + { + // vertical taskbar: find the topmost rect in the available space (which we'll then carve the "bottommost" section out of) + LegacyWindowsApi.RECT topmostRect = freeAreaAvailableRect; + + foreach (var freeAreaChildRect in freeAreaChildRects) { - resultGCHandle.Free(); + if (freeAreaChildRect.Top < topmostRect.Bottom) + { + topmostRect.Bottom = freeAreaChildRect.Top; + } } - } - - return result; - } - internal static bool EnumerateChildWindowsCallback(IntPtr hwnd, IntPtr lParam) - { - // convert lParam back into the result list object - var resultGCHandle = GCHandle.FromIntPtr(lParam); - List? result = resultGCHandle.Target as List; - - if (result is not null) - { - result.Add(hwnd); - } - else - { - Debug.Assert(false, "Could not enumerate child windows"); - } - - return true; - } - - internal static IntPtr FindWindowsTaskbarHandle() - { - return LegacyWindowsApi.FindWindow("Shell_TrayWnd", null); - } - - private static IntPtr FindWindowsTaskbarTaskButtonContainerHandle() - { - var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) - { - return IntPtr.Zero; - } - return LegacyWindowsApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "ReBarWindow32", null); - } - - private static IntPtr FindWindowsTaskbarNotificationTrayHandle() - { - var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) - { - return IntPtr.Zero; - } - return LegacyWindowsApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "TrayNotifyWnd", null); - } - - } - #endregion + + // choose the bottommost space in the topmostRect area; expand our tray button towards the top if/as necessary + newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(topmostRect.Right - trayButtonWidth, topmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + } + } + + LegacyWindowsApi.RECT? changeToRect = null; + if (newRect != currentRectForResult) + { + changeToRect = newRect; + } + + return (currentRectForResult, changeToRect, taskbarOrientation); + } + + private bool RequestRedraw() + { + return LegacyWindowsApi.RedrawWindow( + this.Handle, + IntPtr.Zero, + IntPtr.Zero, + LegacyWindowsApi.RedrawWindowFlags.RDW_ERASE | LegacyWindowsApi.RedrawWindowFlags.RDW_INVALIDATE | LegacyWindowsApi.RedrawWindowFlags.RDW_ALLCHILDREN + ); + } + + internal static List EnumerateChildWindows(IntPtr parentHwnd) + { + var result = new List(); + + // create an unmanaged pointer to our list (using a GC-managed handle) + GCHandle resultGCHandle = GCHandle.Alloc(result, GCHandleType.Normal); + // convert our GCHandle into an IntPtr (which we will unconvert back to a GCHandler in the EnumChildWindows callback) + IntPtr resultGCHandleAsIntPtr = GCHandle.ToIntPtr(resultGCHandle); + + try + { + var enumFunction = new LegacyWindowsApi.EnumWindowsProc(TrayButtonNativeWindow.EnumerateChildWindowsCallback); + LegacyWindowsApi.EnumChildWindows(parentHwnd, enumFunction, resultGCHandleAsIntPtr); + + } + finally + { + if (resultGCHandle.IsAllocated) + { + resultGCHandle.Free(); + } + } + + return result; + } + internal static bool EnumerateChildWindowsCallback(IntPtr hwnd, IntPtr lParam) + { + // convert lParam back into the result list object + var resultGCHandle = GCHandle.FromIntPtr(lParam); + List? result = resultGCHandle.Target as List; + + if (result is not null) + { + result.Add(hwnd); + } + else + { + Debug.Assert(false, "Could not enumerate child windows"); + } + + return true; + } + + internal static IntPtr FindWindowsTaskbarHandle() + { + return LegacyWindowsApi.FindWindow("Shell_TrayWnd", null); + } + + private static IntPtr FindWindowsTaskbarTaskButtonContainerHandle() + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } + return LegacyWindowsApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "ReBarWindow32", null); + } + + private static IntPtr FindWindowsTaskbarNotificationTrayHandle() + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } + return LegacyWindowsApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "TrayNotifyWnd", null); + } + + } + #endregion } diff --git a/Morphic.Controls/TrayButton/Windows10/WindowsNative/MouseWindowMessageHook.cs b/Morphic.Controls/TrayButton/Windows10/WindowsNative/MouseWindowMessageHook.cs new file mode 100644 index 00000000..2698fc54 --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows10/WindowsNative/MouseWindowMessageHook.cs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Morphic.Controls.TrayButton.Windows10.WindowsNative; + +public class MouseWindowMessageHook : IDisposable +{ + PInvoke.User32.WindowsHookDelegate _filterFunction; + PInvoke.User32.SafeHookHandle _hookHandle; + private bool _isDisposed; + + PInvoke.RECT? _trackingRect = null; + + public struct WndProcEventArgs + { + public uint Message; + public int X; + public int Y; + } + public event EventHandler? WndProcEvent; + + public MouseWindowMessageHook() + { + // NOTE: we are using a low-level hook in this implementation, and we are monitoring mouse events globally (and then filtering by RECT below) + _filterFunction = new PInvoke.User32.WindowsHookDelegate(this.MessageFilterProc); + _hookHandle = PInvoke.User32.SetWindowsHookEx(PInvoke.User32.WindowsHookType.WH_MOUSE_LL, _filterFunction, IntPtr.Zero, 0 /* global hook */); + } + + public void UpdateTrackingRegion(PInvoke.RECT rect) + { + _trackingRect = rect; + } + + bool _lastMessageWasInTrackingRect = false; + // NOTE: ideally, we would create a queue of messages and then use Task.Run to run code which dequeued the latest messages sequentially + private int MessageFilterProc(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode < 0) + { + // per Microsoft's docs: if the code is less than zero, we must pass the message along with _no_ intermediate processing + // see: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644986(v=vs.85) + + // call the next hook in the chain and return its result + return PInvoke.User32.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); + } + + // NOTE: as this is a low-level hook, we must process the message in less than the LowLevelHooksTimeout value (in ms) specified at: + // HKEY_CURRENT_USER\Control Panel\Desktop + // [for this reason and others, we simply capture the events and add them to a thread-safe queue...and then dispatch them to the UI thread's event loop] + + switch (nCode) + { + case 0 /* HC_ACTION */: + // wParam and lParam contain information about a mouse message + { + // NOTE: wParam is one of: { WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP } + // NOTE: lParam is a MSLLHOOKSTRUCT structure instance; see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msllhookstruct + + var mouseEventInfo = Marshal.PtrToStructure(lParam); + + var eventArgs = new WndProcEventArgs() + { + Message = (uint)wParam.ToInt64(), + X = mouseEventInfo.pt.x, + Y = mouseEventInfo.pt.y + }; + + if (_trackingRect is not null) + { + if ((mouseEventInfo.pt.x >= _trackingRect.Value.left) && + (mouseEventInfo.pt.x <= _trackingRect.Value.right) && + (mouseEventInfo.pt.y >= _trackingRect.Value.top) && + (mouseEventInfo.pt.y <= _trackingRect.Value.bottom)) + { + // NOTE: this may not be guaranteed to execute in sequence + Task.Run(() => { WndProcEvent?.Invoke(this, eventArgs); }); + _lastMessageWasInTrackingRect = true; + } + else + { + if (_lastMessageWasInTrackingRect == true) + { + // send a WM_MOUSELEAVE event when the mouse leaves the tracking rect + eventArgs.Message = 0x02A3 /* WM_MOUSELEAVE */; + // + // NOTE: this may not be guaranteed to execute in sequence + Task.Run(() => { WndProcEvent?.Invoke(this, eventArgs); }); + } + + _lastMessageWasInTrackingRect = false; + } + } + else + { + // there is no tracking RECT, so track globally + // + // NOTE: this may not be guaranteed to execute in sequence + Task.Run(() => { WndProcEvent?.Invoke(this, eventArgs); }); + _lastMessageWasInTrackingRect = true; + } + } + // NOTE: we are not "processing" the message, so we will always fall-through and let the next hook in the chain process the message + break; + default: + // unsupported code + break; + } + + // call the next hook in the chain and return its result + return PInvoke.User32.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); + } + + #region IDisposable + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + // dispose any managed objects here + } + + // free unmanaged resources + + // NOTE: this function will return false if it fails + // NOTE: in theory the system should clean up after this hook handle automatically (so we could probably comment out the following two lines of code) + _ = LegacyWindowsApi.UnhookWindowsHookEx(_hookHandle.DangerousGetHandle()); + _hookHandle.SetHandleAsInvalid(); + + // set any large fields to null + + _isDisposed = true; + } + } + + ~MouseWindowMessageHook() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable +} diff --git a/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs b/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs index ec4c10d4..cf77bfa5 100644 --- a/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs +++ b/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -22,7 +22,6 @@ // * Consumer Electronics Association Foundation using Morphic.Core; -using PInvoke; using System; using System.Diagnostics; using System.Runtime.InteropServices; @@ -31,418 +30,424 @@ namespace Morphic.Controls.TrayButton.Windows11; internal class ArgbImageNativeWindow : System.Windows.Forms.NativeWindow, IDisposable { - private bool disposedValue; - - private System.Drawing.Bitmap? _sourceBitmap = null; - private System.Drawing.Bitmap? _sizedBitmap = null; - - private bool _visible; - - private static ushort? s_morphicArgbImageClassInfoExAtom = null; - - private ArgbImageNativeWindow() - { - } - - public static MorphicResult CreateNew(IntPtr parentHWnd, int x, int y, int width, int height) - { - var result = new ArgbImageNativeWindow(); - - /* register a custom native window class for our ARGB Image window (or refer to the already-registered class, if we captured it earlier in the application's execution) */ - const string nativeWindowClassName = "Morphic-ArgbImage"; - // - if (s_morphicArgbImageClassInfoExAtom is null) - { - // register our control's custom native window class - var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new WindowsApi.WndProc(result.WndProcCallback)); - var lpWndClassEx = new WindowsApi.WNDCLASSEX - { - cbSize = (uint)Marshal.SizeOf(typeof(WindowsApi.WNDCLASSEX)), - lpfnWndProc = pointerToWndProcCallback, - lpszClassName = nativeWindowClassName, - hCursor = PInvoke.User32.LoadCursor(IntPtr.Zero, (IntPtr)PInvoke.User32.Cursors.IDC_ARROW).DangerousGetHandle() - }; - - // NOTE: RegisterClassEx returns an ATOM (or 0 if the call failed) - var registerClassResult = WindowsApi.RegisterClassEx(ref lpWndClassEx); - if (registerClassResult == 0) // failure - { - var win32Exception = new PInvoke.Win32Exception(Marshal.GetLastWin32Error()); - if (win32Exception.NativeErrorCode == PInvoke.Win32ErrorCode.ERROR_CLASS_ALREADY_EXISTS) - { - Debug.Assert(false, "Class was already registered; we should have recorded this ATOM, and we cannot proceed"); - } - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.Win32Error((uint)win32Exception.ErrorCode)); - } - s_morphicArgbImageClassInfoExAtom = registerClassResult; - } - - /* create an instance of our native window */ - - var windowParams = new System.Windows.Forms.CreateParams() - { - ClassName = s_morphicArgbImageClassInfoExAtom.ToString(), // for simplicity, we pass the value of the custom class as its integer self but in string form; our CreateWindow function will parse this and convert it to an int - Caption = nativeWindowClassName, - Style = unchecked((int)(/*PInvoke.User32.WindowStyles.WS_CLIPSIBLINGS | */PInvoke.User32.WindowStyles.WS_POPUP | PInvoke.User32.WindowStyles.WS_VISIBLE)), - ExStyle = (int)(PInvoke.User32.WindowStylesEx.WS_EX_LAYERED/* | PInvoke.User32.WindowStylesEx.WS_EX_TOOLWINDOW*/ | PInvoke.User32.WindowStylesEx.WS_EX_TRANSPARENT), - //ClassStyle = ?, - X = x, - Y = y, - Width = width, - Height = height, - Parent = parentHWnd, - //Param = ?, - }; - - // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException or Win32Exception - try - { - result.CreateHandle(windowParams); - } - catch (PInvoke.Win32Exception ex) - { - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.Win32Error((uint)ex.ErrorCode)); - } - catch (Exception ex) - { - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.OtherException(ex)); - } - - // since we are making the image visible by default, set its visible state - result._visible = true; - - return MorphicResult.OkResult(result); - } - - // NOTE: the built-in CreateHandle function couldn't accept our custom class (an ATOM rather than a string) as input, so we have overridden CreateHandle and are calling CreateWindowEx manually - // NOTE: in some circumstances, it is possible that we are unable to create our window; our caller may want to consider retrying mechanism - public override void CreateHandle(System.Windows.Forms.CreateParams cp) - { - // NOTE: if cp.ClassName is a string parseable as a short unsigned integer, parse it into an unsigned short; otherwise use the string as the classname - IntPtr classNameAsIntPtr; - bool classNameAsIntPtrRequiresFree = false; - if (cp.ClassName is not null && ushort.TryParse(cp.ClassName, out var classNameAsUshort) == true) - { - classNameAsIntPtr = (IntPtr)classNameAsUshort; - } - else - { - if (cp.ClassName is not null) - { - classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); - classNameAsIntPtrRequiresFree = true; - } - else - { - classNameAsIntPtr = IntPtr.Zero; - } - } - // - try - { - // NOTE: CreateWindowEx will return IntPtr.Zero ("NULL") if it fails - var handle = WindowsApi.CreateWindowEx( - (PInvoke.User32.WindowStylesEx)cp.ExStyle, - classNameAsIntPtr, - cp.Caption, - (PInvoke.User32.WindowStyles)cp.Style, - cp.X, - cp.Y, - cp.Width, - cp.Height, - cp.Parent, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero - ); - if (handle == IntPtr.Zero) - { - var lastError = Marshal.GetLastWin32Error(); - throw new PInvoke.Win32Exception(lastError); - } - - this.AssignHandle(handle); - } - finally - { - if (classNameAsIntPtrRequiresFree == true) - { - Marshal.FreeHGlobal(classNameAsIntPtr); - } - } - } - - // - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - this.DestroyHandle(); - - // TODO: set large fields to null - disposedValue = true; - } - } - - ~ArgbImageNativeWindow() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - // NOTE: during initial creation of the window, callbacks are sent to this delegated event; after creation, messages are captured by the WndProc function instead - private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - switch ((PInvoke.User32.WindowMessage)msg) - { - case PInvoke.User32.WindowMessage.WM_CREATE: - // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit - if (Windows.Win32.PInvoke.BufferedPaintInit() != Windows.Win32.Foundation.HRESULT.S_OK) - { - // failed; abort - Debug.Assert(false, "Could not initialize buffered paint"); - return new IntPtr(-1); // abort window creation process - } - break; - default: - break; - } - - // pass all non-handled messages through to DefWindowProc - return PInvoke.User32.DefWindowProc(hWnd, (PInvoke.User32.WindowMessage)msg, wParam, lParam); - } - - // NOTE: this WndProc method processes all messages after the initial creation of the window - protected override void WndProc(ref System.Windows.Forms.Message m) - { - IntPtr? result = null; - - switch ((PInvoke.User32.WindowMessage)m.Msg) - { - case PInvoke.User32.WindowMessage.WM_NCDESTROY: - { - // NOTE: we are calling this in response to WM_NCDESTROY (instead of WM_DESTROY) - // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit - _ = Windows.Win32.PInvoke.BufferedPaintUnInit(); - - // NOTE: we pass along this message (i.e. we don't return a "handled" result) - } - break; - case PInvoke.User32.WindowMessage.WM_NCPAINT: - { - // we suppress all painting of the non-client areas (so that we can have a transparent window) - // return zero, indicating that we processed the message - result = IntPtr.Zero; - } - break; - default: - break; - } - - // if we handled the message, return 'result'; otherwise, if we did not handle the message, call through to DefWindowProc to handle the message - if (result is not null) - { - m.Result = result.Value!; - } - else - { - // NOTE: per the Microsoft .NET documentation, we should call base.WndProc to process any events which we have not handled; however, - // in our testing, this led to frequent crashes. So instead, we follow the traditional pattern and call DefWindowProc to handle any events which we have not handled - // see: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow.wndproc?view=windowsdesktop-6.0 - m.Result = PInvoke.User32.DefWindowProc(m.HWnd, (PInvoke.User32.WindowMessage)m.Msg, m.WParam, m.LParam); - //base.WndProc(ref m); // causes crashes (when other native windows are capturing/processing/passing along messages) - } - } - - // - - public System.Drawing.Bitmap? GetBitmap() - { - return _sourceBitmap; - } - - public void SetBitmap(System.Drawing.Bitmap? bitmap) - { - _sourceBitmap = bitmap; - this.RecreateSizedBitmap(bitmap); - - this.RequestRedraw(); - } - - public void SetPositionAndSize(Windows.Win32.Foundation.RECT rect) - { - // set the new window position (including size); we must resize the window before recreating the sized bitmap (which will be sized to the updated size) - Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); - - this.RecreateSizedBitmap(_sourceBitmap); - this.RequestRedraw(); - } - - public void SetVisbile(bool value) - { - if (_visible != value) - { - _visible = value; - - var windowStyle = PInvokeExtensions.GetWindowLongPtr_IntPtr((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_STYLE); - nint newWindowStyle; - if (_visible == true) - { - newWindowStyle = (nint)windowStyle | (nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_VISIBLE; - } - else - { - newWindowStyle = (nint)windowStyle & ~(nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_VISIBLE; - } - PInvokeExtensions.SetWindowLongPtr_IntPtr((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_STYLE, newWindowStyle); - } - } - - private void RecreateSizedBitmap(System.Drawing.Bitmap? bitmap) - { - if (bitmap != null) - { - _sizedBitmap = new System.Drawing.Bitmap(bitmap, this.GetCurrentSize()); - } - else - { - _sizedBitmap = null; - } - } - - private System.Drawing.Size GetCurrentSize() - { - Windows.Win32.PInvoke.GetWindowRect((Windows.Win32.Foundation.HWND)this.Handle, out var rect); - return new System.Drawing.Size(rect.right - rect.left, rect.bottom - rect.top); - } - - // - - private void RequestRedraw() - { - // update our layered bitmap - this.UpdateLayeredPainting(); - - // invalidate the window - PInvokeExtensions.RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, /*Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_ERASE | */Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_INVALIDATE/* | Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_ALLCHILDREN*/); - } - - private MorphicResult UpdateLayeredPainting() - { - var ownerHWnd = Windows.Win32.PInvoke.GetWindow((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.GET_WINDOW_CMD.GW_OWNER); - // - var size = this.GetCurrentSize(); - // - var sizedBitmap = _sizedBitmap; - - if (sizedBitmap is not null) - { - // create a GDI bitmap from the Bitmap (using (0, 0, 0, 0) as the color of the ARGB background i.e. transparent) - Windows.Win32.Graphics.Gdi.HGDIOBJ sizedBitmapPointer; - try - { - sizedBitmapPointer = (Windows.Win32.Graphics.Gdi.HGDIOBJ)sizedBitmap.GetHbitmap(System.Drawing.Color.FromArgb(0)); - } - catch - { - Debug.Assert(false, "Could not create GDI bitmap object from the sized bitmap."); + private bool disposedValue; + + private System.Drawing.Bitmap? _sourceBitmap = null; + private System.Drawing.Bitmap? _sizedBitmap = null; + + private bool _visible; + + private static ushort? s_morphicArgbImageClassInfoExAtom = null; + + private ArgbImageNativeWindow() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + // [none] + } + + // free unmanaged resources (unmanaged objects) and override finalizer + this.DestroyHandle(); + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // NOTE: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~ArgbImageNativeWindow() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public static MorphicResult CreateNew(IntPtr parentHWnd, int x, int y, int width, int height) + { + var result = new ArgbImageNativeWindow(); + + /* register a custom native window class for our ARGB Image window (or refer to the already-registered class, if we captured it earlier in the application's execution) */ + const string nativeWindowClassName = "Morphic-ArgbImage"; + // + if (s_morphicArgbImageClassInfoExAtom is null) + { + // register our control's custom native window class + var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new PInvokeExtensions.WndProc(result.WndProcCallback)); + var lpWndClassEx = new PInvokeExtensions.WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(typeof(PInvokeExtensions.WNDCLASSEX)), + lpfnWndProc = pointerToWndProcCallback, + lpszClassName = nativeWindowClassName, + hCursor = PInvoke.User32.LoadCursor(IntPtr.Zero, (IntPtr)PInvoke.User32.Cursors.IDC_ARROW).DangerousGetHandle() + }; + + // NOTE: RegisterClassEx returns an ATOM (or 0 if the call failed) + var registerClassResult = PInvokeExtensions.RegisterClassEx(ref lpWndClassEx); + if (registerClassResult == 0) // failure + { + var win32Exception = new PInvoke.Win32Exception(Marshal.GetLastWin32Error()); + if (win32Exception.NativeErrorCode == PInvoke.Win32ErrorCode.ERROR_CLASS_ALREADY_EXISTS) + { + Debug.Assert(false, "Class was already registered; we should have recorded this ATOM, and we cannot proceed"); + } + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)win32Exception.ErrorCode)); + } + s_morphicArgbImageClassInfoExAtom = registerClassResult; + } + + /* create an instance of our native window */ + + var windowParams = new System.Windows.Forms.CreateParams() + { + ClassName = s_morphicArgbImageClassInfoExAtom.ToString(), // for simplicity, we pass the value of the custom class as its integer self but in string form; our CreateWindow function will parse this and convert it to an int + Caption = nativeWindowClassName, + Style = unchecked((int)(/*PInvoke.User32.WindowStyles.WS_CLIPSIBLINGS | */PInvoke.User32.WindowStyles.WS_POPUP | PInvoke.User32.WindowStyles.WS_VISIBLE)), + ExStyle = (int)(PInvoke.User32.WindowStylesEx.WS_EX_LAYERED/* | PInvoke.User32.WindowStylesEx.WS_EX_TOOLWINDOW*/ | PInvoke.User32.WindowStylesEx.WS_EX_TRANSPARENT), + //ClassStyle = ?, + X = x, + Y = y, + Width = width, + Height = height, + Parent = parentHWnd, + //Param = ?, + }; + + // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException or Win32Exception + try + { + result.CreateHandle(windowParams); + } + catch (PInvoke.Win32Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)ex.ErrorCode)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.OtherException(ex)); + } + + // since we are making the image visible by default, set its visible state + result._visible = true; + + return MorphicResult.OkResult(result); + } + + // NOTE: the built-in CreateHandle function couldn't accept our custom class (an ATOM rather than a string) as input, so we have overridden CreateHandle and are calling CreateWindowEx manually + // NOTE: in some circumstances, it is possible that we are unable to create our window; our caller may want to consider retrying mechanism + public override void CreateHandle(System.Windows.Forms.CreateParams cp) + { + // NOTE: if cp.ClassName is a string parseable as a short unsigned integer, parse it into an unsigned short; otherwise use the string as the classname + IntPtr classNameAsIntPtr; + bool classNameAsIntPtrRequiresFree = false; + if (cp.ClassName is not null && ushort.TryParse(cp.ClassName, out var classNameAsUshort) == true) + { + classNameAsIntPtr = (IntPtr)classNameAsUshort; + } + else + { + if (cp.ClassName is not null) + { + classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); + classNameAsIntPtrRequiresFree = true; + } + else + { + classNameAsIntPtr = IntPtr.Zero; + } + } + // + try + { + // NOTE: CreateWindowEx will return IntPtr.Zero ("NULL") if it fails + var handle = PInvokeExtensions.CreateWindowEx( + (PInvoke.User32.WindowStylesEx)cp.ExStyle, + classNameAsIntPtr, + cp.Caption, + (PInvoke.User32.WindowStyles)cp.Style, + cp.X, + cp.Y, + cp.Width, + cp.Height, + cp.Parent, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + if (handle == IntPtr.Zero) + { + var lastError = Marshal.GetLastWin32Error(); + throw new PInvoke.Win32Exception(lastError); + } + + this.AssignHandle(handle); + } + finally + { + if (classNameAsIntPtrRequiresFree == true) + { + Marshal.FreeHGlobal(classNameAsIntPtr); + } + } + } + + // + + // NOTE: during initial creation of the window, callbacks are sent to this delegated event; after creation, messages are captured by the WndProc function instead + private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch ((PInvoke.User32.WindowMessage)msg) + { + case PInvoke.User32.WindowMessage.WM_CREATE: + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + if (Windows.Win32.PInvoke.BufferedPaintInit() != Windows.Win32.Foundation.HRESULT.S_OK) + { + // failed; abort + Debug.Assert(false, "Could not initialize buffered paint"); + return new IntPtr(-1); // abort window creation process + } + break; + default: + break; + } + + // pass all non-handled messages through to DefWindowProc + return PInvoke.User32.DefWindowProc(hWnd, (PInvoke.User32.WindowMessage)msg, wParam, lParam); + } + + // NOTE: this WndProc method processes all messages after the initial creation of the window + protected override void WndProc(ref System.Windows.Forms.Message m) + { + IntPtr? result = null; + + switch ((PInvoke.User32.WindowMessage)m.Msg) + { + case PInvoke.User32.WindowMessage.WM_NCDESTROY: + { + // NOTE: we are calling this in response to WM_NCDESTROY (instead of WM_DESTROY) + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + _ = Windows.Win32.PInvoke.BufferedPaintUnInit(); + + // NOTE: we pass along this message (i.e. we don't return a "handled" result) + } + break; + case PInvoke.User32.WindowMessage.WM_NCPAINT: + { + // we suppress all painting of the non-client areas (so that we can have a transparent window) + // return zero, indicating that we processed the message + result = IntPtr.Zero; + } + break; + default: + break; + } + + // if we handled the message, return 'result'; otherwise, if we did not handle the message, call through to DefWindowProc to handle the message + if (result is not null) + { + m.Result = result.Value!; + } + else + { + // NOTE: per the Microsoft .NET documentation, we should call base.WndProc to process any events which we have not handled; however, + // in our testing, this led to frequent crashes. So instead, we follow the traditional pattern and call DefWindowProc to handle any events which we have not handled + // see: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow.wndproc?view=windowsdesktop-6.0 + m.Result = PInvoke.User32.DefWindowProc(m.HWnd, (PInvoke.User32.WindowMessage)m.Msg, m.WParam, m.LParam); + //base.WndProc(ref m); // causes crashes (when other native windows are capturing/processing/passing along messages) + } + } + + // + + public System.Drawing.Bitmap? GetBitmap() + { + return _sourceBitmap; + } + + public void SetBitmap(System.Drawing.Bitmap? bitmap) + { + _sourceBitmap = bitmap; + this.RecreateSizedBitmap(bitmap); + + this.RequestRedraw(); + } + + public void SetPositionAndSize(Windows.Win32.Foundation.RECT rect) + { + // set the new window position (including size); we must resize the window before recreating the sized bitmap (which will be sized to the updated size) + Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + + this.RecreateSizedBitmap(_sourceBitmap); + this.RequestRedraw(); + } + + public void SetVisible(bool value) + { + if (_visible != value) + { + _visible = value; + + var windowStyle = PInvokeExtensions.GetWindowLongPtr_IntPtr((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_STYLE); + nint newWindowStyle; + if (_visible == true) + { + newWindowStyle = (nint)windowStyle | (nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_VISIBLE; + } + else + { + newWindowStyle = (nint)windowStyle & ~(nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_VISIBLE; + } + PInvokeExtensions.SetWindowLongPtr_IntPtr((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_STYLE, newWindowStyle); + } + } + + private void RecreateSizedBitmap(System.Drawing.Bitmap? bitmap) + { + if (bitmap != null) + { + _sizedBitmap = new System.Drawing.Bitmap(bitmap, this.GetCurrentSize()); + } + else + { + _sizedBitmap = null; + } + } + + private System.Drawing.Size GetCurrentSize() + { + Windows.Win32.PInvoke.GetWindowRect((Windows.Win32.Foundation.HWND)this.Handle, out var rect); + return new System.Drawing.Size(rect.right - rect.left, rect.bottom - rect.top); + } + + // + + private void RequestRedraw() + { + // update our layered bitmap + this.UpdateLayeredPainting(); + + // invalidate the window + PInvokeExtensions.RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, /*Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_ERASE | */Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_INVALIDATE/* | Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_ALLCHILDREN*/); + } + + private MorphicResult UpdateLayeredPainting() + { + var ownerHWnd = Windows.Win32.PInvoke.GetWindow((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.GET_WINDOW_CMD.GW_OWNER); + // + var size = this.GetCurrentSize(); + // + var sizedBitmap = _sizedBitmap; + + if (sizedBitmap is not null) + { + // create a GDI bitmap from the Bitmap (using (0, 0, 0, 0) as the color of the ARGB background i.e. transparent) + Windows.Win32.Graphics.Gdi.HGDIOBJ sizedBitmapPointer; + try + { + sizedBitmapPointer = (Windows.Win32.Graphics.Gdi.HGDIOBJ)sizedBitmap.GetHbitmap(System.Drawing.Color.FromArgb(0)); + } + catch + { + Debug.Assert(false, "Could not create GDI bitmap object from the sized bitmap."); + return MorphicResult.ErrorResult(); + } + try + { + var ownerDC = Windows.Win32.PInvoke.GetDC(ownerHWnd); + if (ownerDC.Value == IntPtr.Zero) + { + Debug.Assert(false, "Could not get owner DC so that we can draw the icon bitmap."); return MorphicResult.ErrorResult(); - } - try - { - var ownerDC = Windows.Win32.PInvoke.GetDC(ownerHWnd); - if (ownerDC.Value == IntPtr.Zero) + } + try + { + var sourceDC = Windows.Win32.PInvoke.CreateCompatibleDC(ownerDC); + if (sourceDC.Value == IntPtr.Zero) { - Debug.Assert(false, "Could not get owner DC so that we can draw the icon bitmap."); - return MorphicResult.ErrorResult(); + Debug.Assert(false, "Could not get create compatible DC for screen DC so that we can draw the icon bitmap."); + return MorphicResult.ErrorResult(); } try { - var sourceDC = Windows.Win32.PInvoke.CreateCompatibleDC(ownerDC); - if (sourceDC.Value == IntPtr.Zero) - { - Debug.Assert(false, "Could not get create compatible DC for screen DC so that we can draw the icon bitmap."); - return MorphicResult.ErrorResult(); - } - try - { - // select our bitmap in the source DC - var oldSourceDCObject = Windows.Win32.PInvoke.SelectObject(sourceDC, sizedBitmapPointer); - if (oldSourceDCObject.Value == new IntPtr(-1) /*HGDI_ERROR*/) - { - Debug.Assert(false, "Could not select the icon bitmap GDI object to update the layered window with the alpha-blended bitmap."); - return MorphicResult.ErrorResult(); - } - try - { - // configure our blend function to blend the bitmap into its background - var blendfunction = new Windows.Win32.Graphics.Gdi.BLENDFUNCTION() - { - BlendOp = (byte)Windows.Win32.PInvoke.AC_SRC_OVER, /* the only available blend op, this will place the source bitmap over the destination bitmap based on the alpha values of the source pixels */ - BlendFlags = 0, /* must be zero */ - SourceConstantAlpha = 255, /* use per-pixel alpha values */ - AlphaFormat = (byte)Windows.Win32.PInvoke.AC_SRC_ALPHA, /* the bitmap has an alpha channel; it MUST be a 32bpp bitmap */ - }; - var sourcePoint = new System.Drawing.Point(0, 0); - var flags = Windows.Win32.UI.WindowsAndMessaging.UPDATE_LAYERED_WINDOW_FLAGS.ULW_ALPHA; // this flag indicates the blendfunction should be used as the blend function - //var updateLayeredWindowSuccess = Windows.Win32.PInvoke.UpdateLayeredWindow((Windows.Win32.Foundation.HWND)this.Handle, ownerDC, position/* captured position of our window */, size, sourceDC, sourcePoint, (Windows.Win32.Foundation.COLORREF)0/* unused COLORREF*/, blendfunction, flags); - var updateLayeredWindowSuccess = Windows.Win32.PInvoke.UpdateLayeredWindow((Windows.Win32.Foundation.HWND)this.Handle, ownerDC, null/* current position is not changing */, size, sourceDC, sourcePoint, (Windows.Win32.Foundation.COLORREF)0/* unused COLORREF*/, blendfunction, flags); - if (updateLayeredWindowSuccess == false) - { - var win32Error = Marshal.GetLastWin32Error(); - Debug.Assert(false, "Could not update the layered window with the alpha-blended bitmap; win32 error code: " + win32Error.ToString()); - return MorphicResult.ErrorResult(); - } - } - finally - { - // restore the old source object for the source DC - var selectObjectResult = Windows.Win32.PInvoke.SelectObject(sourceDC, oldSourceDCObject); - if (selectObjectResult == new IntPtr(-1) /*HGDI_ERROR*/) - { - Debug.Assert(false, "Could not restore the screen's compatible DC to its previous object after attempting to update the layered window."); - } - } - } - finally - { - var deleteDCSuccess = Windows.Win32.PInvoke.DeleteDC(sourceDC); - Debug.Assert(deleteDCSuccess == true, "Could not delete the compatible DC for the owner DC."); - } - + // select our bitmap in the source DC + var oldSourceDCObject = Windows.Win32.PInvoke.SelectObject(sourceDC, sizedBitmapPointer); + if (oldSourceDCObject.Value == new IntPtr(-1) /*HGDI_ERROR*/) + { + Debug.Assert(false, "Could not select the icon bitmap GDI object to update the layered window with the alpha-blended bitmap."); + return MorphicResult.ErrorResult(); + } + try + { + // configure our blend function to blend the bitmap into its background + var blendfunction = new Windows.Win32.Graphics.Gdi.BLENDFUNCTION() + { + BlendOp = (byte)Windows.Win32.PInvoke.AC_SRC_OVER, /* the only available blend op, this will place the source bitmap over the destination bitmap based on the alpha values of the source pixels */ + BlendFlags = 0, /* must be zero */ + SourceConstantAlpha = 255, /* use per-pixel alpha values */ + AlphaFormat = (byte)Windows.Win32.PInvoke.AC_SRC_ALPHA, /* the bitmap has an alpha channel; it MUST be a 32bpp bitmap */ + }; + var sourcePoint = new System.Drawing.Point(0, 0); + var flags = Windows.Win32.UI.WindowsAndMessaging.UPDATE_LAYERED_WINDOW_FLAGS.ULW_ALPHA; // this flag indicates the blendfunction should be used as the blend function + //var updateLayeredWindowSuccess = Windows.Win32.PInvoke.UpdateLayeredWindow((Windows.Win32.Foundation.HWND)this.Handle, ownerDC, position/* captured position of our window */, size, sourceDC, sourcePoint, (Windows.Win32.Foundation.COLORREF)0/* unused COLORREF*/, blendfunction, flags); + var updateLayeredWindowSuccess = Windows.Win32.PInvoke.UpdateLayeredWindow((Windows.Win32.Foundation.HWND)this.Handle, ownerDC, null/* current position is not changing */, size, sourceDC, sourcePoint, (Windows.Win32.Foundation.COLORREF)0/* unused COLORREF*/, blendfunction, flags); + if (updateLayeredWindowSuccess == false) + { + var win32Error = Marshal.GetLastWin32Error(); + Debug.Assert(false, "Could not update the layered window with the alpha-blended bitmap; win32 error code: " + win32Error.ToString()); + return MorphicResult.ErrorResult(); + } + } + finally + { + // restore the old source object for the source DC + var selectObjectResult = Windows.Win32.PInvoke.SelectObject(sourceDC, oldSourceDCObject); + if (selectObjectResult == new IntPtr(-1) /*HGDI_ERROR*/) + { + Debug.Assert(false, "Could not restore the screen's compatible DC to its previous object after attempting to update the layered window."); + } + } } finally { - var releaseDcResult = Windows.Win32.PInvoke.ReleaseDC(ownerHWnd, ownerDC); - Debug.Assert(releaseDcResult == 1, "Could not release owner DC."); + var deleteDCSuccess = Windows.Win32.PInvoke.DeleteDC(sourceDC); + Debug.Assert(deleteDCSuccess == true, "Could not delete the compatible DC for the owner DC."); } - } - finally - { - var deleteObjectSuccess = Windows.Win32.PInvoke.DeleteObject(sizedBitmapPointer); - Debug.Assert(deleteObjectSuccess == true, "Could not delete the GDI bitmap object which was created from the icon bitmap"); - } - } - else - { - // NOTE: we do not support erasing the bitmap once it's created, so there is nothing to do here; the caller may hide the image by setting its visible state to true - } - - // if we reach here, the operation was successful - return MorphicResult.OkResult(); - } + + } + finally + { + var releaseDcResult = Windows.Win32.PInvoke.ReleaseDC(ownerHWnd, ownerDC); + Debug.Assert(releaseDcResult == 1, "Could not release owner DC."); + } + } + finally + { + var deleteObjectSuccess = Windows.Win32.PInvoke.DeleteObject(sizedBitmapPointer); + Debug.Assert(deleteObjectSuccess == true, "Could not delete the GDI bitmap object which was created from the icon bitmap"); + } + } + else + { + // NOTE: we do not support erasing the bitmap once it's created, so there is nothing to do here; the caller may hide the image by setting its visible state to true + } + + // if we reach here, the operation was successful + return MorphicResult.OkResult(); + } } diff --git a/Morphic.Controls/TrayButton/Windows11/CreateNewError.cs b/Morphic.Controls/TrayButton/Windows11/CreateNewError.cs deleted file mode 100644 index 752facff..00000000 --- a/Morphic.Controls/TrayButton/Windows11/CreateNewError.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using Morphic.Core; -using System; - -namespace Morphic.Controls.TrayButton.Windows11; - -// NOTE: this type is designed to be returned as the Error type in MorphicResult function results -internal record CreateNewError : MorphicAssociatedValueEnum -{ - // enum members - public enum Values - { - CouldNotCalculateWindowPosition, - OtherException/*(Exception exception)*/, - Win32Error/*(uint win32ErrorCode)*/ - } - - // functions to create member instances - public static CreateNewError CouldNotCalculateWindowPosition => new(Values.CouldNotCalculateWindowPosition); - public static CreateNewError OtherException(Exception ex) => new(Values.OtherException) { Exception = ex }; - public static CreateNewError Win32Error(uint win32ErrorCode) => new(Values.Win32Error) { Win32ErrorCode = win32ErrorCode }; - - // associated values - public Exception? Exception { get; private init; } - public uint? Win32ErrorCode { get; private init; } - - // verbatim required constructor implementation for MorphicAssociatedValueEnums - private CreateNewError(Values value) : base(value) { } -} diff --git a/Morphic.Controls/TrayButton/Windows11/ICreateNewError.cs b/Morphic.Controls/TrayButton/Windows11/ICreateNewError.cs new file mode 100644 index 00000000..9cfd1d39 --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows11/ICreateNewError.cs @@ -0,0 +1,33 @@ +// Copyright 2020-2024 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; + +namespace Morphic.Controls.TrayButton.Windows11; + +public interface ICreateNewError +{ + public record CouldNotCalculateWindowPosition() : ICreateNewError; + public record OtherException(Exception exception) : ICreateNewError; + public record Win32Error(uint win32ErrorCode) : ICreateNewError; +} diff --git a/Morphic.Controls/TrayButton/Windows11/TrayButton.cs b/Morphic.Controls/TrayButton/Windows11/TrayButton.cs index 0481743a..23ae3637 100644 --- a/Morphic.Controls/TrayButton/Windows11/TrayButton.cs +++ b/Morphic.Controls/TrayButton/Windows11/TrayButton.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -29,180 +29,184 @@ namespace Morphic.Controls.TrayButton.Windows11; internal class TrayButton : IDisposable { - private System.Drawing.Bitmap? _bitmap = null; - private string? _text = null; - private bool _visible = false; - - public event System.Windows.Forms.MouseEventHandler? MouseUp; - - private TrayButtonNativeWindow? _nativeWindow = null; - - private bool disposedValue; - - internal TrayButton() - { - } - - // - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - this.DestroyNativeWindow(); - - // TODO: set large fields to null - disposedValue = true; - } - } - - ~TrayButton() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - // - - public System.Drawing.Bitmap? Bitmap - { - get - { - return _bitmap; - } - set - { - _bitmap = value; - - _nativeWindow?.SetBitmap(_bitmap); - } - } - - public string? Text - { - get - { - return _text; - } - set - { - _text = value; - - _nativeWindow?.SetText(_text); - } - } - - public bool Visible - { - set - { - if (_visible != value) - { - switch (value) - { - case true: - var showResult = this.Show(); - Debug.Assert(showResult.IsSuccess == true, "Could not show Morphic icon on task bar."); - break; - case false: - this.Hide(); - break; - } - } - } - get - { - return _visible; - } - } - - // - - public MorphicResult Show() - { - _visible = true; - - if (_nativeWindow is null) - { - var createNativeWindowResult = this.CreateNativeWindow(); - if (createNativeWindowResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - } - - return MorphicResult.OkResult(); - } - - public void Hide() - { - _visible = false; - - if (_nativeWindow is not null) - { - this.DestroyNativeWindow(); - } - } - - // - - public void SuppressTaskbarButtonResurfaceChecks(bool suppress) - { - _nativeWindow?.SuppressTaskbarButtonResurfaceChecks(suppress); - } - - // - - private MorphicResult CreateNativeWindow() - { - // if our native window already exists, return an error - if (_nativeWindow is not null) - { - return MorphicResult.ErrorResult(); - } - - // create the native window - var createNewResult = TrayButtonNativeWindow.CreateNew(); - if (createNewResult.IsError) - { - return MorphicResult.ErrorResult(); - } - var nativeWindow = createNewResult.Value!; - - // wire up the native window's MouseUp event (so that we pass along its event to our creator) - nativeWindow.MouseUp += (s, e) => - { - this.MouseUp?.Invoke(s, e); - }; - - // set the bitmap ("icon") for the native window - nativeWindow.SetBitmap(_bitmap); - // - // set the (tooltip) text for the native window - nativeWindow.SetText(_text); - - // store the reference to our new native window - _nativeWindow = nativeWindow; - - return MorphicResult.OkResult(); - } - - private void DestroyNativeWindow() - { - _nativeWindow?.Dispose(); - _nativeWindow = null; - } + private bool disposedValue; + + private System.Drawing.Bitmap? _bitmap = null; + private string? _text = null; + private bool _visible = false; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + private TrayButtonNativeWindow? _nativeWindow = null; + + internal TrayButton() + { + } + + // + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + this.DestroyManagedNativeWindow(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrayButton() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public System.Drawing.Bitmap? Bitmap + { + get + { + return _bitmap; + } + set + { + _bitmap = value; + + _nativeWindow?.SetBitmap(_bitmap); + } + } + + public string? Text + { + get + { + return _text; + } + set + { + _text = value; + + _nativeWindow?.SetText(_text); + } + } + + public bool Visible + { + set + { + if (_visible != value) + { + switch (value) + { + case true: + var showResult = this.Show(); + Debug.Assert(showResult.IsSuccess == true, "Could not show Morphic icon (taskbar button) on taskbar."); + break; + case false: + this.Hide(); + break; + } + } + } + get + { + return _visible; + } + } + + // + + public MorphicResult Show() + { + _visible = true; + + if (_nativeWindow is null) + { + var createNativeWindowResult = this.CreateNativeWindow(); + if (createNativeWindowResult.IsError == true) + { + return MorphicResult.ErrorResult(); + } + } + + return MorphicResult.OkResult(); + } + + public void Hide() + { + _visible = false; + + if (_nativeWindow is not null) + { + this.DestroyManagedNativeWindow(); + } + } + + // + + public void SuppressTaskbarButtonResurfaceChecks(bool suppress) + { + _nativeWindow?.SuppressTaskbarButtonResurfaceChecks(suppress); + } + + // + + private MorphicResult CreateNativeWindow() + { + // if our native window already exists, return an error + if (_nativeWindow is not null) + { + return MorphicResult.ErrorResult(); + } + + // create the native window + var createNewResult = TrayButtonNativeWindow.CreateNew(); + if (createNewResult.IsError) + { + return MorphicResult.ErrorResult(); + } + var nativeWindow = createNewResult.Value!; + + // wire up the native window's MouseUp event (so that we bubble up its event to our creator) + nativeWindow.MouseUp += (s, e) => + { + this.MouseUp?.Invoke(s, e); + }; + + // set the bitmap ("icon") for the native window + nativeWindow.SetBitmap(_bitmap); + // + // set the (tooltip) text for the native window + nativeWindow.SetText(_text); + + // store the reference to our new native window + _nativeWindow = nativeWindow; + + return MorphicResult.OkResult(); + } + + private void DestroyManagedNativeWindow() + { + _nativeWindow?.Dispose(); + _nativeWindow = null; + } } diff --git a/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs b/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs index 344974b3..ec90acaf 100644 --- a/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs +++ b/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -31,1227 +31,1230 @@ namespace Morphic.Controls.TrayButton.Windows11; internal class TrayButtonNativeWindow : System.Windows.Forms.NativeWindow, IDisposable { - private bool disposedValue; - - private static ushort? s_morphicTrayButtonClassInfoExAtom = null; - - private System.Windows.Visibility _visibility; - private bool _taskbarIsTopmost; - - private System.Threading.Timer? _resurfaceTaskbarButtonTimer; - private static readonly TimeSpan RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN = new TimeSpan(0, 0, 30); - - private ArgbImageNativeWindow? _argbImageNativeWindow = null; - - private IntPtr _tooltipWindowHandle; - private bool _tooltipInfoAdded = false; - private string? _tooltipText; - - private IntPtr _locationChangeWindowEventHook = IntPtr.Zero; - private WindowsApi.WinEventProc? _locationChangeWindowEventProc = null; - - private IntPtr _objectReorderWindowEventHook = IntPtr.Zero; - private WindowsApi.WinEventProc? _objectReorderWindowEventProc = null; - - [Flags] - private enum TrayButtonVisualStateFlags - { - None = 0, // normal visual state - Hover = 1, - LeftButtonPressed = 2, - RightButtonPressed = 4 - } - private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; - - private const byte ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE = 1; - - public event System.Windows.Forms.MouseEventHandler? MouseUp; - - private TrayButtonNativeWindow() - { - } - - public static MorphicResult CreateNew() - { - var result = new TrayButtonNativeWindow(); - - /* register a custom native window class for our Morphic Tray Button (or refer to the already-registered class, if we captured it earlier in the application's execution) */ - const string nativeWindowClassName = "Morphic-TrayButton"; - // - if (s_morphicTrayButtonClassInfoExAtom is null) - { - // register our control's custom native window class - var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new WindowsApi.WndProc(result.WndProcCallback)); - var lpWndClassEx = new WindowsApi.WNDCLASSEX - { - cbSize = (uint)Marshal.SizeOf(typeof(WindowsApi.WNDCLASSEX)), - lpfnWndProc = pointerToWndProcCallback, - lpszClassName = nativeWindowClassName, - hCursor = PInvoke.User32.LoadCursor(IntPtr.Zero, (IntPtr)PInvoke.User32.Cursors.IDC_ARROW).DangerousGetHandle() - }; - - // NOTE: RegisterClassEx returns an ATOM (or 0 if the call failed) - var registerClassResult = WindowsApi.RegisterClassEx(ref lpWndClassEx); - if (registerClassResult == 0) // failure - { - var win32Exception = new PInvoke.Win32Exception(Marshal.GetLastWin32Error()); - if (win32Exception.NativeErrorCode == PInvoke.Win32ErrorCode.ERROR_CLASS_ALREADY_EXISTS) + private bool disposedValue; + + private static ushort? s_morphicTrayButtonClassInfoExAtom = null; + + private System.Windows.Visibility _visibility; + private bool _taskbarIsTopmost; + + private System.Threading.Timer? _resurfaceTaskbarButtonTimer; + private static readonly TimeSpan RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN = new TimeSpan(0, 0, 30); + + private ArgbImageNativeWindow? _argbImageNativeWindow = null; + + private IntPtr _tooltipWindowHandle; + private bool _tooltipInfoAdded = false; + private string? _tooltipText; + + private IntPtr _locationChangeWindowEventHook = IntPtr.Zero; + private PInvokeExtensions.WinEventProc? _locationChangeWindowEventProc = null; + + private IntPtr _objectReorderWindowEventHook = IntPtr.Zero; + private PInvokeExtensions.WinEventProc? _objectReorderWindowEventProc = null; + + [Flags] + private enum TrayButtonVisualStateFlags + { + None = 0, // normal visual state + Hover = 1, + LeftButtonPressed = 2, + RightButtonPressed = 4 + } + private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; + + private const byte ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE = 1; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + private TrayButtonNativeWindow() + { + } + + // + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + if (_objectReorderWindowEventHook != IntPtr.Zero) + { + PInvokeExtensions.UnhookWinEvent(_objectReorderWindowEventHook); + } + if (_locationChangeWindowEventHook != IntPtr.Zero) + { + PInvokeExtensions.UnhookWinEvent(_locationChangeWindowEventHook); + } + + _argbImageNativeWindow?.Dispose(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + this.DestroyHandle(); + _ = this.DestroyTooltipWindow(); + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // NOTE: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~TrayButtonNativeWindow() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public static MorphicResult CreateNew() + { + var result = new TrayButtonNativeWindow(); + + /* register a custom native window class for our Morphic Tray Button (or refer to the already-registered class, if we captured it earlier in the application's execution) */ + const string nativeWindowClassName = "Morphic-TrayButton"; + // + if (s_morphicTrayButtonClassInfoExAtom is null) + { + // register our control's custom native window class + var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new PInvokeExtensions.WndProc(result.WndProcCallback)); + var lpWndClassEx = new PInvokeExtensions.WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(typeof(PInvokeExtensions.WNDCLASSEX)), + lpfnWndProc = pointerToWndProcCallback, + lpszClassName = nativeWindowClassName, + hCursor = PInvoke.User32.LoadCursor(IntPtr.Zero, (IntPtr)PInvoke.User32.Cursors.IDC_ARROW).DangerousGetHandle() + }; + + // NOTE: RegisterClassEx returns an ATOM (or 0 if the call failed) + var registerClassResult = PInvokeExtensions.RegisterClassEx(ref lpWndClassEx); + if (registerClassResult == 0) // failure + { + var win32Exception = new PInvoke.Win32Exception(Marshal.GetLastWin32Error()); + if (win32Exception.NativeErrorCode == PInvoke.Win32ErrorCode.ERROR_CLASS_ALREADY_EXISTS) + { + Debug.Assert(false, "Class was already registered; we should have recorded this ATOM, and we cannot proceed"); + } + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)win32Exception.ErrorCode)); + } + s_morphicTrayButtonClassInfoExAtom = registerClassResult; + } + + /* calculate the initial position of the tray button */ + var calculatePositionResult = TrayButtonNativeWindow.CalculatePositionAndSizeForTrayButton(null); + if (calculatePositionResult.IsError) + { + Debug.Assert(false, "Cannot calculate position for tray button"); + return MorphicResult.ErrorResult(new ICreateNewError.CouldNotCalculateWindowPosition()); + } + var trayButtonPositionAndSize = calculatePositionResult.Value!; + + /* get the handle for the taskbar; it will be the owner of our native window (so that our window sits above it in the zorder) */ + // NOTE: we will still need to push our window to the front of its owner's zorder stack in some circumstances, as certain actions (such as popping up the task list balloons above the task bar) will reorder the taskbar's zorder and push us behind the taskbar + // NOTE: making the taskbar our owner has the side-effect of putting our window above full-screen applications (even though our window is not itself "always on top"); we will need to hide our window whenever a window goes full-screen on the same monitor (and re-show our window whenever the window exits full-screen mode) + var taskbarHandle = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); + + // capture the current state of the taskbar; this is combined with the visibility value to determine whether or not the window is actually visible to the user + result._taskbarIsTopmost = TrayButtonNativeWindow.IsTaskbarTopmost(); + + + /* create an instance of our native window */ + + var windowParams = new System.Windows.Forms.CreateParams() + { + ClassName = s_morphicTrayButtonClassInfoExAtom.ToString(), // for simplicity, we pass the value of the custom class as its integer self but in string form; our CreateWindow function will parse this and convert it to an int + Caption = nativeWindowClassName, + Style = unchecked((int)(/*PInvoke.User32.WindowStyles.WS_CLIPSIBLINGS | */PInvoke.User32.WindowStyles.WS_POPUP /*| PInvoke.User32.WindowStyles.WS_TABSTOP*/ | PInvoke.User32.WindowStyles.WS_VISIBLE)), + ExStyle = (int)(PInvoke.User32.WindowStylesEx.WS_EX_LAYERED/* | PInvoke.User32.WindowStylesEx.WS_EX_TOOLWINDOW*//* | PInvoke.User32.WindowStylesEx.WS_EX_TOPMOST*/), + //ClassStyle = ?, + X = trayButtonPositionAndSize.left, + Y = trayButtonPositionAndSize.top, + Width = trayButtonPositionAndSize.right - trayButtonPositionAndSize.left, + Height = trayButtonPositionAndSize.bottom - trayButtonPositionAndSize.top, + Parent = taskbarHandle.Value, + //Param = ?, + }; + + // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException or Win32Exception + try + { + result.CreateHandle(windowParams); + } + catch (PInvoke.Win32Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)ex.ErrorCode)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.OtherException(ex)); + } + + // set the window's background transparency to 0% (in the range of a 0 to 255 alpha channel, with 255 being 100%) + // NOTE: an alpha value of 0 (0%) makes our window complete see-through but it has the side-effect of not capturing any mouse events; to counteract this, + // we set our "tranparent" alpha value to 1 instead. We will only use an alpha value of 0 when we want our window to be invisible and also not capture mouse events + var setBackgroundAlphaResult = TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)result.Handle, ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE); + if (setBackgroundAlphaResult.IsError) + { + switch (setBackgroundAlphaResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(uint win32ErrorCode): + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + + // since we are making the control visible by default, set its _visibility state + result._visibility = System.Windows.Visibility.Visible; + + // create an instance of the ArgbImageNativeWindow to hold our icon; we cannot draw the bitmap directly on this window as the bitmap would then be alphablended the same % as our background (instead of being independently blended over our window) + var argbImageNativeWindowResult = ArgbImageNativeWindow.CreateNew(result.Handle, windowParams.X, windowParams.Y, windowParams.Width, windowParams.Height); + if (argbImageNativeWindowResult.IsError == true) + { + result.Dispose(); + return MorphicResult.ErrorResult(argbImageNativeWindowResult.Error!); + } + result._argbImageNativeWindow = argbImageNativeWindowResult.Value!; + + /* wire up windows event hook listeners, to watch for events which require adjusting the zorder of our window */ + + // NOTE: we could provide the process handle and thread of processes/threads which we were interested in specifically, but for now we're interested in more than one window so we filter broadly + var locationChangeWindowEventProc = new PInvokeExtensions.WinEventProc(result.LocationChangeWindowEventProc); + var locationChangeWindowEventHook = PInvokeExtensions.SetWinEventHook( + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_LOCATIONCHANGE, // start index + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_LOCATIONCHANGE, // end index + IntPtr.Zero, + locationChangeWindowEventProc, + 0, // process handle (0 = all processes on current desktop) + 0, // thread (0 = all existing threads on current desktop) + PInvokeExtensions.WinEventHookFlags.WINEVENT_OUTOFCONTEXT | PInvokeExtensions.WinEventHookFlags.WINEVENT_SKIPOWNPROCESS + ); + Debug.Assert(locationChangeWindowEventHook != IntPtr.Zero, "Could not wire up location change window event listener for tray button"); + // + result._locationChangeWindowEventHook = locationChangeWindowEventHook; + // NOTE: we must capture the delegate so that it is not garbage collected; otherwise the native callbacks can crash the .NET execution engine + result._locationChangeWindowEventProc = locationChangeWindowEventProc; + // + // + // + var objectReorderWindowEventProc = new PInvokeExtensions.WinEventProc(result.ObjectReorderWindowEventProc); + var objectReorderWindowEventHook = PInvokeExtensions.SetWinEventHook( + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_REORDER, // start index + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_REORDER, // end index + IntPtr.Zero, + objectReorderWindowEventProc, + 0, // process handle (0 = all processes on current desktop) + 0, // thread (0 = all existing threads on current desktop) + PInvokeExtensions.WinEventHookFlags.WINEVENT_OUTOFCONTEXT | PInvokeExtensions.WinEventHookFlags.WINEVENT_SKIPOWNPROCESS + ); + Debug.Assert(objectReorderWindowEventHook != IntPtr.Zero, "Could not wire up object reorder window event listener for tray button"); + // + result._objectReorderWindowEventHook = objectReorderWindowEventHook; + // NOTE: we must capture the delegate so that it is not garbage collected; otherwise the native callbacks can crash the .NET execution engine + result._objectReorderWindowEventProc = objectReorderWindowEventProc; + + // create the tooltip window (although we won't provide it with any actual text until/unless the text is set + result._tooltipWindowHandle = result.CreateTooltipWindow(); + result._tooltipText = null; + + // start a timer on the new instance, to resurface the Morphic tray button icon from time to time (just in case it gets hidden under the taskbar) + result._resurfaceTaskbarButtonTimer = new(result.ResurfaceTaskButtonTimerCallback, null, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN); + + return MorphicResult.OkResult(result); + } + + // NOTE: the built-in CreateHandle function couldn't accept our custom class (an ATOM rather than a string) as input, so we have overridden CreateHandle and are calling CreateWindowEx manually + // NOTE: in some circumstances, it is possible that we are unable to create our window; our caller may want to consider retrying mechanism + public override void CreateHandle(System.Windows.Forms.CreateParams cp) + { + // NOTE: if cp.ClassName is a string parseable as a short unsigned integer, parse it into an unsigned short; otherwise use the string as the classname + IntPtr classNameAsIntPtr; + bool classNameAsIntPtrRequiresFree = false; + if (cp.ClassName is not null && ushort.TryParse(cp.ClassName, out var classNameAsUshort) == true) + { + classNameAsIntPtr = (IntPtr)classNameAsUshort; + } + else + { + if (cp.ClassName is not null) + { + classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); + classNameAsIntPtrRequiresFree = true; + } + else + { + classNameAsIntPtr = IntPtr.Zero; + } + } + // + try + { + // NOTE: CreateWindowEx will return IntPtr.Zero ("NULL") if it fails + var handle = PInvokeExtensions.CreateWindowEx( + (PInvoke.User32.WindowStylesEx)cp.ExStyle, + classNameAsIntPtr, + cp.Caption, + (PInvoke.User32.WindowStyles)cp.Style, + cp.X, + cp.Y, + cp.Width, + cp.Height, + cp.Parent, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + if (handle == IntPtr.Zero) + { + var lastError = Marshal.GetLastWin32Error(); + throw new PInvoke.Win32Exception(lastError); + } + + this.AssignHandle(handle); + } + finally + { + if (classNameAsIntPtrRequiresFree == true) + { + Marshal.FreeHGlobal(classNameAsIntPtr); + } + } + } + + + // Listen to when the handle changes to keep the argb image native window synced + protected override void OnHandleChange() + { + base.OnHandleChange(); + + // NOTE: if we ever need to update our children (or other owned windows) to let them know that our handle had changed, this is where we would add that code + } + + // NOTE: during initial creation of the window, callbacks are sent to this delegated event; after creation, messages are captured by the WndProc function instead + private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch ((PInvoke.User32.WindowMessage)msg) + { + case PInvoke.User32.WindowMessage.WM_CREATE: + // NOTE: it may not technically be necessary for us to use buffered painting for this control since we're effectively just painting it with a single fill color--but + // we do so to maintain consistency with the ArgbImageNativeWindow class and other user-painted forms + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + if (Windows.Win32.PInvoke.BufferedPaintInit() != Windows.Win32.Foundation.HRESULT.S_OK) + { + // failed; abort + Debug.Assert(false, "Could not initialize buffered paint"); + return new IntPtr(-1); // abort window creation process + } + break; + default: + break; + } + + // pass all non-handled messages through to DefWindowProc + return PInvoke.User32.DefWindowProc(hWnd, (PInvoke.User32.WindowMessage)msg, wParam, lParam); + } + + // NOTE: this WndProc method processes all messages after the initial creation of the window + protected override void WndProc(ref System.Windows.Forms.Message m) + { + IntPtr? result = null; + + switch ((PInvoke.User32.WindowMessage)m.Msg) + { + case PInvoke.User32.WindowMessage.WM_LBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.UpdateVisualStateAlpha(); + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_LBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + this.UpdateVisualStateAlpha(); + + var convertLParamResult = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (convertLParamResult.IsSuccess) { - Debug.Assert(false, "Class was already registered; we should have recorded this ATOM, and we cannot proceed"); - } - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.Win32Error((uint)win32Exception.ErrorCode)); - } - s_morphicTrayButtonClassInfoExAtom = registerClassResult; - } - - /* calculate the initial position of the tray button */ - var calculatePositionResult = TrayButtonNativeWindow.CalculatePositionAndSizeForTrayButton(null); - if (calculatePositionResult.IsError) - { - Debug.Assert(false, "Cannot calculate position for tray button"); - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.CouldNotCalculateWindowPosition); - } - var trayButtonPositionAndSize = calculatePositionResult.Value!; - - /* get the handle for the taskbar; it will be the owner of our native window (so that our window sits above it in the zorder) */ - // NOTE: we will still need to push our window to the front of its owner's zorder stack in some circumstances, as certain actions (such as popping up the task list balloons above the task bar) will reorder the taskbar's zorder and push us behind the taskbar - // NOTE: making the taskbar our owner has the side-effect of putting our window above full-screen applications (even though our window is not itself "always on top"); we will need to hide our window whenever a window goes full-screen on the same monitor (and re-show our window whenever the window exits full-screen mode) - var taskbarHandle = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); - - // capture the current state of the taskbar; this is combined with the visibility value to determine whether or not the window is actually visible to the user - result._taskbarIsTopmost = TrayButtonNativeWindow.IsTaskbarTopmost(); - - - /* create an instance of our native window */ - - var windowParams = new System.Windows.Forms.CreateParams() - { - ClassName = s_morphicTrayButtonClassInfoExAtom.ToString(), // for simplicity, we pass the value of the custom class as its integer self but in string form; our CreateWindow function will parse this and convert it to an int - Caption = nativeWindowClassName, - Style = unchecked((int)(/*PInvoke.User32.WindowStyles.WS_CLIPSIBLINGS | */PInvoke.User32.WindowStyles.WS_POPUP /*| PInvoke.User32.WindowStyles.WS_TABSTOP*/ | PInvoke.User32.WindowStyles.WS_VISIBLE)), - ExStyle = (int)(PInvoke.User32.WindowStylesEx.WS_EX_LAYERED/* | PInvoke.User32.WindowStylesEx.WS_EX_TOOLWINDOW*//* | PInvoke.User32.WindowStylesEx.WS_EX_TOPMOST*/), - //ClassStyle = ?, - X = trayButtonPositionAndSize.left, - Y = trayButtonPositionAndSize.top, - Width = trayButtonPositionAndSize.right - trayButtonPositionAndSize.left, - Height = trayButtonPositionAndSize.bottom - trayButtonPositionAndSize.top, - Parent = taskbarHandle.Value, - //Param = ?, - }; - - // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException or Win32Exception - try - { - result.CreateHandle(windowParams); - } - catch (PInvoke.Win32Exception ex) - { - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.Win32Error((uint)ex.ErrorCode)); - } - catch (Exception ex) - { - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.OtherException(ex)); - } - - // set the window's background transparency to 0% (in the range of a 0 to 255 alpha channel, with 255 being 100%) - // NOTE: an alpha value of 0 (0%) makes our window complete see-through but it has the side-effect of not capturing any mouse events; to counteract this, - // we set our "tranparent" alpha value to 1 instead. We will only use an alpha value of 0 when we want our window to be invisible and also not capture mouse events - var setBackgroundAlphaResult = TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)result.Handle, ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE); - if (setBackgroundAlphaResult.IsError) - { - switch (setBackgroundAlphaResult.Error!.Value) - { - case Morphic.WindowsNative.Win32ApiError.Values.Win32Error: - var win32Error = setBackgroundAlphaResult.Error!.Win32ErrorCode!.Value; - return MorphicResult.ErrorResult(Morphic.Controls.TrayButton.Windows11.CreateNewError.Win32Error(win32Error)); - default: - throw new Exception("invalid code path"); - } - } - - // since we are making the control visible by default, set its _visibility state - result._visibility = System.Windows.Visibility.Visible; - - // create an instance of the ArgbImageNativeWindow to hold our icon; we cannot draw the bitmap directly on this window as the bitmap would then be alphablended the same % as our background (instead of being independently blended over our window) - var argbImageNativeWindowResult = ArgbImageNativeWindow.CreateNew(result.Handle, windowParams.X, windowParams.Y, windowParams.Width, windowParams.Height); - if (argbImageNativeWindowResult.IsError == true) - { - result.Dispose(); - return MorphicResult.ErrorResult(argbImageNativeWindowResult.Error!); - } - result._argbImageNativeWindow = argbImageNativeWindowResult.Value!; - - /* wire up windows event hook listeners, to watch for events which require adjusting the zorder of our window */ - - // NOTE: we could provide the process handle and thread of processes/threads which we were interested in specifically, but for now we're interested in more than one window so we filter broadly - var locationChangeWindowEventProc = new WindowsApi.WinEventProc(result.LocationChangeWindowEventProc); - var locationChangeWindowEventHook = WindowsApi.SetWinEventHook( - WindowsApi.WinEventHookType.EVENT_OBJECT_LOCATIONCHANGE, // start index - WindowsApi.WinEventHookType.EVENT_OBJECT_LOCATIONCHANGE, // end index - IntPtr.Zero, - locationChangeWindowEventProc, - 0, // process handle (0 = all processes on current desktop) - 0, // thread (0 = all existing threads on current desktop) - WindowsApi.WinEventHookFlags.WINEVENT_OUTOFCONTEXT | WindowsApi.WinEventHookFlags.WINEVENT_SKIPOWNPROCESS - ); - Debug.Assert(locationChangeWindowEventHook != IntPtr.Zero, "Could not wire up location change window event listener for tray button"); - // - result._locationChangeWindowEventHook = locationChangeWindowEventHook; - // NOTE: we must capture the delegate so that it is not garbage collected; otherwise the native callbacks can crash the .NET execution engine - result._locationChangeWindowEventProc = locationChangeWindowEventProc; - // - // - // - var objectReorderWindowEventProc = new WindowsApi.WinEventProc(result.ObjectReorderWindowEventProc); - var objectReorderWindowEventHook = WindowsApi.SetWinEventHook( - WindowsApi.WinEventHookType.EVENT_OBJECT_REORDER, // start index - WindowsApi.WinEventHookType.EVENT_OBJECT_REORDER, // end index - IntPtr.Zero, - objectReorderWindowEventProc, - 0, // process handle (0 = all processes on current desktop) - 0, // thread (0 = all existing threads on current desktop) - WindowsApi.WinEventHookFlags.WINEVENT_OUTOFCONTEXT | WindowsApi.WinEventHookFlags.WINEVENT_SKIPOWNPROCESS - ); - Debug.Assert(objectReorderWindowEventHook != IntPtr.Zero, "Could not wire up object reorder window event listener for tray button"); - // - result._objectReorderWindowEventHook = objectReorderWindowEventHook; - // NOTE: we must capture the delegate so that it is not garbage collected; otherwise the native callbacks can crash the .NET execution engine - result._objectReorderWindowEventProc = objectReorderWindowEventProc; - - // create the tooltip window (although we won't provide it with any actual text until/unless the text is set - result._tooltipWindowHandle = result.CreateTooltipWindow(); - result._tooltipText = null; - - // start a timer on the new instance, to resurface the Morphic tray button icon from time to time (just in case it gets hidden under the taskbar) - result._resurfaceTaskbarButtonTimer = new(result.ResurfaceTaskButtonTimerCallback, null, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN); - - return MorphicResult.OkResult(result); - } - - // NOTE: the built-in CreateHandle function couldn't accept our custom class (an ATOM rather than a string) as input, so we have overridden CreateHandle and are calling CreateWindowEx manually - // NOTE: in some circumstances, it is possible that we are unable to create our window; our caller may want to consider retrying mechanism - public override void CreateHandle(System.Windows.Forms.CreateParams cp) - { - // NOTE: if cp.ClassName is a string parseable as a short unsigned integer, parse it into an unsigned short; otherwise use the string as the classname - IntPtr classNameAsIntPtr; - bool classNameAsIntPtrRequiresFree = false; - if (cp.ClassName is not null && ushort.TryParse(cp.ClassName, out var classNameAsUshort) == true) - { - classNameAsIntPtr = (IntPtr)classNameAsUshort; - } - else - { - if (cp.ClassName is not null) - { - classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); - classNameAsIntPtrRequiresFree = true; - } - else - { - classNameAsIntPtr = IntPtr.Zero; - } - } - // - try - { - // NOTE: CreateWindowEx will return IntPtr.Zero ("NULL") if it fails - var handle = WindowsApi.CreateWindowEx( - (PInvoke.User32.WindowStylesEx)cp.ExStyle, - classNameAsIntPtr, - cp.Caption, - (PInvoke.User32.WindowStyles)cp.Style, - cp.X, - cp.Y, - cp.Width, - cp.Height, - cp.Parent, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero - ); - if (handle == IntPtr.Zero) - { - var lastError = Marshal.GetLastWin32Error(); - throw new PInvoke.Win32Exception(lastError); - } - - this.AssignHandle(handle); - } - finally - { - if (classNameAsIntPtrRequiresFree == true) - { - Marshal.FreeHGlobal(classNameAsIntPtr); - } - } - } - - - // Listen to when the handle changes to keep the argb image native window synced - protected override void OnHandleChange() - { - base.OnHandleChange(); - - // NOTE: if we ever need to update our children (or other owned windows) to let them know that our handle had changed, this is where we would add that code - } - - // - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // dispose managed state (managed objects) - - if (_objectReorderWindowEventHook != IntPtr.Zero) - { - WindowsApi.UnhookWinEvent(_objectReorderWindowEventHook); + var hitPoint = convertLParamResult.Value!; + + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, hitPoint.X, hitPoint.Y, 0); + Task.Run(() => this.MouseUp?.Invoke(this, mouseArgs)); } - if (_locationChangeWindowEventHook != IntPtr.Zero) + else { - WindowsApi.UnhookWinEvent(_locationChangeWindowEventHook); + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); } - _argbImageNativeWindow?.Dispose(); - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - this.DestroyHandle(); - _ = this.DestroyTooltipWindow(); - - // TODO: set large fields to null - disposedValue = true; - } - } - - ~TrayButtonNativeWindow() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - // NOTE: during initial creation of the window, callbacks are sent to this delegated event; after creation, messages are captured by the WndProc function instead - private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - switch ((PInvoke.User32.WindowMessage)msg) - { - case PInvoke.User32.WindowMessage.WM_CREATE: - // NOTE: it may not technically be necessary for us to use buffered painting for this control since we're effectively just painting it with a single fill color--but - // we do so to maintain consistency with the ArgbImageNativeWindow class and other user-painted forms - // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit - if (Windows.Win32.PInvoke.BufferedPaintInit() != Windows.Win32.Foundation.HRESULT.S_OK) + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_MOUSELEAVE: + { + // the cursor has left our tray button's window area; remove the hover state from our visual state + _visualState &= ~TrayButtonVisualStateFlags.Hover; + + // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here + // (and then we check them again when the mouse moves back over the button) + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.UpdateVisualStateAlpha(); + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_MOUSEMOVE: + { + // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) + // + // NOTE: if the cursor moves off of the tray button while the button is pressed, we would have removed the "pressed" focus as well as the "hover" focus + // because we can't track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during + // mousemove so that we can re-visualize (re-set flags for) the pressed state as appropriate. + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & PInvokeExtensions.MK_LBUTTON) != 0)) { - // failed; abort - Debug.Assert(false, "Could not initialize buffered paint"); - return new IntPtr(-1); // abort window creation process + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.UpdateVisualStateAlpha(); } - break; - default: - break; - } - - // pass all non-handled messages through to DefWindowProc - return PInvoke.User32.DefWindowProc(hWnd, (PInvoke.User32.WindowMessage)msg, wParam, lParam); - } - - // NOTE: this WndProc method processes all messages after the initial creation of the window - protected override void WndProc(ref System.Windows.Forms.Message m) - { - IntPtr? result = null; - - switch ((PInvoke.User32.WindowMessage)m.Msg) - { - case PInvoke.User32.WindowMessage.WM_LBUTTONDOWN: + if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & PInvokeExtensions.MK_RBUTTON) != 0)) { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.UpdateVisualStateAlpha(); - - result = IntPtr.Zero; + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.UpdateVisualStateAlpha(); } - break; - case PInvoke.User32.WindowMessage.WM_LBUTTONUP: - { - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - this.UpdateVisualStateAlpha(); - - var convertLParamResult = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); - if (convertLParamResult.IsSuccess) - { - var hitPoint = convertLParamResult.Value!; - - var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, hitPoint.X, hitPoint.Y, 0); - Task.Run(() => this.MouseUp?.Invoke(this, mouseArgs)); - } - else - { - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - } - - result = IntPtr.Zero; - } - break; - case PInvoke.User32.WindowMessage.WM_MOUSELEAVE: - { - // the cursor has left our tray button's window area; remove the hover state from our visual state - _visualState &= ~TrayButtonVisualStateFlags.Hover; - - // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here - // (and then we check them again when the mouse moves back over the button) - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.UpdateVisualStateAlpha(); - result = IntPtr.Zero; - } - break; - case PInvoke.User32.WindowMessage.WM_MOUSEMOVE: - { - // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) - // - // NOTE: if the cursor moves off of the tray button while the button is pressed, we would have removed the "pressed" focus as well as the "hover" focus - // because we can't track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during - // mousemove so that we can re-visualize (re-set flags for) the pressed state as appropriate. - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & WindowsApi.MK_LBUTTON) != 0)) - { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.UpdateVisualStateAlpha(); - } - if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & WindowsApi.MK_RBUTTON) != 0)) - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.UpdateVisualStateAlpha(); - } - - result = IntPtr.Zero; - } - break; - case PInvoke.User32.WindowMessage.WM_NCDESTROY: + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_NCDESTROY: + { + // NOTE: we are calling this in response to WM_NCDESTROY (instead of WM_DESTROY) + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + _ = Windows.Win32.PInvoke.BufferedPaintUnInit(); + + // NOTE: we pass along this message (i.e. we don't return a "handled" result) + } + break; + case PInvoke.User32.WindowMessage.WM_NCPAINT: + { + // we suppress all painting of the non-client areas (so that we can have a transparent window) + // return zero, indicating that we processed the message + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_PAINT: + { + // NOTE: we override the built-in paint functionality with our own Paint function + this.OnPaintWindowsMessage((Windows.Win32.Foundation.HWND)m.HWnd); + // + // return zero, indicating that we processed the message + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.UpdateVisualStateAlpha(); + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.UpdateVisualStateAlpha(); + + var convertLParamResult = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (convertLParamResult.IsSuccess) { - // NOTE: we are calling this in response to WM_NCDESTROY (instead of WM_DESTROY) - // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit - _ = Windows.Win32.PInvoke.BufferedPaintUnInit(); + var hitPoint = convertLParamResult.Value!; - // NOTE: we pass along this message (i.e. we don't return a "handled" result) - } - break; - case PInvoke.User32.WindowMessage.WM_NCPAINT: - { - // we suppress all painting of the non-client areas (so that we can have a transparent window) - // return zero, indicating that we processed the message - result = IntPtr.Zero; + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, hitPoint.X, hitPoint.Y, 0); + Task.Run(() => this.MouseUp?.Invoke(this, mouseArgs)); } - break; - case PInvoke.User32.WindowMessage.WM_PAINT: + else { - // NOTE: we override the built-in paint functionality with our own Paint function - this.OnPaintWindowsMessage((Windows.Win32.Foundation.HWND)m.HWnd); - // - // return zero, indicating that we processed the message - result = IntPtr.Zero; + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); } - break; - case PInvoke.User32.WindowMessage.WM_RBUTTONDOWN: - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.UpdateVisualStateAlpha(); - result = IntPtr.Zero; - } - break; - case PInvoke.User32.WindowMessage.WM_RBUTTONUP: - { - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.UpdateVisualStateAlpha(); - - var convertLParamResult = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); - if (convertLParamResult.IsSuccess) - { - var hitPoint = convertLParamResult.Value!; - - var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, hitPoint.X, hitPoint.Y, 0); - Task.Run(() => this.MouseUp?.Invoke(this, mouseArgs)); - } - else - { - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - } - - result = IntPtr.Zero; - } - break; - case PInvoke.User32.WindowMessage.WM_SETCURSOR: + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_SETCURSOR: + { + // wParam: window handle + // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event + + var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); + var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); + + // NOTE: for messages which we handle, we return "TRUE" (1) to halt further message processing; this may not technically be necessary + // see: https://learn.microsoft.com/en-us/windows/win32/menurc/wm-setcursor + switch ((PInvoke.User32.WindowMessage)mouseMsg) { - // wParam: window handle - // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event - - var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); - var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); - - // NOTE: for messages which we handle, we return "TRUE" (1) to halt further message processing; this may not technically be necessary - // see: https://learn.microsoft.com/en-us/windows/win32/menurc/wm-setcursor - switch ((PInvoke.User32.WindowMessage)mouseMsg) - { - case PInvoke.User32.WindowMessage.WM_LBUTTONDOWN: - { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.UpdateVisualStateAlpha(); - - result = new IntPtr(1); - } - break; - case PInvoke.User32.WindowMessage.WM_LBUTTONUP: - { - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - this.UpdateVisualStateAlpha(); - - result = new IntPtr(1); - } - break; - case PInvoke.User32.WindowMessage.WM_MOUSEMOVE: - { - // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter"), then start tracking it now (so that we can capture its move out of our box) - // NOTE: we track whether or not we are tracking the mouse by analyzing the hover state of our visual state flags - if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) - { - // track mousehover (for tooltips) and mouseleave (to remove hover effect) - var eventTrack = WindowsApi.TRACKMOUSEEVENT.CreateNew(WindowsApi.TRACKMOUSEEVENTFlags.TME_LEAVE, this.Handle, WindowsApi.HOVER_DEFAULT); - var trackMouseEventSuccess = WindowsApi.TrackMouseEvent(ref eventTrack); - if (trackMouseEventSuccess == false) - { - // failed; we could capture the win32 error code via GetLastWin32Error - Debug.Assert(false, "Could not set up tracking of tray button window area"); - } - - _visualState |= TrayButtonVisualStateFlags.Hover; - this.UpdateVisualStateAlpha(); - } - result = new IntPtr(1); - } - break; - case PInvoke.User32.WindowMessage.WM_RBUTTONDOWN: - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.UpdateVisualStateAlpha(); - - result = new IntPtr(1); - } - break; - case PInvoke.User32.WindowMessage.WM_RBUTTONUP: - { - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.UpdateVisualStateAlpha(); - - result = new IntPtr(1); - } - break; - default: - // unhandled setcurosr mouse message - break; - } + case PInvoke.User32.WindowMessage.WM_LBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.UpdateVisualStateAlpha(); + + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_LBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + this.UpdateVisualStateAlpha(); + + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_MOUSEMOVE: + { + // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter"), then start tracking it now (so that we can capture its move out of our box) + // NOTE: we track whether or not we are tracking the mouse by analyzing the hover state of our visual state flags + if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) + { + // track mousehover (for tooltips) and mouseleave (to remove hover effect) + var eventTrack = PInvokeExtensions.TRACKMOUSEEVENT.CreateNew(PInvokeExtensions.TRACKMOUSEEVENTFlags.TME_LEAVE, this.Handle, PInvokeExtensions.HOVER_DEFAULT); + var trackMouseEventSuccess = PInvokeExtensions.TrackMouseEvent(ref eventTrack); + if (trackMouseEventSuccess == false) + { + // failed; we could capture the win32 error code via GetLastWin32Error + Debug.Assert(false, "Could not set up tracking of tray button window area"); + } + + _visualState |= TrayButtonVisualStateFlags.Hover; + this.UpdateVisualStateAlpha(); + } + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.UpdateVisualStateAlpha(); + + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.UpdateVisualStateAlpha(); + + result = new IntPtr(1); + } + break; + default: + // unhandled setcurosr mouse message + break; } - break; - default: - break; - } - - // if we handled the message, return 'result'; otherwise, if we did not handle the message, call through to DefWindowProc to handle the message - if (result is not null) - { - m.Result = result.Value!; - } - else - { - // NOTE: per the Microsoft .NET documentation, we should call base.WndProc to process any events which we have not handled; however, - // in our testing, this led to frequent crashes. So instead, we follow the traditional pattern and call DefWindowProc to handle any events which we have not handled - // see: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow.wndproc?view=windowsdesktop-6.0 - m.Result = PInvoke.User32.DefWindowProc(m.HWnd, (PInvoke.User32.WindowMessage)m.Msg, m.WParam, m.LParam); - //base.WndProc(ref m); // causes crashes (when other native windows are capturing/processing/passing along messages) - } - } - - // NOTE: this function may ONLY be called when responding to a WM_PAINT message - private void OnPaintWindowsMessage(Windows.Win32.Foundation.HWND hWnd) - { - // create a device context for drawing; we must destroy this automatically in a finally block. We are effectively replicating the functionality of C++'s CPaintDC. - Windows.Win32.Graphics.Gdi.PAINTSTRUCT paintStruct; - // NOTE: we experienced significant issues using PInvoke.User32.BeginPaint (possibly due to its IntPtr result wrapper), so we have redeclared the BeginPaint function ourselves - var deviceContext = Windows.Win32.PInvoke.BeginPaint(hWnd, out paintStruct)!; - if (deviceContext == IntPtr.Zero) - { - // no display context is available - Debug.Assert(false, "Cannot paint TrayButton in response to WM_PAINT message; no display context is available."); - return; - } - try - { - // NOTE: to avoid flickering, we use buffered painting to erase the background, fill the background with a single (white) brush, and then apply the painted area to the window in a single paint operation - Windows.Win32.Graphics.Gdi.HDC bufferedPaintDc; - var paintBufferHandle = Windows.Win32.PInvoke.BeginBufferedPaint(paintStruct.hdc, in paintStruct.rcPaint, Windows.Win32.UI.Controls.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, null, out bufferedPaintDc); - if (paintBufferHandle == IntPtr.Zero) - { - Debug.Assert(false, "Cannot begin a buffered paint operation for TrayButton (when responding to a WM_PAINT message)."); + } + break; + default: + break; + } + + // if we handled the message, return 'result'; otherwise, if we did not handle the message, call through to DefWindowProc to handle the message + if (result is not null) + { + m.Result = result.Value!; + } + else + { + // NOTE: per the Microsoft .NET documentation, we should call base.WndProc to process any events which we have not handled; however, + // in our testing, this led to frequent crashes. So instead, we follow the traditional pattern and call DefWindowProc to handle any events which we have not handled + // see: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow.wndproc?view=windowsdesktop-6.0 + m.Result = PInvoke.User32.DefWindowProc(m.HWnd, (PInvoke.User32.WindowMessage)m.Msg, m.WParam, m.LParam); + //base.WndProc(ref m); // causes crashes (when other native windows are capturing/processing/passing along messages) + } + } + + // NOTE: this function may ONLY be called when responding to a WM_PAINT message + private void OnPaintWindowsMessage(Windows.Win32.Foundation.HWND hWnd) + { + // create a device context for drawing; we must destroy this automatically in a finally block. We are effectively replicating the functionality of C++'s CPaintDC. + Windows.Win32.Graphics.Gdi.PAINTSTRUCT paintStruct; + // NOTE: we experienced significant issues using PInvoke.User32.BeginPaint (possibly due to its IntPtr result wrapper), so we have redeclared the BeginPaint function ourselves + var deviceContext = Windows.Win32.PInvoke.BeginPaint(hWnd, out paintStruct)!; + if (deviceContext == IntPtr.Zero) + { + // no display context is available + Debug.Assert(false, "Cannot paint TrayButton in response to WM_PAINT message; no display context is available."); + return; + } + try + { + // NOTE: to avoid flickering, we use buffered painting to erase the background, fill the background with a single (white) brush, and then apply the painted area to the window in a single paint operation + Windows.Win32.Graphics.Gdi.HDC bufferedPaintDc; + var paintBufferHandle = Windows.Win32.PInvoke.BeginBufferedPaint(paintStruct.hdc, in paintStruct.rcPaint, Windows.Win32.UI.Controls.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, null, out bufferedPaintDc); + if (paintBufferHandle == IntPtr.Zero) + { + Debug.Assert(false, "Cannot begin a buffered paint operation for TrayButton (when responding to a WM_PAINT message)."); + return; + } + try + { + // NOTE: this is the section where we call all of our actual (buffered) paint operations + + // clear our window's background (i.e. the buffer background) + var bufferedPaintClearHresult = Windows.Win32.PInvoke.BufferedPaintClear(paintBufferHandle, paintStruct.rcPaint); + if (bufferedPaintClearHresult != Windows.Win32.Foundation.HRESULT.S_OK) + { + Debug.Assert(false, "Could not clear background of TrayButton window--using buffered clearing (when responding to a WM_Paint message)."); return; - } - try - { - // NOTE: this is the section where we call all of our actual (buffered) paint operations - - // clear our window's background (i.e. the buffer background) - var bufferedPaintClearHresult = Windows.Win32.PInvoke.BufferedPaintClear(paintBufferHandle, paintStruct.rcPaint); - if (bufferedPaintClearHresult != Windows.Win32.Foundation.HRESULT.S_OK) - { - Debug.Assert(false, "Could not clear background of TrayButton window--using buffered clearing (when responding to a WM_Paint message)."); - return; - } + } - // create a solid white brush - var createSolidBrushResult = Windows.Win32.PInvoke.CreateSolidBrush((Windows.Win32.Foundation.COLORREF)0x00FFFFFF); - if (createSolidBrushResult == IntPtr.Zero) - { - Debug.Assert(false, "Could not create white brush to paint the background of the TrayButton window (when responding to a WM_Paint message)."); - return; - } - var whiteBrush = createSolidBrushResult; - try - { - int fillRectResult; - unsafe - { - fillRectResult = Windows.Win32.PInvoke.FillRect(bufferedPaintDc, &paintStruct.rcPaint, whiteBrush); - } - Debug.Assert(fillRectResult != 0, "Could not fill highlight background of Tray icon with white brush"); - } - finally - { - // clean up the white solid brush we created for the fill operation - var deleteObjectSuccess = Windows.Win32.PInvoke.DeleteObject(whiteBrush); - Debug.Assert(deleteObjectSuccess == true, "Could not delete white brush object used to highlight Tray icon"); - } - } - finally - { - // complete the buffered paint operation and free the buffered paint handle - var endBufferedPaintHresult = Windows.Win32.PInvoke.EndBufferedPaint(paintBufferHandle, true /* copy buffer to DC, completing the paint operation */); - Debug.Assert(endBufferedPaintHresult == Windows.Win32.Foundation.HRESULT.S_OK, "Error while attempting to end buffered paint operation for TrayButton; hresult: " + endBufferedPaintHresult); - } - } - finally - { - // mark the end of painting; this function must always be called when BeginPaint was called (and succeeded), and only after drawing is complete - // NOTE: per the MSDN docs, this function never returns zero (so there is no result to check) - _ = Windows.Win32.PInvoke.EndPaint(hWnd, in paintStruct); - } - } - - public System.Windows.Visibility Visibility - { - get - { - return _visibility; - } - set - { - if (_visibility != value) - { - _visibility = value; - this.UpdateVisibility(); - } - } - } - - private void UpdateVisibility() - { - _argbImageNativeWindow?.SetVisbile(this.ShouldWindowBeVisible()); - this.UpdateVisualStateAlpha(); - } - - private bool ShouldWindowBeVisible() - { - return (_visibility == System.Windows.Visibility.Visible) && (_taskbarIsTopmost == true); - } - - private void UpdateVisualStateAlpha() - { - // default to "Normal" visual state - Double highlightOpacity = 0.0; - - if (this.ShouldWindowBeVisible()) - { - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || - ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) - { - highlightOpacity = 0.25; - } - else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) - { - highlightOpacity = 0.1; - } - - var alpha = (byte)((double)255 * highlightOpacity); - TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)this.Handle, Math.Max(alpha, ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE)); - } - else - { - // collapsed or hidden controls should be invisible - TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)this.Handle, 0); - } - } - - private static MorphicResult SetBackgroundAlpha(Windows.Win32.Foundation.HWND handle, byte alpha) - { - // set the window's background transparency to 0% (in the range of a 0 to 255 alpha channel, with 255 being 100%) - var setLayeredWindowAttributesSuccess = Windows.Win32.PInvoke.SetLayeredWindowAttributes(handle, (Windows.Win32.Foundation.COLORREF)0, alpha, Windows.Win32.UI.WindowsAndMessaging.LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA); - if (setLayeredWindowAttributesSuccess == false) - { - var win32Error = (uint)System.Runtime.InteropServices.Marshal.GetLastWin32Error(); - return MorphicResult.ErrorResult(Morphic.WindowsNative.Win32ApiError.Win32Error(win32Error)); - } - - return MorphicResult.OkResult(); - } - - // - - private void LocationChangeWindowEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) - { - // we cannot process a location change message if the hwnd is zero - if (hwnd == IntPtr.Zero) - { - return; - } - - // attempt to capture the class name for the window; if the window has already been destroyed, this will fail - string? className = null; - var getWindowClassNameResult = TrayButtonNativeWindow.GetWindowClassName(hwnd); - if (getWindowClassNameResult.IsSuccess) - { - className = getWindowClassNameResult.Value!; - } - - if (className == "TaskListThumbnailWnd" || className == "TaskListOverlayWnd") - { - // if the window being moved was one of the task list windows (i.e. the windows that pop up above the taskbar), then our zorder has probably been pushed down. To counteract this, we make sure our window is "TOPMOST" - // NOTE: in initial testing, we set the window to TOPMOST in the ExStyles during handle construction. This was not always successful in keeping the window topmost, however, possibly because the taskbar becomes "more" topmost sometimes. So we re-set the window zorder here instead (without activating the window). - this.BringTaskButtonTopmostWithoutActivating(); - } - else if (className == "Shell_TrayWnd"/* || className == "ReBarWindow32"*/ || className == "TrayNotifyWnd") - { - // if the window being moved was the taskbar or the taskbar's notification tray, recalculate and update our position - // NOTE: we might also consider watching for location changes of the task button container, but as we don't use it for position/size calculations at the present time we do not watch accordingly - var repositionResult = this.RecalculatePositionAndRepositionWindow(); - Debug.Assert(repositionResult.IsSuccess, "Could not reposition Tray Button window"); - } - } - - // NOTE: this function is used to temporary suppress taskbar button resurface checks (which are done when the app needs to place other content above the taskbar and above our control...such as a right-click context menu) - public void SuppressTaskbarButtonResurfaceChecks(bool suppress) - { - if (suppress == true) - { - _resurfaceTaskbarButtonTimer?.Dispose(); - _resurfaceTaskbarButtonTimer = null; - } - else - { - _resurfaceTaskbarButtonTimer = new(this.ResurfaceTaskButtonTimerCallback, null, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN); - } - } - - // NOTE: just in case we miss any edge cases to resurface our button, we resurface it from time to time on a timer - private void ResurfaceTaskButtonTimerCallback(object? state) - { - this.BringTaskButtonTopmostWithoutActivating(); - } - - private void BringTaskButtonTopmostWithoutActivating() - { - Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.Foundation.HWND.HWND_TOPMOST, 0, 0, 0, 0, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOMOVE | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOSIZE | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); - } - - private void ObjectReorderWindowEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) - { - // we cannot process an object reorder message if the hwnd is zero - if (hwnd == IntPtr.Zero) - { - return; - } - - // attempt to capture the class name for the window; if the window has already been destroyed, this will fail - string? className = null; - var getWindowClassNameResult = TrayButtonNativeWindow.GetWindowClassName(hwnd); - if (getWindowClassNameResult.IsSuccess) - { - className = getWindowClassNameResult.Value!; - } - - // capture the desktop handle - var desktopHandle = Windows.Win32.PInvoke.GetDesktopWindow(); - - // if the reordered window was either the taskbar or the desktop, update the _taskbarIsTopmost state; this will generally be triggered when an app goes full-screen (or full-screen mode is exited) - if (className == "Shell_TrayWnd" || hwnd == desktopHandle.Value) - { - // whenever the window ordering changes, resurface our control - this.BringTaskButtonTopmostWithoutActivating(); - - // determine if the taskbar is topmost; the taskbar's topmost flag is removed when an app goes full-screen and should cover the taskbar (e.g. a full-screen video) - _taskbarIsTopmost = TrayButtonNativeWindow.IsTaskbarTopmost(/*hwnd -- not passed in, since the handle could be the desktop */); - // - // NOTE: UpdateVisibility takes both the .Visibility property and the topmost state of the taskbar into consideration to determine whether or not to show the control - this.UpdateVisibility(); - } - } - - private static bool IsTaskbarTopmost(Windows.Win32.Foundation.HWND? taskbarHWnd = null) - { - var taskbarHandle = taskbarHWnd ?? TrayButtonNativeWindow.GetWindowsTaskbarHandle(); - - var taskbarWindowExStyle = PInvokeExtensions.GetWindowLongPtr_IntPtr(taskbarHandle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); - var taskbarIsTopmost = ((nint)taskbarWindowExStyle & (nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_EX_STYLE.WS_EX_TOPMOST) != 0; - - return taskbarIsTopmost; - } - - private MorphicResult RecalculatePositionAndRepositionWindow() - { - // first, reposition our control (NOTE: this will be required to subsequently determine the position of our bitmap) - var calculatePositionResult = TrayButtonNativeWindow.CalculatePositionAndSizeForTrayButton(this.Handle); - if (calculatePositionResult.IsError) - { - Debug.Assert(false, "Cannot calculate position for tray button"); - return MorphicResult.ErrorResult(); - } - var trayButtonPositionAndSize = calculatePositionResult.Value!; - // - var size = new System.Drawing.Size(trayButtonPositionAndSize.right - trayButtonPositionAndSize.left, trayButtonPositionAndSize.bottom - trayButtonPositionAndSize.top); - Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, trayButtonPositionAndSize.left, trayButtonPositionAndSize.top, size.Width, size.Height, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); - - // once the control is repositioned, reposition the bitmap - var bitmap = _argbImageNativeWindow?.GetBitmap(); - if (bitmap is not null) - { - this.PositionAndResizeBitmap(bitmap); - } - - // also reposition the tooltip's tracking rectangle - if (_tooltipText is not null) - { - this.UpdateTooltipTextAndTracking(); - } - - - _ = Windows.Win32.PInvoke.BringWindowToTop((Windows.Win32.Foundation.HWND)this.Handle); - - return MorphicResult.OkResult(); - } - - private static MorphicResult GetWindowClassName(IntPtr hWnd) - { - System.Text.StringBuilder classNameBuilder = new(256); - var getClassNameResult = WindowsApi.GetClassName(hWnd, classNameBuilder, classNameBuilder.Capacity); - if (getClassNameResult == 0) - { - var win32Error = Marshal.GetLastWin32Error(); - return MorphicResult.ErrorResult(Morphic.WindowsNative.Win32ApiError.Win32Error((uint)win32Error)); - } - - var classNameAsString = classNameBuilder.ToString(); - return MorphicResult.OkResult(classNameAsString); - } - - // - - public void SetBitmap(System.Drawing.Bitmap? bitmap) - { - if (bitmap is not null) - { - this.PositionAndResizeBitmap(bitmap); - } - _argbImageNativeWindow?.SetBitmap(bitmap); - } - - private void PositionAndResizeBitmap(System.Drawing.Bitmap bitmap) - { - // then, reposition the bitmap - Windows.Win32.PInvoke.GetWindowRect((Windows.Win32.Foundation.HWND)this.Handle, out var positionAndSize); - var bitmapSize = bitmap.Size; - - var argbImageNativeWindowSize = TrayButtonNativeWindow.CalculateWidthAndHeightForBitmap(positionAndSize, bitmapSize); - var bitmapRect = TrayButtonNativeWindow.CalculateCenterRectInsideRect(positionAndSize, argbImageNativeWindowSize); - - _argbImageNativeWindow?.SetPositionAndSize(bitmapRect); - } - - public void SetText(string? text) - { - _tooltipText = text; - - this.UpdateTooltipTextAndTracking(); - } - - // - - private IntPtr CreateTooltipWindow() - { - if (_tooltipWindowHandle != IntPtr.Zero) - { - // tooltip window already exists - return _tooltipWindowHandle; - } - - var tooltipWindowHandle = PInvoke.User32.CreateWindowEx( - 0 /* no styles */, - WindowsApi.TOOLTIPS_CLASS, - null, - PInvoke.User32.WindowStyles.WS_POPUP | (PInvoke.User32.WindowStyles)WindowsApi.TTS_ALWAYSTIP, - WindowsApi.CW_USEDEFAULT, - WindowsApi.CW_USEDEFAULT, - WindowsApi.CW_USEDEFAULT, - WindowsApi.CW_USEDEFAULT, - this.Handle, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - // NOTE: Microsoft's documentation seems to indicate that we should set the tooltip as topmost, but in our testing this was unnecessary. It's possible that using SendMessage to add/remove tooltip text automatically handles this when the system handles showing the tooltip - // see: https://learn.microsoft.com/en-us/windows/win32/controls/tooltip-controls - //PInvoke.User32.SetWindowPos(tooltipWindowHandle, WindowsApi.HWND_TOPMOST, 0, 0, 0, 0, PInvoke.User32.SetWindowPosFlags.SWP_NOMOVE | PInvoke.User32.SetWindowPosFlags.SWP_NOSIZE | PInvoke.User32.SetWindowPosFlags.SWP_NOACTIVATE); - - Debug.Assert(tooltipWindowHandle != IntPtr.Zero, "Could not create tooltip window."); - - return tooltipWindowHandle; - } - - private bool DestroyTooltipWindow() - { - if (_tooltipWindowHandle == IntPtr.Zero) - { - return true; - } - - // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. - _tooltipText = null; - this.UpdateTooltipTextAndTracking(); - - var result = Windows.Win32.PInvoke.DestroyWindow((Windows.Win32.Foundation.HWND)_tooltipWindowHandle); - _tooltipWindowHandle = (Windows.Win32.Foundation.HWND)IntPtr.Zero; - - return result; - } - - private void UpdateTooltipTextAndTracking() - { - if (_tooltipWindowHandle == IntPtr.Zero) - { - // tooltip window does not exist; failed; abort - Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); - return; - } - - var trayButtonNativeWindowHandle = this.Handle; - if (trayButtonNativeWindowHandle == IntPtr.Zero) - { - // tray button window does not exist; there is no tool window to update - return; - } - - var getClientRectSuccess = PInvoke.User32.GetClientRect(this.Handle, out var trayButtonClientRect); - if (getClientRectSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip"); - return; - } - - var toolinfo = new WindowsApi.TOOLINFO(); - toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); - toolinfo.hwnd = this.Handle; - toolinfo.uFlags = Morphic.Controls.TrayButton.Windows10.LegacyWindowsApi.TTF_SUBCLASS; - toolinfo.lpszText = _tooltipText; - toolinfo.uId = unchecked((nuint)(nint)this.Handle); // unique identifier (for adding/deleting the tooltip) - toolinfo.rect = trayButtonClientRect; - // - var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); - try - { - Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); - if (toolinfo.lpszText is not null) - { - if (_tooltipInfoAdded == false) - { - _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)WindowsApi.TTM_ADDTOOL, IntPtr.Zero, pointerToToolinfo); - _tooltipInfoAdded = true; - } - else + // create a solid white brush + var createSolidBrushResult = Windows.Win32.PInvoke.CreateSolidBrush((Windows.Win32.Foundation.COLORREF)0x00FFFFFF); + if (createSolidBrushResult == IntPtr.Zero) + { + Debug.Assert(false, "Could not create white brush to paint the background of the TrayButton window (when responding to a WM_Paint message)."); + return; + } + var whiteBrush = createSolidBrushResult; + try + { + int fillRectResult; + unsafe { - // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) - _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)WindowsApi.TTM_DELTOOL, IntPtr.Zero, pointerToToolinfo); - _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)WindowsApi.TTM_ADDTOOL, IntPtr.Zero, pointerToToolinfo); + fillRectResult = Windows.Win32.PInvoke.FillRect(bufferedPaintDc, &paintStruct.rcPaint, whiteBrush); } - } - else - { - // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed - _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)WindowsApi.TTM_DELTOOL, IntPtr.Zero, pointerToToolinfo); - _tooltipInfoAdded = false; - } - } - finally - { - Marshal.FreeHGlobal(pointerToToolinfo); - } - } - - // - - /* helper functions */ - - internal static Windows.Win32.Foundation.RECT CalculateCenterRectInsideRect(Windows.Win32.Foundation.RECT outerRect, System.Drawing.Size innerSize) - { - var outerWidth = outerRect.right - outerRect.left; - var outerHeight = outerRect.bottom - outerRect.top; - - var innerWidth = innerSize.Width; - var innerHeight = innerSize.Height; - - var left = outerRect.left + ((outerWidth - innerWidth) / 2); - var top = outerRect.top + ((outerHeight - innerHeight) / 2); - var right = left + innerWidth; - var bottom = top + innerHeight; - - - return new Windows.Win32.Foundation.RECT() - { - left = left, - top = top, - right = right, - bottom = bottom, - }; - } - - internal static MorphicResult CalculatePositionAndSizeForTrayButton(IntPtr? trayButtonHandle) - { - // NOTE: in this implementation, we simply place the tray button over the taskbar, directly to the left of the system tray - // in the future, we may want to consider searching for any children which might occupy the area--and any system windows which are owned by the taskbar or any of its children--and then try to find a place to the "left" of those - - // get the handles for the taskbar, task button container, and the notify tray - // - var taskbarHandle = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) { return MorphicResult.ErrorResult(); } - // - var taskButtonContainerHandle = TrayButtonNativeWindow.GetWindowsTaskbarTaskButtonContainerHandle(taskbarHandle); - if (taskButtonContainerHandle == IntPtr.Zero) { return MorphicResult.ErrorResult(); } - // - var notifyTrayHandle = TrayButtonNativeWindow.GetWindowsTaskbarNotificationTrayHandle(taskbarHandle); - if (notifyTrayHandle == IntPtr.Zero) { return MorphicResult.ErrorResult(); } - - // get the RECTs for the taskbar, task button container and the notify tray - // - var getTaskbarRectSuccess = Windows.Win32.PInvoke.GetWindowRect(taskbarHandle, out var taskbarRect); - if (getTaskbarRectSuccess == false) { return MorphicResult.ErrorResult(); } - // - var getTaskButtonContainerRectSuccess = Windows.Win32.PInvoke.GetWindowRect(taskButtonContainerHandle, out var taskButtonContainerRect); - if (getTaskButtonContainerRectSuccess == false) { return MorphicResult.ErrorResult(); } - // - var getNotifyTrayRectSuccess = Windows.Win32.PInvoke.GetWindowRect(notifyTrayHandle, out var notifyTrayRect); - if (getNotifyTrayRectSuccess == false) { return MorphicResult.ErrorResult(); } - - // determine the taskbar's orientation - // - System.Windows.Forms.Orientation taskbarOrientation; - if ((taskbarRect.right - taskbarRect.left) > (taskbarRect.bottom - taskbarRect.top)) - { - taskbarOrientation = System.Windows.Forms.Orientation.Horizontal; - } - else - { - taskbarOrientation = System.Windows.Forms.Orientation.Vertical; - } - - // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) - int trayButtonHeight; - int trayButtonWidth; - // NOTE: on some computers, the taskbar and notify tray return an inaccurate size, but the task button container appears to always return the correct size; therefore we match our primary dimension to the taskbutton container's same dimension - // NOTE: the inaccurate size returned by GetWindowRect may be due to our moving this class from the main application to a helper library (i.e. perhaps the pixel scaling isn't applying correctly), or it could just be a weird quirk on some computers. - // [The GetWindowRect issue hapepns with both our own homebuilt PINVOKE methods as well as with PInvoke.User32.GetWindowRect; the function is returning the correct left, bottom and right positions of the taskbar and notify tray--but is - // sometimes misrepresenting the top (i.e. height) value of both the taskbar and notify tray rects] - if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) - { - // option 1: base our primary dimension off of the taskbutton container's same dimension - trayButtonHeight = taskButtonContainerRect.bottom - taskButtonContainerRect.top; - // - // option 2: base our primary dimension off of the taskbar's same dimension - //trayButtonHeight = taskbarRect.bottom - taskbarRect.top; - // - // [and then scale the secondary dimension to 80% of the size of the primary dimension] - trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); - } - else - { - // option 1: base our primary dimension off of the taskbutton container's same dimension - trayButtonWidth = taskButtonContainerRect.right - taskButtonContainerRect.left; - // - // option 2: base our primary dimension off of the taskbar's same dimension - //trayButtonWidth = taskbarRect.right - taskbarRect.left; - // - // [and then scale the secondary dimension to 80% of the size of the primary dimension] - trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); - } - - // choose a space in the rightmost/bottommost position of the taskbar - int trayButtonX; - int trayButtonY; - if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) - { - trayButtonX = notifyTrayRect.left - trayButtonWidth; - // NOTE: if we have any issues with positioning, try to replace taskbarRect.bottom with taskButtoncontainerRect.bottom (if we chose option #1 for our size calculations above) - trayButtonY = taskbarRect.bottom - trayButtonHeight; - } - else - { - // NOTE: if we have any issues with positioning, try to replace taskbarRect.bottom with taskButtoncontainerRect.right (if we chose option #1 for our size calculations above) - trayButtonX = taskbarRect.right - trayButtonWidth; - trayButtonY = notifyTrayRect.top - trayButtonHeight; - } - - var result = new Windows.Win32.Foundation.RECT() { left = trayButtonX, top = trayButtonY, right = trayButtonX + trayButtonWidth, bottom = trayButtonY + trayButtonHeight }; - return MorphicResult.OkResult(result); - } - - // - - private MorphicResult ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) - { - var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); - var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); - // convert x and y to screen coordinates - var hitPoint = new PInvoke.POINT { x = x, y = y }; - - // NOTE: the instructions for MapWindowPoints instruct us to call SetLastError before calling MapWindowPoints to ensure that we can distinguish a result of 0 from an error if the last win32 error wasn't set (because it wasn't an error) - Marshal.SetLastPInvokeError(0); - // - // NOTE: the PInvoke implementation of MapWindowPoints did not support passing in a POINT struct, so we manually declared the function - var mapWindowPointsResult = WindowsApi.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); - if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != 0) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - return MorphicResult.ErrorResult(); - } - - var result = new System.Drawing.Point(hitPoint.x, hitPoint.y); - return MorphicResult.OkResult(result); - } - - // - - // NOTE: this function takes the window size as input and calculates the size of the icon to display, centered, within the window. - private static System.Drawing.Size CalculateWidthAndHeightForBitmap(Windows.Win32.Foundation.RECT availableRect, System.Drawing.Size bitmapSize) - { - var availableSize = new System.Drawing.Size(availableRect.right - availableRect.left, availableRect.bottom - availableRect.top); - - /* determine the larger dimension (width or height) */ - //int largerDimensionSize; - //int smallerDimensionSize; - System.Drawing.Size insideMarginsSize; - if (availableSize.Height > availableSize.Width) - { - //largerDimensionSize = availableSize.Height; - //smallerDimensionSize = availableSize.Width; - // - // strategy 1: consume up to 90% of the width of the box and up to 66% of the height - //insideMarginsSize = new((int)((double)availableSize.Width * 0.9), (int)((double)availableSize.Height * (2.0 / 3.0))); - // - // strategy 2: consume up to 66% of the width of the box and up to 66% of the height - insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * (2.0 / 3.0))); - } - else - { - //largerDimensionSize = availableSize.Width; - //smallerDimensionSize = availableSize.Height; - // - // strategy 1: consume up to 66% of the width of the box and up to 90% of the height - //insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * 0.9)); - // - // strategy 2: consume up to 66% of the width of the box and up to 66% of the height - insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * (2.0 / 3.0))); - } - - /* shrink the bitmap size down so that it fits inside the available rect */ - - // by default, assume the bitmap will be the size of the source image - int bitmapWidth = bitmapSize.Width; - int bitmapHeight = bitmapSize.Height; - // - // if bitmap is wider than the available rect, shrink it equally in both directions - if (bitmapWidth > insideMarginsSize.Width) - { - double scaleFactor = (double)insideMarginsSize.Width / (double)bitmapWidth; - bitmapWidth = insideMarginsSize.Width; - bitmapHeight = (int)((double)bitmapHeight * scaleFactor); - } - // - // if bitmap is taller than the available rect, shrink it further (and equally in both directions) - if (bitmapHeight > insideMarginsSize.Height) - { - double scaleFactor = (double)insideMarginsSize.Height / (double)bitmapHeight; - bitmapWidth = (int)((double)bitmapWidth * scaleFactor); - bitmapHeight = insideMarginsSize.Height; - } - - // if bitmap does not touch either of the two margins (i.e. is too small), enlarge it now. - if (bitmapWidth != insideMarginsSize.Width && bitmapHeight != insideMarginsSize.Height) - { - // if bitmap is not as wide as the insideMarginsWidth, enlarge it now (equally in both directions) - if (bitmapWidth < insideMarginsSize.Width) - { - double scaleFactor = (double)insideMarginsSize.Width / (double)bitmapWidth; - bitmapWidth = insideMarginsSize.Width; - bitmapHeight = (int)((double)bitmapHeight * scaleFactor); - } - // - // if bitmap is now too tall, shrink it back down (equally in both directions) - if (bitmapHeight > insideMarginsSize.Height) - { - double scaleFactor = (double)insideMarginsSize.Height / (double)bitmapHeight; - bitmapWidth = (int)((double)bitmapWidth * scaleFactor); - bitmapHeight = insideMarginsSize.Height; - } - } - - return new System.Drawing.Size(bitmapWidth, bitmapHeight); - } - - // - - private static Windows.Win32.Foundation.HWND GetWindowsTaskbarHandle() - { - return Windows.Win32.PInvoke.FindWindow("Shell_TrayWnd", null); - } - // - private static Windows.Win32.Foundation.HWND GetWindowsTaskbarTaskButtonContainerHandle(Windows.Win32.Foundation.HWND taskbarHandle) - { - if (taskbarHandle.Value == IntPtr.Zero) - { - return (Windows.Win32.Foundation.HWND)IntPtr.Zero; - } - return Windows.Win32.PInvoke.FindWindowEx(taskbarHandle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, "ReBarWindow32", null); - } - // - private static Windows.Win32.Foundation.HWND GetWindowsTaskbarNotificationTrayHandle(Windows.Win32.Foundation.HWND taskbarHandle) - { - if (taskbarHandle.Value == IntPtr.Zero) - { - return (Windows.Win32.Foundation.HWND)IntPtr.Zero; - } - return Windows.Win32.PInvoke.FindWindowEx(taskbarHandle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, "TrayNotifyWnd", null); - } + Debug.Assert(fillRectResult != 0, "Could not fill highlight background of Tray icon with white brush"); + } + finally + { + // clean up the white solid brush we created for the fill operation + var deleteObjectSuccess = Windows.Win32.PInvoke.DeleteObject(whiteBrush); + Debug.Assert(deleteObjectSuccess == true, "Could not delete white brush object used to highlight Tray icon"); + } + } + finally + { + // complete the buffered paint operation and free the buffered paint handle + var endBufferedPaintHresult = Windows.Win32.PInvoke.EndBufferedPaint(paintBufferHandle, true /* copy buffer to DC, completing the paint operation */); + Debug.Assert(endBufferedPaintHresult == Windows.Win32.Foundation.HRESULT.S_OK, "Error while attempting to end buffered paint operation for TrayButton; hresult: " + endBufferedPaintHresult); + } + } + finally + { + // mark the end of painting; this function must always be called when BeginPaint was called (and succeeded), and only after drawing is complete + // NOTE: per the MSDN docs, this function never returns zero (so there is no result to check) + _ = Windows.Win32.PInvoke.EndPaint(hWnd, in paintStruct); + } + } + + public System.Windows.Visibility Visibility + { + get + { + return _visibility; + } + set + { + if (_visibility != value) + { + _visibility = value; + this.UpdateVisibility(); + } + } + } + + private void UpdateVisibility() + { + _argbImageNativeWindow?.SetVisible(this.ShouldWindowBeVisible()); + this.UpdateVisualStateAlpha(); + } + + private bool ShouldWindowBeVisible() + { + return (_visibility == System.Windows.Visibility.Visible) && (_taskbarIsTopmost == true); + } + + private void UpdateVisualStateAlpha() + { + // default to "Normal" visual state + Double highlightOpacity = 0.0; + + if (this.ShouldWindowBeVisible()) + { + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || + ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) + { + highlightOpacity = 0.25; + } + else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) + { + highlightOpacity = 0.1; + } + + var alpha = (byte)((double)255 * highlightOpacity); + TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)this.Handle, Math.Max(alpha, ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE)); + } + else + { + // collapsed or hidden controls should be invisible + TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)this.Handle, 0); + } + } + + private static MorphicResult SetBackgroundAlpha(Windows.Win32.Foundation.HWND handle, byte alpha) + { + // set the window's background transparency to 0% (in the range of a 0 to 255 alpha channel, with 255 being 100%) + var setLayeredWindowAttributesSuccess = Windows.Win32.PInvoke.SetLayeredWindowAttributes(handle, (Windows.Win32.Foundation.COLORREF)0, alpha, Windows.Win32.UI.WindowsAndMessaging.LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA); + if (setLayeredWindowAttributesSuccess == false) + { + var win32Error = (uint)System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error(win32Error)); + } + + return MorphicResult.OkResult(); + } + + // + + private void LocationChangeWindowEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) + { + // we cannot process a location change message if the hwnd is zero + if (hwnd == IntPtr.Zero) + { + return; + } + + // attempt to capture the class name for the window; if the window has already been destroyed, this will fail + string? className = null; + var getWindowClassNameResult = TrayButtonNativeWindow.GetWindowClassName(hwnd); + if (getWindowClassNameResult.IsSuccess) + { + className = getWindowClassNameResult.Value!; + } + + if (className == "TaskListThumbnailWnd" || className == "TaskListOverlayWnd") + { + // if the window being moved was one of the task list windows (i.e. the windows that pop up above the taskbar), then our zorder has probably been pushed down. To counteract this, we make sure our window is "TOPMOST" + // NOTE: in initial testing, we set the window to TOPMOST in the ExStyles during handle construction. This was not always successful in keeping the window topmost, however, possibly because the taskbar becomes "more" topmost sometimes. So we re-set the window zorder here instead (without activating the window). + this.BringTaskButtonTopmostWithoutActivating(); + } + else if (className == "Shell_TrayWnd"/* || className == "ReBarWindow32"*/ || className == "TrayNotifyWnd") + { + // if the window being moved was the taskbar or the taskbar's notification tray, recalculate and update our position + // NOTE: we might also consider watching for location changes of the task button container, but as we don't use it for position/size calculations at the present time we do not watch accordingly + var repositionResult = this.RecalculatePositionAndRepositionWindow(); + Debug.Assert(repositionResult.IsSuccess, "Could not reposition Tray Button window"); + } + } + + // NOTE: this function is used to temporary suppress taskbar button resurface checks (which are done when the app needs to place other content above the taskbar and above our control...such as a right-click context menu) + public void SuppressTaskbarButtonResurfaceChecks(bool suppress) + { + if (suppress == true) + { + _resurfaceTaskbarButtonTimer?.Dispose(); + _resurfaceTaskbarButtonTimer = null; + } + else + { + _resurfaceTaskbarButtonTimer = new(this.ResurfaceTaskButtonTimerCallback, null, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN); + } + } + + // NOTE: just in case we miss any edge cases to resurface our button, we resurface it from time to time on a timer + private void ResurfaceTaskButtonTimerCallback(object? state) + { + this.BringTaskButtonTopmostWithoutActivating(); + } + + private void BringTaskButtonTopmostWithoutActivating() + { + Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.Foundation.HWND.HWND_TOPMOST, 0, 0, 0, 0, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOMOVE | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOSIZE | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + } + + private void ObjectReorderWindowEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) + { + // we cannot process an object reorder message if the hwnd is zero + if (hwnd == IntPtr.Zero) + { + return; + } + + // attempt to capture the class name for the window; if the window has already been destroyed, this will fail + string? className = null; + var getWindowClassNameResult = TrayButtonNativeWindow.GetWindowClassName(hwnd); + if (getWindowClassNameResult.IsSuccess) + { + className = getWindowClassNameResult.Value!; + } + + // capture the desktop handle + var desktopHandle = Windows.Win32.PInvoke.GetDesktopWindow(); + + // if the reordered window was either the taskbar or the desktop, update the _taskbarIsTopmost state; this will generally be triggered when an app goes full-screen (or full-screen mode is exited) + if (className == "Shell_TrayWnd" || hwnd == desktopHandle.Value) + { + // whenever the window ordering changes, resurface our control + this.BringTaskButtonTopmostWithoutActivating(); + + // determine if the taskbar is topmost; the taskbar's topmost flag is removed when an app goes full-screen and should cover the taskbar (e.g. a full-screen video) + _taskbarIsTopmost = TrayButtonNativeWindow.IsTaskbarTopmost(/*hwnd -- not passed in, since the handle could be the desktop */); + // + // NOTE: UpdateVisibility takes both the .Visibility property and the topmost state of the taskbar into consideration to determine whether or not to show the control + this.UpdateVisibility(); + } + } + + private static bool IsTaskbarTopmost(Windows.Win32.Foundation.HWND? taskbarHWnd = null) + { + var taskbarHandle = taskbarHWnd ?? TrayButtonNativeWindow.GetWindowsTaskbarHandle(); + + var taskbarWindowExStyle = PInvokeExtensions.GetWindowLongPtr_IntPtr(taskbarHandle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + var taskbarIsTopmost = ((nint)taskbarWindowExStyle & (nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_EX_STYLE.WS_EX_TOPMOST) != 0; + + return taskbarIsTopmost; + } + + private MorphicResult RecalculatePositionAndRepositionWindow() + { + // first, reposition our control (NOTE: this will be required to subsequently determine the position of our bitmap) + var calculatePositionResult = TrayButtonNativeWindow.CalculatePositionAndSizeForTrayButton(this.Handle); + if (calculatePositionResult.IsError) + { + Debug.Assert(false, "Cannot calculate position for tray button"); + return MorphicResult.ErrorResult(); + } + var trayButtonPositionAndSize = calculatePositionResult.Value!; + // + var size = new System.Drawing.Size(trayButtonPositionAndSize.right - trayButtonPositionAndSize.left, trayButtonPositionAndSize.bottom - trayButtonPositionAndSize.top); + Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, trayButtonPositionAndSize.left, trayButtonPositionAndSize.top, size.Width, size.Height, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + + // once the control is repositioned, reposition the bitmap + var bitmap = _argbImageNativeWindow?.GetBitmap(); + if (bitmap is not null) + { + this.PositionAndResizeBitmap(bitmap); + } + + // also reposition the tooltip's tracking rectangle + if (_tooltipText is not null) + { + this.UpdateTooltipTextAndTracking(); + } + + + _ = Windows.Win32.PInvoke.BringWindowToTop((Windows.Win32.Foundation.HWND)this.Handle); + + return MorphicResult.OkResult(); + } + + private static MorphicResult GetWindowClassName(IntPtr hWnd) + { + System.Text.StringBuilder classNameBuilder = new(256); + var getClassNameResult = PInvokeExtensions.GetClassName(hWnd, classNameBuilder, classNameBuilder.Capacity); + if (getClassNameResult == 0) + { + var win32Error = Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error((uint)win32Error)); + } + + var classNameAsString = classNameBuilder.ToString(); + return MorphicResult.OkResult(classNameAsString); + } + + // + + public void SetBitmap(System.Drawing.Bitmap? bitmap) + { + if (bitmap is not null) + { + this.PositionAndResizeBitmap(bitmap); + } + _argbImageNativeWindow?.SetBitmap(bitmap); + } + + private void PositionAndResizeBitmap(System.Drawing.Bitmap bitmap) + { + // then, reposition the bitmap + Windows.Win32.PInvoke.GetWindowRect((Windows.Win32.Foundation.HWND)this.Handle, out var positionAndSize); + var bitmapSize = bitmap.Size; + + var argbImageNativeWindowSize = TrayButtonNativeWindow.CalculateWidthAndHeightForBitmap(positionAndSize, bitmapSize); + var bitmapRect = TrayButtonNativeWindow.CalculateCenterRectInsideRect(positionAndSize, argbImageNativeWindowSize); + + _argbImageNativeWindow?.SetPositionAndSize(bitmapRect); + } + + public void SetText(string? text) + { + _tooltipText = text; + + this.UpdateTooltipTextAndTracking(); + } + + // + + private IntPtr CreateTooltipWindow() + { + if (_tooltipWindowHandle != IntPtr.Zero) + { + // tooltip window already exists + return _tooltipWindowHandle; + } + + var tooltipWindowHandle = PInvoke.User32.CreateWindowEx( + 0 /* no styles */, + PInvokeExtensions.TOOLTIPS_CLASS, + null, + PInvoke.User32.WindowStyles.WS_POPUP | (PInvoke.User32.WindowStyles)PInvokeExtensions.TTS_ALWAYSTIP, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + this.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + // NOTE: Microsoft's documentation seems to indicate that we should set the tooltip as topmost, but in our testing this was unnecessary. It's possible that using SendMessage to add/remove tooltip text automatically handles this when the system handles showing the tooltip + // see: https://learn.microsoft.com/en-us/windows/win32/controls/tooltip-controls + //PInvoke.User32.SetWindowPos(tooltipWindowHandle, PInvokeExtensions.HWND_TOPMOST, 0, 0, 0, 0, PInvoke.User32.SetWindowPosFlags.SWP_NOMOVE | PInvoke.User32.SetWindowPosFlags.SWP_NOSIZE | PInvoke.User32.SetWindowPosFlags.SWP_NOACTIVATE); + + Debug.Assert(tooltipWindowHandle != IntPtr.Zero, "Could not create tooltip window."); + + return tooltipWindowHandle; + } + + private bool DestroyTooltipWindow() + { + if (_tooltipWindowHandle == IntPtr.Zero) + { + return true; + } + + // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. + _tooltipText = null; + this.UpdateTooltipTextAndTracking(); + + var result = Windows.Win32.PInvoke.DestroyWindow((Windows.Win32.Foundation.HWND)_tooltipWindowHandle); + _tooltipWindowHandle = (Windows.Win32.Foundation.HWND)IntPtr.Zero; + + return result; + } + + private void UpdateTooltipTextAndTracking() + { + if (_tooltipWindowHandle == IntPtr.Zero) + { + // tooltip window does not exist; failed; abort + Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); + return; + } + + var trayButtonNativeWindowHandle = this.Handle; + if (trayButtonNativeWindowHandle == IntPtr.Zero) + { + // tray button window does not exist; there is no tool window to update + return; + } + + var getClientRectSuccess = PInvoke.User32.GetClientRect(this.Handle, out var trayButtonClientRect); + if (getClientRectSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip"); + return; + } + + var toolinfo = new PInvokeExtensions.TOOLINFO(); + toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); + toolinfo.hwnd = this.Handle; + toolinfo.uFlags = PInvokeExtensions.TTF_SUBCLASS; + toolinfo.lpszText = _tooltipText; + toolinfo.uId = unchecked((nuint)(nint)this.Handle); // unique identifier (for adding/deleting the tooltip) + toolinfo.rect = trayButtonClientRect; + // + var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); + try + { + Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); + if (toolinfo.lpszText is not null) + { + if (_tooltipInfoAdded == false) + { + _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)PInvokeExtensions.TTM_ADDTOOL, IntPtr.Zero, pointerToToolinfo); + _tooltipInfoAdded = true; + } + else + { + // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) + _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)PInvokeExtensions.TTM_DELTOOL, IntPtr.Zero, pointerToToolinfo); + _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)PInvokeExtensions.TTM_ADDTOOL, IntPtr.Zero, pointerToToolinfo); + } + } + else + { + // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed + _ = PInvoke.User32.SendMessage(_tooltipWindowHandle, (PInvoke.User32.WindowMessage)PInvokeExtensions.TTM_DELTOOL, IntPtr.Zero, pointerToToolinfo); + _tooltipInfoAdded = false; + } + } + finally + { + Marshal.FreeHGlobal(pointerToToolinfo); + } + } + + // + + /* helper functions */ + + internal static Windows.Win32.Foundation.RECT CalculateCenterRectInsideRect(Windows.Win32.Foundation.RECT outerRect, System.Drawing.Size innerSize) + { + var outerWidth = outerRect.right - outerRect.left; + var outerHeight = outerRect.bottom - outerRect.top; + + var innerWidth = innerSize.Width; + var innerHeight = innerSize.Height; + + var left = outerRect.left + ((outerWidth - innerWidth) / 2); + var top = outerRect.top + ((outerHeight - innerHeight) / 2); + var right = left + innerWidth; + var bottom = top + innerHeight; + + + return new Windows.Win32.Foundation.RECT() + { + left = left, + top = top, + right = right, + bottom = bottom, + }; + } + + internal static MorphicResult CalculatePositionAndSizeForTrayButton(IntPtr? trayButtonHandle) + { + // NOTE: in this implementation, we simply place the tray button over the taskbar, directly to the left of the system tray + // in the future, we may want to consider searching for any children which might occupy the area--and any system windows which are owned by the taskbar or any of its children--and then try to find a place to the "left" of those + + // get the handles for the taskbar, task button container, and the notify tray + // + var taskbarHandle = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) { return MorphicResult.ErrorResult(); } + // + var taskButtonContainerHandle = TrayButtonNativeWindow.GetWindowsTaskbarTaskButtonContainerHandle(taskbarHandle); + if (taskButtonContainerHandle == IntPtr.Zero) { return MorphicResult.ErrorResult(); } + // + var notifyTrayHandle = TrayButtonNativeWindow.GetWindowsTaskbarNotificationTrayHandle(taskbarHandle); + if (notifyTrayHandle == IntPtr.Zero) { return MorphicResult.ErrorResult(); } + + // get the RECTs for the taskbar, task button container and the notify tray + // + var getTaskbarRectSuccess = Windows.Win32.PInvoke.GetWindowRect(taskbarHandle, out var taskbarRect); + if (getTaskbarRectSuccess == false) { return MorphicResult.ErrorResult(); } + // + var getTaskButtonContainerRectSuccess = Windows.Win32.PInvoke.GetWindowRect(taskButtonContainerHandle, out var taskButtonContainerRect); + if (getTaskButtonContainerRectSuccess == false) { return MorphicResult.ErrorResult(); } + // + var getNotifyTrayRectSuccess = Windows.Win32.PInvoke.GetWindowRect(notifyTrayHandle, out var notifyTrayRect); + if (getNotifyTrayRectSuccess == false) { return MorphicResult.ErrorResult(); } + + // determine the taskbar's orientation + // + System.Windows.Forms.Orientation taskbarOrientation; + if ((taskbarRect.right - taskbarRect.left) > (taskbarRect.bottom - taskbarRect.top)) + { + taskbarOrientation = System.Windows.Forms.Orientation.Horizontal; + } + else + { + taskbarOrientation = System.Windows.Forms.Orientation.Vertical; + } + + // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) + int trayButtonHeight; + int trayButtonWidth; + // NOTE: on some computers, the taskbar and notify tray return an inaccurate size, but the task button container appears to always return the correct size; therefore we match our primary dimension to the taskbutton container's same dimension + // NOTE: the inaccurate size returned by GetWindowRect may be due to our moving this class from the main application to a helper library (i.e. perhaps the pixel scaling isn't applying correctly), or it could just be a weird quirk on some computers. + // [The GetWindowRect issue hapepns with both our own homebuilt PINVOKE methods as well as with PInvoke.User32.GetWindowRect; the function is returning the correct left, bottom and right positions of the taskbar and notify tray--but is + // sometimes misrepresenting the top (i.e. height) value of both the taskbar and notify tray rects] + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + // option 1: base our primary dimension off of the taskbutton container's same dimension + trayButtonHeight = taskButtonContainerRect.bottom - taskButtonContainerRect.top; + // + // option 2: base our primary dimension off of the taskbar's same dimension + //trayButtonHeight = taskbarRect.bottom - taskbarRect.top; + // + // [and then scale the secondary dimension to 80% of the size of the primary dimension] + trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); + } + else + { + // option 1: base our primary dimension off of the taskbutton container's same dimension + trayButtonWidth = taskButtonContainerRect.right - taskButtonContainerRect.left; + // + // option 2: base our primary dimension off of the taskbar's same dimension + //trayButtonWidth = taskbarRect.right - taskbarRect.left; + // + // [and then scale the secondary dimension to 80% of the size of the primary dimension] + trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); + } + + // choose a space in the rightmost/bottommost position of the taskbar + int trayButtonX; + int trayButtonY; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + trayButtonX = notifyTrayRect.left - trayButtonWidth; + // NOTE: if we have any issues with positioning, try to replace taskbarRect.bottom with taskButtoncontainerRect.bottom (if we chose option #1 for our size calculations above) + trayButtonY = taskbarRect.bottom - trayButtonHeight; + } + else + { + // NOTE: if we have any issues with positioning, try to replace taskbarRect.bottom with taskButtoncontainerRect.right (if we chose option #1 for our size calculations above) + trayButtonX = taskbarRect.right - trayButtonWidth; + trayButtonY = notifyTrayRect.top - trayButtonHeight; + } + + var result = new Windows.Win32.Foundation.RECT() { left = trayButtonX, top = trayButtonY, right = trayButtonX + trayButtonWidth, bottom = trayButtonY + trayButtonHeight }; + return MorphicResult.OkResult(result); + } + + // + + private MorphicResult ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) + { + var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); + var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); + // convert x and y to screen coordinates + var hitPoint = new PInvoke.POINT { x = x, y = y }; + + // NOTE: the instructions for MapWindowPoints instruct us to call SetLastError before calling MapWindowPoints to ensure that we can distinguish a result of 0 from an error if the last win32 error wasn't set (because it wasn't an error) + Marshal.SetLastPInvokeError(0); + // + // NOTE: the PInvoke implementation of MapWindowPoints did not support passing in a POINT struct, so we manually declared the function + var mapWindowPointsResult = PInvokeExtensions.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); + if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != 0) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + return MorphicResult.ErrorResult(); + } + + var result = new System.Drawing.Point(hitPoint.x, hitPoint.y); + return MorphicResult.OkResult(result); + } + + // + + // NOTE: this function takes the window size as input and calculates the size of the icon to display, centered, within the window. + private static System.Drawing.Size CalculateWidthAndHeightForBitmap(Windows.Win32.Foundation.RECT availableRect, System.Drawing.Size bitmapSize) + { + var availableSize = new System.Drawing.Size(availableRect.right - availableRect.left, availableRect.bottom - availableRect.top); + + /* determine the larger dimension (width or height) */ + //int largerDimensionSize; + //int smallerDimensionSize; + System.Drawing.Size insideMarginsSize; + if (availableSize.Height > availableSize.Width) + { + //largerDimensionSize = availableSize.Height; + //smallerDimensionSize = availableSize.Width; + // + // strategy 1: consume up to 90% of the width of the box and up to 66% of the height + //insideMarginsSize = new((int)((double)availableSize.Width * 0.9), (int)((double)availableSize.Height * (2.0 / 3.0))); + // + // strategy 2: consume up to 66% of the width of the box and up to 66% of the height + insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * (2.0 / 3.0))); + } + else + { + //largerDimensionSize = availableSize.Width; + //smallerDimensionSize = availableSize.Height; + // + // strategy 1: consume up to 66% of the width of the box and up to 90% of the height + //insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * 0.9)); + // + // strategy 2: consume up to 66% of the width of the box and up to 66% of the height + insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * (2.0 / 3.0))); + } + + /* shrink the bitmap size down so that it fits inside the available rect */ + + // by default, assume the bitmap will be the size of the source image + int bitmapWidth = bitmapSize.Width; + int bitmapHeight = bitmapSize.Height; + // + // if bitmap is wider than the available rect, shrink it equally in both directions + if (bitmapWidth > insideMarginsSize.Width) + { + double scaleFactor = (double)insideMarginsSize.Width / (double)bitmapWidth; + bitmapWidth = insideMarginsSize.Width; + bitmapHeight = (int)((double)bitmapHeight * scaleFactor); + } + // + // if bitmap is taller than the available rect, shrink it further (and equally in both directions) + if (bitmapHeight > insideMarginsSize.Height) + { + double scaleFactor = (double)insideMarginsSize.Height / (double)bitmapHeight; + bitmapWidth = (int)((double)bitmapWidth * scaleFactor); + bitmapHeight = insideMarginsSize.Height; + } + + // if bitmap does not touch either of the two margins (i.e. is too small), enlarge it now. + if (bitmapWidth != insideMarginsSize.Width && bitmapHeight != insideMarginsSize.Height) + { + // if bitmap is not as wide as the insideMarginsWidth, enlarge it now (equally in both directions) + if (bitmapWidth < insideMarginsSize.Width) + { + double scaleFactor = (double)insideMarginsSize.Width / (double)bitmapWidth; + bitmapWidth = insideMarginsSize.Width; + bitmapHeight = (int)((double)bitmapHeight * scaleFactor); + } + // + // if bitmap is now too tall, shrink it back down (equally in both directions) + if (bitmapHeight > insideMarginsSize.Height) + { + double scaleFactor = (double)insideMarginsSize.Height / (double)bitmapHeight; + bitmapWidth = (int)((double)bitmapWidth * scaleFactor); + bitmapHeight = insideMarginsSize.Height; + } + } + + return new System.Drawing.Size(bitmapWidth, bitmapHeight); + } + + // + + private static Windows.Win32.Foundation.HWND GetWindowsTaskbarHandle() + { + return Windows.Win32.PInvoke.FindWindow("Shell_TrayWnd", null); + } + // + private static Windows.Win32.Foundation.HWND GetWindowsTaskbarTaskButtonContainerHandle(Windows.Win32.Foundation.HWND taskbarHandle) + { + if (taskbarHandle.Value == IntPtr.Zero) + { + return (Windows.Win32.Foundation.HWND)IntPtr.Zero; + } + return Windows.Win32.PInvoke.FindWindowEx(taskbarHandle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, "ReBarWindow32", null); + } + // + private static Windows.Win32.Foundation.HWND GetWindowsTaskbarNotificationTrayHandle(Windows.Win32.Foundation.HWND taskbarHandle) + { + if (taskbarHandle.Value == IntPtr.Zero) + { + return (Windows.Win32.Foundation.HWND)IntPtr.Zero; + } + return Windows.Win32.PInvoke.FindWindowEx(taskbarHandle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, "TrayNotifyWnd", null); + } } diff --git a/Morphic.Controls/WindowsApi.cs b/Morphic.Controls/WindowsApi.cs deleted file mode 100644 index 676dd13d..00000000 --- a/Morphic.Controls/WindowsApi.cs +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace Morphic.Controls; - -internal class WindowsApi -{ - #region common/well-known declarations - - /* well-known HRESULT values */ - - internal static readonly int S_OK = 0x00000000; - - #endregion common/well-known declarations - - - #region commctrl - - internal const string TOOLTIPS_CLASS = "tooltips_class32"; - - internal const ushort TTM_ADDTOOL = WM_USER + 50; - internal const byte TTS_ALWAYSTIP = 0x01; - internal const ushort TTM_DELTOOL = WM_USER + 51; - - // https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-tttoolinfow - internal struct TOOLINFO - { - public uint cbSize; - public uint uFlags; - public IntPtr hwnd; - public UIntPtr uId; - public PInvoke.RECT rect; - public IntPtr hinst; - [MarshalAs(UnmanagedType.LPTStr)] - public string? lpszText; - public IntPtr lParam; - //public IntPtr reserved; // NOTE: this exists in the official declaration as a void pointer but adding it causes SendMessage to fail; pinvoke.net leaves it out and so do we - } - - #endregion commctrl - - - #region windgi - - // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createsolidbrush - [DllImport("gdi32.dll")] - internal static extern IntPtr CreateSolidBrush(uint crColor); - - #endregion wingdi - - - #region winuser - - internal const int CW_USEDEFAULT = unchecked((int)0x80000000); - - internal const nint HWND_TOPMOST = -1; - - internal const uint MK_LBUTTON = 0x0001; - internal const uint MK_RBUTTON = 0x0002; - - internal const ushort WM_USER = 0x0400; - - - public enum WinEventHookType : uint - { - EVENT_AIA_START = 0xA000, - EVENT_AIA_END = 0xAFFF, - EVENT_MIN = 0x00000001, - EVENT_MAX = 0x7FFFFFFF, - EVENT_OBJECT_ACCELERATORCHANGE = 0x8012, - EVENT_OBJECT_CLOAKED = 0x8017, - EVENT_OBJECT_CONTENTSCROLLED = 0x8015, - EVENT_OBJECT_CREATE = 0x8000, - EVENT_OBJECT_DEFACTIONCHANGE = 0x8011, - EVENT_OBJECT_DESCRIPTIONCHANGE = 0x800D, - EVENT_OBJECT_DESTROY = 0x8001, - EVENT_OBJECT_DRAGSTART = 0x8021, - EVENT_OBJECT_DRAGCANCEL = 0x8022, - EVENT_OBJECT_DRAGCOMPLETE = 0x8023, - EVENT_OBJECT_DRAGENTER = 0x8024, - EVENT_OBJECT_DRAGLEAVE = 0x8025, - EVENT_OBJECT_DRAGDROPPED = 0x8026, - EVENT_OBJECT_END = 0x80FF, - EVENT_OBJECT_FOCUS = 0x8005, - EVENT_OBJECT_HELPCHANGE = 0x8010, - EVENT_OBJECT_HIDE = 0x8003, - EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED = 0x8020, - EVENT_OBJECT_IME_HIDE = 0x8028, - EVENT_OBJECT_IME_SHOW = 0x8027, - EVENT_OBJECT_IME_CHANGE = 0x8029, - EVENT_OBJECT_INVOKED = 0x8013, - EVENT_OBJECT_LIVEREGIONCHANGED = 0x8019, - EVENT_OBJECT_LOCATIONCHANGE = 0x800B, - EVENT_OBJECT_NAMECHANGE = 0x800C, - EVENT_OBJECT_PARENTCHANGE = 0x800F, - EVENT_OBJECT_REORDER = 0x8004, - EVENT_OBJECT_SELECTION = 0x8006, - EVENT_OBJECT_SELECTIONADD = 0x8007, - EVENT_OBJECT_SELECTIONREMOVE = 0x8008, - EVENT_OBJECT_SELECTIONWITHIN = 0x8009, - EVENT_OBJECT_SHOW = 0x8002, - EVENT_OBJECT_STATECHANGE = 0x800A, - EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED = 0x8030, - EVENT_OBJECT_TEXTSELECTIONCHANGED = 0x8014, - EVENT_OBJECT_UNCLOAKED = 0x8018, - EVENT_OBJECT_VALUECHANGE = 0x800E, - EVENT_OEM_DEFINED_START = 0x0101, - EVENT_OEM_DEFINED_END = 0x01FF, - EVENT_SYSTEM_ALERT = 0x0002, - EVENT_SYSTEM_ARRANGMENTPREVIEW = 0x8016, - EVENT_SYSTEM_CAPTUREEND = 0x0009, - EVENT_SYSTEM_CAPTURESTART = 0x0008, - EVENT_SYSTEM_CONTEXTHELPEND = 0x000D, - EVENT_SYSTEM_CONTEXTHELPSTART = 0x000C, - EVENT_SYSTEM_DESKTOPSWITCH = 0x0020, - EVENT_SYSTEM_DIALOGEND = 0x0011, - EVENT_SYSTEM_DIALOGSTART = 0x0010, - EVENT_SYSTEM_DRAGDROPEND = 0x000F, - EVENT_SYSTEM_DRAGDROPSTART = 0x000E, - EVENT_SYSTEM_END = 0x00FF, - EVENT_SYSTEM_FOREGROUND = 0x0003, - EVENT_SYSTEM_MENUPOPUPEND = 0x0007, - EVENT_SYSTEM_MENUPOPUPSTART = 0x0006, - EVENT_SYSTEM_MENUEND = 0x0005, - EVENT_SYSTEM_MENUSTART = 0x0004, - EVENT_SYSTEM_MINIMIZEEND = 0x0017, - EVENT_SYSTEM_MINIMIZESTART = 0x0016, - EVENT_SYSTEM_MOVESIZEEND = 0x000B, - EVENT_SYSTEM_MOVESIZESTART = 0x000A, - EVENT_SYSTEM_SCROLLINGEND = 0x0013, - EVENT_SYSTEM_SCROLLINGSTART = 0x0012, - EVENT_SYSTEM_SOUND = 0x0001, - EVENT_SYSTEM_SWITCHEND = 0x0015, - EVENT_SYSTEM_SWITCHSTART = 0x0014, - EVENT_UIA_EVENTID_START = 0x4E00, - EVENT_UIA_EVENTID_END = 0x4EFF, - EVENT_UIA_PROPID_START = 0x7500, - EVENT_UIA_PROPID_END = 0x75FF - } - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook - [Flags] - internal enum WinEventHookFlags : uint - { - WINEVENT_OUTOFCONTEXT = 0x0000, // Events are ASYNC - WINEVENT_SKIPOWNTHREAD = 0x0001, // Don't call back for events on installer's thread - WINEVENT_SKIPOWNPROCESS = 0x0002, // Don't call back for events on installer's process - WINEVENT_INCONTEXT = 0x0004, // Events are SYNC, this causes your dll to be injected into every process - } - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-paintstruct - [StructLayout(LayoutKind.Sequential)] - internal struct PAINTSTRUCT - { - public IntPtr hdc; - public bool fErase; - public PInvoke.RECT rcPaint; - public bool fRestore; - public bool fIncUpdate; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - public byte[] rgbReserved; - } - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent - [Flags] - internal enum TRACKMOUSEEVENTFlags : uint - { - TME_CANCEL = 0x80000000, - TME_HOVER = 0x00000001, - TME_LEAVE = 0x00000002, - TME_NONCLIENT = 0x00000010, - TME_QUERY = 0x40000000 - } - - internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent - [StructLayout(LayoutKind.Sequential)] - internal struct TRACKMOUSEEVENT - { - public uint cbSize; - public TRACKMOUSEEVENTFlags dwFlags; - public IntPtr hWnd; - public uint dwHoverTime; - - public static TRACKMOUSEEVENT CreateNew(TRACKMOUSEEVENTFlags dwFlags, IntPtr hWnd, uint dwHoverTime) - { - var result = new TRACKMOUSEEVENT() - { - cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)), - dwFlags = dwFlags, - hWnd = hWnd, - dwHoverTime = dwHoverTime - }; - return result; - } - } - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WNDCLASSEX - { - public uint cbSize; - public uint style; - public IntPtr lpfnWndProc; - public int cbClsExtra; - public int cbWndExtra; - public IntPtr hInstance; - public IntPtr hIcon; - public IntPtr hCursor; - public IntPtr hbrBackground; - public string lpszMenuName; - public string lpszClassName; - public IntPtr hIconSm; - - public static WNDCLASSEX CreateNew() - { - var result = new WNDCLASSEX() - { - cbSize = (uint)Marshal.SizeOf(typeof(WNDCLASSEX)) - }; - return result; - } - } - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-beginpaint - [DllImport("user32.dll")] - internal static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw - [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern IntPtr CreateWindowEx( - PInvoke.User32.WindowStylesEx dwExStyle, - IntPtr lpClassName, - string? lpWindowName, - PInvoke.User32.WindowStyles dwStyle, - int x, - int y, - int nWidth, - int nHeight, - IntPtr hWndParent, - IntPtr hMenu, - IntPtr hInstance, - IntPtr lpParam - ); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-endpaint - [DllImport("user32.dll")] - internal static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint); - - [DllImport("user32.dll", SetLastError = true)] - internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-fillrect - [DllImport("user32.dll")] - internal static extern int FillRect(IntPtr hDC, [In] ref PInvoke.RECT lprc, IntPtr hbr); - - // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints - // NOTE: this signature is the POINT option (in which cPoints must always be set to 1). - [DllImport("user32.dll", SetLastError = true)] - internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref PInvoke.POINT lpPoints, uint cPoints); - - // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints - // NOTE: this signature is the RECT option (in which cPoints must always be set to 2). - [DllImport("user32.dll", SetLastError = true)] - internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref PInvoke.RECT lpPoints, uint cPoints); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw - [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern ushort RegisterClassEx([In] ref WNDCLASSEX lpWndClass); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook - [DllImport("user32.dll")] - internal static extern IntPtr SetWinEventHook(WinEventHookType eventMin, WinEventHookType eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, WinEventHookFlags dwFlags); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackmouseevent - [DllImport("user32.dll", SetLastError = true)] - internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unhookwinevent - [DllImport("user32.dll")] - internal static extern bool UnhookWinEvent(IntPtr hWinEventHook); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wineventproc - internal delegate void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime); - - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc - internal delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - #endregion winuser -} diff --git a/Morphic.WindowsNative/ExtendedPInvoke.cs b/Morphic.WindowsNative/ExtendedPInvoke.cs index bdab7a40..ebdfcb90 100644 --- a/Morphic.WindowsNative/ExtendedPInvoke.cs +++ b/Morphic.WindowsNative/ExtendedPInvoke.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Raising the Floor - US, Inc. +// Copyright 2020-2024 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. @@ -1018,21 +1018,6 @@ internal enum HighContrastFlags : uint HCF_OPTION_NOTHEMECHANGE = 0x00001000, } - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msllhookstruct - [StructLayout(LayoutKind.Sequential)] - internal struct MSLLHOOKSTRUCT - { - public PInvoke.POINT pt; - // NOTE: the mouseData DWORD is apparently used as a signed integer (rather than as a uint) - public int mouseData; - public uint flags; - public uint time; - public UIntPtr dwExtraInfo; - } - - [DllImport("user32.dll")] - internal static extern bool UnhookWindowsHookEx(IntPtr hhk); - #endregion WinUser.h } diff --git a/Morphic.WindowsNative/IWin32ApiError.cs b/Morphic.WindowsNative/IWin32ApiError.cs new file mode 100644 index 00000000..09f5b04d --- /dev/null +++ b/Morphic.WindowsNative/IWin32ApiError.cs @@ -0,0 +1,31 @@ +// Copyright 2020-2024 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-windowsnative-lib-cs/blob/main/LICENSE +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; + +namespace Morphic.WindowsNative; + +public interface IWin32ApiError +{ + public record Win32Error(uint win32ErrorCode) : IWin32ApiError; +} diff --git a/Morphic.WindowsNative/WindowMessageHooks/MouseWindowMessageHook.cs b/Morphic.WindowsNative/WindowMessageHooks/MouseWindowMessageHook.cs deleted file mode 100644 index b5b82287..00000000 --- a/Morphic.WindowsNative/WindowMessageHooks/MouseWindowMessageHook.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2021-2023 Raising the Floor - US, Inc. -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-windowsnative-lib-cs/blob/main/LICENSE -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace Morphic.WindowsNative.WindowMessageHooks; - -public class MouseWindowMessageHook : IDisposable -{ - PInvoke.User32.WindowsHookDelegate _filterFunction; - PInvoke.User32.SafeHookHandle _hookHandle; - private bool _isDisposed; - - PInvoke.RECT? _trackingRect = null; - - public struct WndProcEventArgs - { - public uint Message; - public int X; - public int Y; - } - public event EventHandler WndProcEvent; - - public MouseWindowMessageHook() - { - // NOTE: we are using a low-level hook in this implementation, and we are monitoring mouse events globally (and then filtering by RECT below) - _filterFunction = new PInvoke.User32.WindowsHookDelegate(this.MessageFilterProc); - _hookHandle = PInvoke.User32.SetWindowsHookEx(PInvoke.User32.WindowsHookType.WH_MOUSE_LL, _filterFunction, IntPtr.Zero, 0 /* global hook */); - } - - public void UpdateTrackingRegion(PInvoke.RECT rect) - { - _trackingRect = rect; - } - - bool _lastMessageWasInTrackingRect = false; - // NOTE: ideally, we would create a queue of messages and then use Task.Run to run code which dequeued the latest messages sequentially - private int MessageFilterProc(int nCode, IntPtr wParam, IntPtr lParam) - { - if (nCode < 0) - { - // per Microsoft's docs: if the code is less than zero, we must pass the message along with _no_ intermediate processing - // see: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644986(v=vs.85) - - // call the next hook in the chain and return its result - return PInvoke.User32.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); - } - - // NOTE: as this is a low-level hook, we must process the message in less than the LowLevelHooksTimeout value (in ms) specified at: - // HKEY_CURRENT_USER\Control Panel\Desktop - // [for this reason and others, we simply capture the events and add them to a thread-safe queue...and then dispatch them to the UI thread's event loop] - - switch (nCode) - { - case 0 /* HC_ACTION */: - // wParam and lParam contain information about a mouse message - { - // NOTE: wParam is one of: { WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP } - // NOTE: lParam is a MSLLHOOKSTRUCT structure instance; see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msllhookstruct - - var mouseEventInfo = Marshal.PtrToStructure(lParam); - - var eventArgs = new WndProcEventArgs() - { - Message = (uint)wParam.ToInt64(), - X = mouseEventInfo.pt.x, - Y = mouseEventInfo.pt.y - }; - - if (_trackingRect is not null) - { - if ((mouseEventInfo.pt.x >= _trackingRect.Value.left) && - (mouseEventInfo.pt.x <= _trackingRect.Value.right) && - (mouseEventInfo.pt.y >= _trackingRect.Value.top) && - (mouseEventInfo.pt.y <= _trackingRect.Value.bottom)) - { - // NOTE: this may not be guaranteed to execute in sequence - Task.Run(() => { WndProcEvent(this, eventArgs); }); - _lastMessageWasInTrackingRect = true; - } - else - { - if (_lastMessageWasInTrackingRect == true) - { - // send a WM_MOUSELEAVE event when the mouse leaves the tracking rect - eventArgs.Message = 0x02A3 /* WM_MOUSELEAVE */; - // - // NOTE: this may not be guaranteed to execute in sequence - Task.Run(() => { WndProcEvent(this, eventArgs); }); - } - - _lastMessageWasInTrackingRect = false; - } - } - else - { - // there is no tracking RECT, so track globally - // - // NOTE: this may not be guaranteed to execute in sequence - Task.Run(() => { WndProcEvent(this, eventArgs); }); - _lastMessageWasInTrackingRect = true; - } - } - // NOTE: we are not "processing" the message, so we will always fall-through and let the next hook in the chain process the message - break; - default: - // unsupported code - break; - } - - // call the next hook in the chain and return its result - return PInvoke.User32.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); - } - - #region IDisposable - protected virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - // dispose any managed objects here - } - - // free unmanaged resources - - // NOTE: this function will return false if it fails - // NOTE: in theory the system should clean up after this hook handle automatically (so we could probably comment out the following two lines of code) - _ = ExtendedPInvoke.UnhookWindowsHookEx(_hookHandle.DangerousGetHandle()); - _hookHandle.SetHandleAsInvalid(); - - // set any large fields to null - - _isDisposed = true; - } - } - - ~MouseWindowMessageHook() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - #endregion IDisposable -}