using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32; using CPF.Drawing; using System.Globalization; using CPF.Controls; using CPF.Input; using CPF.Platform; namespace CPF.Windows { public sealed class NotifyIcon : CpfObject, INotifyIconImpl { private const int WM_TRAYMOUSEMESSAGE = (int)UnmanagedMethods.WindowsMessage.WM_USER + 1024; private static int WM_TASKBARCREATED = UnmanagedMethods.RegisterWindowMessage("TaskbarCreated"); private object syncObj = new object(); IntPtr iconHandle; //private Image icon = null; private string text = ""; private int id = 0; private bool added = false; private NotifyIconNativeWindow window = null; private static int nextId = 0; //private object userData; private bool doubleClick = false; // checks if doubleclick is fired // Visible defaults to false, but the NotifyIconDesigner makes it seem like the default is // true. We do this because while visible is the more common case, if it was a true default, // there would be no way to create a hidden NotifyIcon without being visible for a moment. private bool visible = false; /// /// /// /// Initializes a new instance of the class. /// /// public NotifyIcon() { id = ++nextId; window = new NotifyIconNativeWindow(this); UpdateIcon(visible); } /// /// /// [This event is raised on the NIN_BALLOONUSERCLICK message.] /// public event EventHandler BalloonTipClicked { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// [This event is raised on the NIN_BALLOONTIMEOUT message.] /// public event EventHandler BalloonTipClosed { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// [This event is raised on the NIN_BALLOONSHOW or NIN_BALLOONHIDE message.] /// public event EventHandler BalloonTipShown { add { AddHandler(value); } remove { RemoveHandler(value); } } [PropertyMetadata("")] public string BalloonTipText { get { return GetValue(); } set { SetValue(value); } } public static bool IsEnumValid(Enum enumValue, int value, int minValue, int maxValue) { bool valid = (value >= minValue) && (value <= maxValue); return valid; } /// /// /// /// Gets or sets the BalloonTip icon displayed when /// the mouse hovers over a system tray icon. /// /// public ToolTipIcon BalloonTipIcon { get { return GetValue(); } set { //valid values are 0x0 to 0x3 if (!IsEnumValid(value, (int)value, (int)ToolTipIcon.None, (int)ToolTipIcon.Error)) { throw new InvalidEnumArgumentException("value", (int)value, typeof(ToolTipIcon)); } SetValue(value); } } /// /// /// /// Gets or sets the BalloonTip title displayed when /// the mouse hovers over a system tray icon. /// /// [PropertyMetadata("")] public string BalloonTipTitle { get { return GetValue(); } set { SetValue(value); } } /// /// /// /// Gets or sets context menu /// for the tray icon. /// /// public ContextMenu ContextMenu { get { return GetValue(); } set { SetValue(value); } } /// /// /// /// Gets or sets the current /// icon. /// /// public Image Icon { get { return GetValue(); } set { SetValue(value); } } [PropertyChanged(nameof(Icon))] void OnIcon(object newValue, object oldValue, CPF.PropertyMetadataAttribute attribute) { UpdateIcon(visible); } /// /// /// /// Gets or sets the ToolTip text displayed when /// the mouse hovers over a system tray icon. /// /// public string Text { get { return text; } set { if (value == null) value = ""; if (value != null && !value.Equals(this.text)) { if (value != null && value.Length > 63) { throw new ArgumentOutOfRangeException("Text", value, "文字太长,不能超过63个字符"); } this.text = value; if (added) { UpdateIcon(true); } } } } /// /// /// /// Gets or sets a value indicating whether the icon is visible in the Windows System Tray. /// /// public bool Visible { get { return visible; } set { if (visible != value) { UpdateIcon(value); visible = value; } } } /// /// /// Occurs when the user clicks the icon in the system tray. /// public event EventHandler Click { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// Occurs when the user double-clicks the icon in the system tray. /// public event EventHandler DoubleClick { add { AddHandler(value); } remove { RemoveHandler(value); } } public event EventHandler MouseClick { add { AddHandler(value); } remove { RemoveHandler(value); } } public event EventHandler MouseDoubleClick { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// /// Occurs when the /// user presses a mouse button while the pointer is over the icon in the system tray. /// /// public event EventHandler MouseDown { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// /// Occurs /// when the user moves the mouse pointer over the icon in the system tray. /// /// public event EventHandler MouseMove { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// /// Occurs when the /// user releases the mouse button while the pointer /// is over the icon in the system tray. /// /// public event EventHandler MouseUp { add { AddHandler(value); } remove { RemoveHandler(value); } } /// /// /// /// Disposes of the resources (other than memory) used by the /// . /// /// protected override void Dispose(bool disposing) { if (disposing) { if (window != null) { this.Icon = null; this.Text = String.Empty; UpdateIcon(false); window.DestroyHandle(); window = null; ContextMenu = null; if (iconHandle != IntPtr.Zero) { UnmanagedMethods.DestroyIcon(iconHandle); } iconHandle = IntPtr.Zero; } } //else //{ // // This same post is done in ControlNativeWindow's finalize method, so if you change // // it, change it there too. // // // if (window != null && window.Handle != IntPtr.Zero) // { // UnmanagedMethods.PostMessage(new HandleRef(window, window.Handle), UnmanagedMethods.WindowsMessage.WM_CLOSE, 0, 0); // window.ReleaseHandle(); // } //} base.Dispose(disposing); } private void OnBalloonTipClicked() { this.RaiseEvent(EventArgs.Empty, nameof(BalloonTipClicked)); } /// /// /// /// This method raised the BalloonTipClosed event. /// /// private void OnBalloonTipClosed() { this.RaiseEvent(EventArgs.Empty, nameof(BalloonTipClosed)); } /// /// /// /// This method raised the BalloonTipShown event. /// /// private void OnBalloonTipShown() { this.RaiseEvent(EventArgs.Empty, nameof(BalloonTipShown)); } /// /// /// /// This method actually raises the Click event. Inheriting classes should /// override this if they wish to be notified of a Click event. (This is far /// preferable to actually adding an event handler.) They should not, /// however, forget to call base.onClick(e); before exiting, to ensure that /// other recipients do actually get the event. /// /// private void OnClick(EventArgs e) { RaiseEvent(e, nameof(Click)); } /// /// /// Inheriting classes should override this method to handle this event. /// Call base.onDoubleClick to send this event to any registered event listeners. /// private void OnDoubleClick(EventArgs e) { RaiseEvent(e, nameof(DoubleClick)); } /// /// /// Inheriting classes should override this method to handle this event. /// Call base.OnMouseClick to send this event to any registered event listeners. /// private void OnMouseClick(NotifyIconMouseEventArgs mea) { RaiseEvent(mea, nameof(MouseClick)); } /// /// /// Inheriting classes should override this method to handle this event. /// Call base.OnMouseDoubleClick to send this event to any registered event listeners. /// private void OnMouseDoubleClick(NotifyIconMouseEventArgs mea) { RaiseEvent(mea, nameof(MouseDoubleClick)); } /// /// /// /// Raises the event. /// Inheriting classes should override this method to handle this event. /// Call base.onMouseDown to send this event to any registered event listeners. /// /// /// private void OnMouseDown(NotifyIconMouseEventArgs e) { RaiseEvent(new Input.NotifyIconMouseEventArgs(e.Button), nameof(MouseDown)); } /// /// /// /// Inheriting classes should override this method to handle this event. /// Call base.onMouseMove to send this event to any registered event listeners. /// /// /// private void OnMouseMove(NotifyIconMouseEventArgs e) { RaiseEvent(e, nameof(MouseMove)); } /// /// /// /// Inheriting classes should override this method to handle this event. /// Call base.onMouseUp to send this event to any registered event listeners. /// /// private void OnMouseUp(NotifyIconMouseEventArgs e) { RaiseEvent(new Input.NotifyIconMouseEventArgs(e.Button), nameof(MouseUp)); } /// /// /// /// Displays a balloon tooltip in the taskbar. /// /// The system enforces minimum and maximum timeout values. Timeout /// values that are too large are set to the maximum value and values /// that are too small default to the minimum value. The operating system's /// default minimum and maximum timeout values are 10 seconds and 30 seconds, /// respectively. /// /// No more than one balloon ToolTip at at time is displayed for the taskbar. /// If an application attempts to display a ToolTip when one is already being displayed, /// the ToolTip will not appear until the existing balloon ToolTip has been visible for at /// least the system minimum timeout value. For example, a balloon ToolTip with timeout /// set to 30 seconds has been visible for seven seconds when another application attempts /// to display a balloon ToolTip. If the system minimum timeout is ten seconds, the first /// ToolTip displays for an additional three seconds before being replaced by the second ToolTip. /// /// public void ShowBalloonTip(int timeout) { ShowBalloonTip(timeout, this.BalloonTipTitle, this.BalloonTipText, this.BalloonTipIcon); } /// /// /// /// Displays a balloon tooltip in the taskbar with the specified title, /// text, and icon for a duration of the specified timeout value. /// /// The system enforces minimum and maximum timeout values. Timeout /// values that are too large are set to the maximum value and values /// that are too small default to the minimum value. The operating system's /// default minimum and maximum timeout values are 10 seconds and 30 seconds, /// respectively. /// /// No more than one balloon ToolTip at at time is displayed for the taskbar. /// If an application attempts to display a ToolTip when one is already being displayed, /// the ToolTip will not appear until the existing balloon ToolTip has been visible for at /// least the system minimum timeout value. For example, a balloon ToolTip with timeout /// set to 30 seconds has been visible for seven seconds when another application attempts /// to display a balloon ToolTip. If the system minimum timeout is ten seconds, the first /// ToolTip displays for an additional three seconds before being replaced by the second ToolTip. /// /// public void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon tipIcon) { if (timeout < 0) { throw new ArgumentOutOfRangeException("timeout", "ShowBalloonTip超时" + timeout); } if (string.IsNullOrEmpty(tipText)) { throw new ArgumentException("tipText不能为空"); } //valid values are 0x0 to 0x3 if (!IsEnumValid(tipIcon, (int)tipIcon, (int)ToolTipIcon.None, (int)ToolTipIcon.Error)) { throw new InvalidEnumArgumentException("tipIcon", (int)tipIcon, typeof(ToolTipIcon)); } if (added) { // Bail if in design mode... //if (DesignMode) //{ // return; //} //IntSecurity.UnrestrictedWindows.Demand(); UnmanagedMethods.NOTIFYICONDATA data = new UnmanagedMethods.NOTIFYICONDATA(); if (window.Handle == IntPtr.Zero) { window.CreateHandle(new CreateParams()); } data.hWnd = window.Handle; data.uID = id; data.uFlags = UnmanagedMethods.NIF_INFO; data.uTimeoutOrVersion = timeout; data.szInfoTitle = tipTitle; data.szInfo = tipText; switch (tipIcon) { case ToolTipIcon.Info: data.dwInfoFlags = UnmanagedMethods.NIIF_INFO; break; case ToolTipIcon.Warning: data.dwInfoFlags = UnmanagedMethods.NIIF_WARNING; break; case ToolTipIcon.Error: data.dwInfoFlags = UnmanagedMethods.NIIF_ERROR; break; case ToolTipIcon.None: data.dwInfoFlags = UnmanagedMethods.NIIF_NONE; break; } UnmanagedMethods.Shell_NotifyIcon(UnmanagedMethods.NIM_MODIFY, data); } } /// /// /// Shows the context menu for the tray icon. /// /// private void ShowContextMenu() { UnmanagedMethods.SetForegroundWindow(window.Handle); var contextMenu = ContextMenu; if (contextMenu != null) { //UnmanagedMethods.POINT pt; //UnmanagedMethods.GetCursorPos(out pt); // VS7 #38994 // The solution to this problem was found in MSDN Article ID: Q135788. // Summary: the current window must be made the foreground window // before calling TrackPopupMenuEx, and a task switch must be // forced after the call. if (contextMenu != null) { contextMenu.Placement = PlacementMode.Mouse; contextMenu.PopupMarginBottm = 1; contextMenu.PopupMarginTop = "auto"; contextMenu.IsOpen = true; //contextMenu.OnPopup(EventArgs.Empty); //SafeNativeMethods.TrackPopupMenuEx(new HandleRef(contextMenu, contextMenu.Handle), // UnmanagedMethods.TPM_VERTICAL | UnmanagedMethods.TPM_RIGHTALIGN, // pt.x, // pt.y, // new HandleRef(window, window.Handle), // null); //// Force task switch (see above) //UnmanagedMethods.PostMessage(new HandleRef(window, window.Handle), UnmanagedMethods.WindowsMessage.WM_NULL, IntPtr.Zero, IntPtr.Zero); } } } /// /// /// Updates the icon in the system tray. /// /// private void UpdateIcon(bool showIconInTray) { lock (syncObj) { // Bail if in design mode... // //if (DesignMode) //{ // return; //} //IntSecurity.UnrestrictedWindows.Demand(); window.LockReference(showIconInTray); UnmanagedMethods.NOTIFYICONDATA data = new UnmanagedMethods.NOTIFYICONDATA(); data.uCallbackMessage = WM_TRAYMOUSEMESSAGE; data.uFlags = UnmanagedMethods.NIF_MESSAGE; if (showIconInTray) { if (window.Handle == IntPtr.Zero) { window.CreateHandle(new CreateParams()); } } data.hWnd = window.Handle; data.uID = id; data.hIcon = IntPtr.Zero; data.szTip = null; if (iconHandle != IntPtr.Zero) { UnmanagedMethods.DestroyIcon(iconHandle); } iconHandle = IntPtr.Zero; if (Icon != null) { data.uFlags |= UnmanagedMethods.NIF_ICON; var stream = Icon.SaveToStream(ImageFormat.Png); var states = UnmanagedMethods.GdipCreateBitmapFromStream(new GPStream(stream), out IntPtr bitmap); stream.Dispose(); UnmanagedMethods.GdipCreateHICONFromBitmap(bitmap, out IntPtr hIcon); UnmanagedMethods.GdipDisposeImage(bitmap); iconHandle = hIcon; data.hIcon = iconHandle; } data.uFlags |= UnmanagedMethods.NIF_TIP; data.szTip = text; if (showIconInTray && Icon != null) { if (!added) { UnmanagedMethods.Shell_NotifyIcon(UnmanagedMethods.NIM_ADD, data); added = true; } else { UnmanagedMethods.Shell_NotifyIcon(UnmanagedMethods.NIM_MODIFY, data); } } else if (added) { UnmanagedMethods.Shell_NotifyIcon(UnmanagedMethods.NIM_DELETE, data); added = false; } } } /// /// /// Handles the mouse-down event /// /// private void WmMouseDown(ref Message m, MouseButton button, int clicks) { if (clicks == 2) { OnDoubleClick(new NotifyIconMouseEventArgs(button, 2)); OnMouseDoubleClick(new NotifyIconMouseEventArgs(button, 2)); doubleClick = true; } OnMouseDown(new NotifyIconMouseEventArgs(button, clicks)); } /// /// /// Handles the mouse-move event /// /// private void WmMouseMove(ref Message m) { OnMouseMove(new NotifyIconMouseEventArgs(MouseButton.None, 0)); } /// /// /// Handles the mouse-up event /// /// private void WmMouseUp(ref Message m, MouseButton button) { OnMouseUp(new NotifyIconMouseEventArgs(button, 0)); //subhag if (!doubleClick) { OnClick(new NotifyIconMouseEventArgs(button, 0)); OnMouseClick(new NotifyIconMouseEventArgs(button, 0)); } doubleClick = false; } private void WmTaskbarCreated(ref Message m) { added = false; UpdateIcon(visible); } private void WndProc(ref Message msg) { switch ((UnmanagedMethods.WindowsMessage)msg.Msg) { case (UnmanagedMethods.WindowsMessage)WM_TRAYMOUSEMESSAGE: switch ((UnmanagedMethods.WindowsMessage)msg.LParam) { case UnmanagedMethods.WindowsMessage.WM_LBUTTONDBLCLK: WmMouseDown(ref msg, MouseButton.Left, 2); break; case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: WmMouseDown(ref msg, MouseButton.Left, 1); break; case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP: WmMouseUp(ref msg, MouseButton.Left); break; case UnmanagedMethods.WindowsMessage.WM_MBUTTONDBLCLK: WmMouseDown(ref msg, MouseButton.Middle, 2); break; case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: WmMouseDown(ref msg, MouseButton.Middle, 1); break; case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP: WmMouseUp(ref msg, MouseButton.Middle); break; case UnmanagedMethods.WindowsMessage.WM_MOUSEMOVE: WmMouseMove(ref msg); break; case UnmanagedMethods.WindowsMessage.WM_RBUTTONDBLCLK: WmMouseDown(ref msg, MouseButton.Right, 2); break; case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: WmMouseDown(ref msg, MouseButton.Right, 1); break; case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP: //if (ContextMenu != null) { ShowContextMenu(); } WmMouseUp(ref msg, MouseButton.Right); break; case (UnmanagedMethods.WindowsMessage)UnmanagedMethods.NIN_BALLOONSHOW: OnBalloonTipShown(); break; case (UnmanagedMethods.WindowsMessage)UnmanagedMethods.NIN_BALLOONHIDE: OnBalloonTipClosed(); break; case (UnmanagedMethods.WindowsMessage)UnmanagedMethods.NIN_BALLOONTIMEOUT: OnBalloonTipClosed(); break; case (UnmanagedMethods.WindowsMessage)UnmanagedMethods.NIN_BALLOONUSERCLICK: OnBalloonTipClicked(); break; } break; case UnmanagedMethods.WindowsMessage.WM_COMMAND: if (IntPtr.Zero == msg.LParam) { //if (Command.DispatchID((int)msg.WParam & 0xFFFF)) return; } else { window.DefWndProc(ref msg); } break; case UnmanagedMethods.WindowsMessage.WM_DRAWITEM: // If the wparam is zero, then the message was sent by a menu. // See WM_DRAWITEM in MSDN. if (msg.WParam == IntPtr.Zero) { //WmDrawItemMenuItem(ref msg); } break; case UnmanagedMethods.WindowsMessage.WM_MEASUREITEM: // If the wparam is zero, then the message was sent by a menu. if (msg.WParam == IntPtr.Zero) { //WmMeasureMenuItem(ref msg); } break; case UnmanagedMethods.WindowsMessage.WM_INITMENUPOPUP: WmInitMenuPopup(ref msg); break; case UnmanagedMethods.WindowsMessage.WM_DESTROY: // Remove the icon from the taskbar UpdateIcon(false); break; default: if (msg.Msg == WM_TASKBARCREATED) { WmTaskbarCreated(ref msg); } window.DefWndProc(ref msg); break; } } private void WmInitMenuPopup(ref Message m) { //if (contextMenu != null) //{ // if (contextMenu.ProcessInitMenuPopup(m.WParam)) // { // return; // } //} window.DefWndProc(ref m); } //private void WmMeasureMenuItem(ref Message m) //{ // // Obtain the menu item object // UnmanagedMethods.MEASUREITEMSTRUCT mis = (UnmanagedMethods.MEASUREITEMSTRUCT)m.GetLParam(typeof(UnmanagedMethods.MEASUREITEMSTRUCT)); // Debug.Assert(m.LParam != IntPtr.Zero, "m.lparam is null"); // // A pointer to the correct MenuItem is stored in the measure item // // information sent with the message. // // (See MenuItem.CreateMenuItemInfo) // MenuItem menuItem = MenuItem.GetMenuItemFromItemData(mis.itemData); // Debug.Assert(menuItem != null, "UniqueID is not associated with a menu item"); // // Delegate this message to the menu item // if (menuItem != null) // { // menuItem.WmMeasureItem(ref m); // } //} //private void WmDrawItemMenuItem(ref Message m) //{ // // Obtain the menu item object // UnmanagedMethods.DRAWITEMSTRUCT dis = (UnmanagedMethods.DRAWITEMSTRUCT)m.GetLParam(typeof(UnmanagedMethods.DRAWITEMSTRUCT)); // // A pointer to the correct MenuItem is stored in the draw item // // information sent with the message. // // (See MenuItem.CreateMenuItemInfo) // MenuItem menuItem = MenuItem.GetMenuItemFromItemData(dis.itemData); // // Delegate this message to the menu item // if (menuItem != null) // { // menuItem.WmDrawItem(ref m); // } //} /// /// /// Defines a placeholder window that the NotifyIcon is attached to. /// /// private class NotifyIconNativeWindow : NativeWindow { internal NotifyIcon reference; private GCHandle rootRef; // We will root the control when we do not want to be elligible for garbage collection. /// /// /// Create a new NotifyIcon, and bind the window to the NotifyIcon component. /// /// internal NotifyIconNativeWindow(NotifyIcon component) { reference = component; } ~NotifyIconNativeWindow() { // This same post is done in Control's Dispose method, so if you change // it, change it there too. // if (Handle != IntPtr.Zero) { UnmanagedMethods.PostMessage(Handle, (uint)UnmanagedMethods.WindowsMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); } // This releases the handle from our window proc, re-routing it back to // the system. } public void LockReference(bool locked) { if (locked) { if (!rootRef.IsAllocated) { rootRef = GCHandle.Alloc(reference, GCHandleType.Normal); } } else { if (rootRef.IsAllocated) { rootRef.Free(); } } } /// /// /// Pass messages on to the NotifyIcon object's wndproc handler. /// /// protected override void WndProc(ref Message m) { Debug.Assert(reference != null, "NotifyIcon was garbage collected while it was still visible. How did we let that happen?"); reference.WndProc(ref m); } } } /// public enum ToolTipIcon : byte { /// /// /// /// No Icon. /// /// None = 0, /// /// /// /// A Information Icon. /// /// Info = 1, /// /// /// /// A Warning Icon. /// /// Warning = 2, /// /// /// /// A Error Icon. /// /// Error = 3 } public class NotifyIconMouseEventArgs : EventArgs { public NotifyIconMouseEventArgs(MouseButton button, int clicks) { Button = button; Clicks = clicks; } public MouseButton Button { get; set; } public int Clicks { get; set; } } }