// hardcodet.net NotifyIcon for WPF // Copyright (c) 2009 - 2013 Philipp Sumi // Contact and Information: http://www.hardcodet.net // // This library is free software; you can redistribute it and/or // modify it under the terms of the Code Project Open License (CPOL); // either version 1.0 of the License, or (at your option) any later // version. // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE using System; using System.ComponentModel; using System.Diagnostics; namespace MangaReader.Avalonia.Platform.Win.Interop { /// /// Receives messages from the taskbar icon through /// window messages of an underlying helper window. /// public class WindowMessageSink : IDisposable { #region members /// /// The ID of messages that are received from the the /// taskbar icon. /// public const int CallbackMessageId = 0x400; /// /// The ID of the message that is being received if the /// taskbar is (re)started. /// private uint taskbarRestartMessageId; /// /// Used to track whether a mouse-up event is just /// the aftermath of a double-click and therefore needs /// to be suppressed. /// private bool isDoubleClick; /// /// A delegate that processes messages of the hidden /// native window that receives window messages. Storing /// this reference makes sure we don't loose our reference /// to the message window. /// private WindowProcedureHandler messageHandler; /// /// Window class ID. /// internal string WindowId { get; private set; } /// /// Handle for the message window. /// internal IntPtr MessageWindowHandle { get; private set; } /// /// The version of the underlying icon. Defines how /// incoming messages are interpreted. /// public NotifyIconVersion Version { get; set; } #endregion #region events /// /// The custom tooltip should be closed or hidden. /// public event Action ChangeToolTipStateRequest; /// /// Fired in case the user clicked or moved within /// the taskbar icon area. /// public event Action MouseEventReceived; /// /// Fired if a balloon ToolTip was either displayed /// or closed (indicated by the boolean flag). /// public event Action BalloonToolTipChanged; /// /// Fired if the taskbar was created or restarted. Requires the taskbar /// icon to be reset. /// public event Action TaskbarCreated; #endregion #region construction /// /// Creates a new message sink that receives message from /// a given taskbar icon. /// /// public WindowMessageSink(NotifyIconVersion version) { Version = version; CreateMessageWindow(); } #endregion #region CreateMessageWindow /// /// Creates the helper message window that is used /// to receive messages from the taskbar icon. /// private void CreateMessageWindow() { //generate a unique ID for the window WindowId = "WPFTaskbarIcon_" + Guid.NewGuid(); //register window message handler messageHandler = OnWindowMessageReceived; // Create a simple window class which is reference through //the messageHandler delegate WindowClass wc; wc.style = 0; wc.lpfnWndProc = messageHandler; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = IntPtr.Zero; wc.hIcon = IntPtr.Zero; wc.hCursor = IntPtr.Zero; wc.hbrBackground = IntPtr.Zero; wc.lpszMenuName = string.Empty; wc.lpszClassName = WindowId; // Register the window class WinApi.RegisterClass(ref wc); // Get the message used to indicate the taskbar has been restarted // This is used to re-add icons when the taskbar restarts taskbarRestartMessageId = WinApi.RegisterWindowMessage("TaskbarCreated"); // Create the message window MessageWindowHandle = WinApi.CreateWindowEx(0, WindowId, "", 0, 0, 0, 1, 1, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (MessageWindowHandle == IntPtr.Zero) { throw new Win32Exception("Message window handle was not a valid pointer"); } } #endregion #region Handle Window Messages /// /// Callback method that receives messages from the taskbar area. /// private IntPtr OnWindowMessageReceived(IntPtr hWnd, uint messageId, IntPtr wParam, IntPtr lParam) { if (messageId == taskbarRestartMessageId) { //recreate the icon if the taskbar was restarted (e.g. due to Win Explorer shutdown) var listener = TaskbarCreated; listener?.Invoke(); } //forward message ProcessWindowMessage(messageId, wParam, lParam); // Pass the message to the default window procedure return WinApi.DefWindowProc(hWnd, messageId, wParam, lParam); } /// /// Processes incoming system messages. /// /// Callback ID. /// If the version is /// or higher, this parameter can be used to resolve mouse coordinates. /// Currently not in use. /// Provides information about the event. private void ProcessWindowMessage(uint msg, IntPtr wParam, IntPtr lParam) { if (msg != CallbackMessageId) return; var message = (WindowsMessages) lParam.ToInt32(); switch (message) { case WindowsMessages.WM_CONTEXTMENU: // TODO: Handle WM_CONTEXTMENU, see https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw Debug.WriteLine("Unhandled WM_CONTEXTMENU"); break; case WindowsMessages.WM_MOUSEMOVE: MouseEventReceived?.Invoke(MouseEvent.MouseMove); break; case WindowsMessages.WM_LBUTTONDOWN: MouseEventReceived?.Invoke(MouseEvent.IconLeftMouseDown); break; case WindowsMessages.WM_LBUTTONUP: if (!isDoubleClick) { MouseEventReceived?.Invoke(MouseEvent.IconLeftMouseUp); } isDoubleClick = false; break; case WindowsMessages.WM_LBUTTONDBLCLK: isDoubleClick = true; MouseEventReceived?.Invoke(MouseEvent.IconDoubleClick); break; case WindowsMessages.WM_RBUTTONDOWN: MouseEventReceived?.Invoke(MouseEvent.IconRightMouseDown); break; case WindowsMessages.WM_RBUTTONUP: MouseEventReceived?.Invoke(MouseEvent.IconRightMouseUp); break; case WindowsMessages.WM_RBUTTONDBLCLK: //double click with right mouse button - do not trigger event break; case WindowsMessages.WM_MBUTTONDOWN: MouseEventReceived?.Invoke(MouseEvent.IconMiddleMouseDown); break; case WindowsMessages.WM_MBUTTONUP: MouseEventReceived?.Invoke(MouseEvent.IconMiddleMouseUp); break; case WindowsMessages.WM_MBUTTONDBLCLK: //double click with middle mouse button - do not trigger event break; case WindowsMessages.NIN_BALLOONSHOW: BalloonToolTipChanged?.Invoke(true); break; case WindowsMessages.NIN_BALLOONHIDE: case WindowsMessages.NIN_BALLOONTIMEOUT: BalloonToolTipChanged?.Invoke(false); break; case WindowsMessages.NIN_BALLOONUSERCLICK: MouseEventReceived?.Invoke(MouseEvent.BalloonToolTipClicked); break; case WindowsMessages.NIN_POPUPOPEN: ChangeToolTipStateRequest?.Invoke(true); break; case WindowsMessages.NIN_POPUPCLOSE: ChangeToolTipStateRequest?.Invoke(false); break; case WindowsMessages.NIN_SELECT: // TODO: Handle NIN_SELECT see https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw Debug.WriteLine("Unhandled NIN_SELECT"); break; case WindowsMessages.NIN_KEYSELECT: // TODO: Handle NIN_KEYSELECT see https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw Debug.WriteLine("Unhandled NIN_KEYSELECT"); break; default: Debug.WriteLine("Unhandled NotifyIcon message ID: " + lParam); break; } } #endregion #region Dispose /// /// Set to true as soon as Dispose has been invoked. /// public bool IsDisposed { get; private set; } /// /// Disposes the object. /// /// This method is not virtual by design. Derived classes /// should override . /// public void Dispose() { Dispose(true); // This object will be cleaned up by the Dispose method. // Therefore, you should call GC.SuppressFinalize to // take this object off the finalization queue // and prevent finalization code for this object // from executing a second time. GC.SuppressFinalize(this); } /// /// This destructor will run only if the /// method does not get called. This gives this base class the /// opportunity to finalize. /// /// Important: Do not provide destructor in types derived from /// this class. /// /// ~WindowMessageSink() { Dispose(false); } /// /// Removes the windows hook that receives window /// messages and closes the underlying helper window. /// private void Dispose(bool disposing) { //don't do anything if the component is already disposed if (IsDisposed) return; IsDisposed = true; //always destroy the unmanaged handle (even if called from the GC) WinApi.DestroyWindow(MessageWindowHandle); messageHandler = null; } #endregion } }