// 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 { /// <summary> /// Receives messages from the taskbar icon through /// window messages of an underlying helper window. /// </summary> public class WindowMessageSink : IDisposable { #region members /// <summary> /// The ID of messages that are received from the the /// taskbar icon. /// </summary> public const int CallbackMessageId = 0x400; /// <summary> /// The ID of the message that is being received if the /// taskbar is (re)started. /// </summary> private uint taskbarRestartMessageId; /// <summary> /// Used to track whether a mouse-up event is just /// the aftermath of a double-click and therefore needs /// to be suppressed. /// </summary> private bool isDoubleClick; /// <summary> /// 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. /// </summary> private WindowProcedureHandler messageHandler; /// <summary> /// Window class ID. /// </summary> internal string WindowId { get; private set; } /// <summary> /// Handle for the message window. /// </summary> internal IntPtr MessageWindowHandle { get; private set; } /// <summary> /// The version of the underlying icon. Defines how /// incoming messages are interpreted. /// </summary> public NotifyIconVersion Version { get; set; } #endregion #region events /// <summary> /// The custom tooltip should be closed or hidden. /// </summary> public event Action<bool> ChangeToolTipStateRequest; /// <summary> /// Fired in case the user clicked or moved within /// the taskbar icon area. /// </summary> public event Action<MouseEvent> MouseEventReceived; /// <summary> /// Fired if a balloon ToolTip was either displayed /// or closed (indicated by the boolean flag). /// </summary> public event Action<bool> BalloonToolTipChanged; /// <summary> /// Fired if the taskbar was created or restarted. Requires the taskbar /// icon to be reset. /// </summary> public event Action TaskbarCreated; #endregion #region construction /// <summary> /// Creates a new message sink that receives message from /// a given taskbar icon. /// </summary> /// <param name="version"></param> public WindowMessageSink(NotifyIconVersion version) { Version = version; CreateMessageWindow(); } #endregion #region CreateMessageWindow /// <summary> /// Creates the helper message window that is used /// to receive messages from the taskbar icon. /// </summary> 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 /// <summary> /// Callback method that receives messages from the taskbar area. /// </summary> 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); } /// <summary> /// Processes incoming system messages. /// </summary> /// <param name="msg">Callback ID.</param> /// <param name="wParam">If the version is <see cref="NotifyIconVersion.Vista"/> /// or higher, this parameter can be used to resolve mouse coordinates. /// Currently not in use.</param> /// <param name="lParam">Provides information about the event.</param> 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 /// <summary> /// Set to true as soon as <c>Dispose</c> has been invoked. /// </summary> public bool IsDisposed { get; private set; } /// <summary> /// Disposes the object. /// </summary> /// <remarks>This method is not virtual by design. Derived classes /// should override <see cref="Dispose(bool)"/>. /// </remarks> 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); } /// <summary> /// This destructor will run only if the <see cref="Dispose()"/> /// method does not get called. This gives this base class the /// opportunity to finalize. /// <para> /// Important: Do not provide destructor in types derived from /// this class. /// </para> /// </summary> ~WindowMessageSink() { Dispose(false); } /// <summary> /// Removes the windows hook that receives window /// messages and closes the underlying helper window. /// </summary> 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 } }