|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Drawing;
namespace System.Windows.Forms;
/// <summary>
/// Specifies a component that creates an icon in the Windows System Tray. This class cannot be inherited.
/// </summary>
[DefaultProperty(nameof(Text))]
[DefaultEvent(nameof(MouseDoubleClick))]
[Designer($"System.Windows.Forms.Design.NotifyIconDesigner, {AssemblyRef.SystemDesign}")]
[ToolboxItemFilter("System.Windows.Forms")]
[SRDescription(nameof(SR.DescriptionNotifyIcon))]
public sealed partial class NotifyIcon : Component
{
internal const int MaxTextSize = 127;
private static readonly object s_mouseDownEvent = new();
private static readonly object s_mouseMoveEvent = new();
private static readonly object s_mouseUpEvent = new();
private static readonly object s_clickEvent = new();
private static readonly object s_doubleClickEvent = new();
private static readonly object s_mouseClickEvent = new();
private static readonly object s_mouseDoubleClickEvent = new();
private static readonly object s_balloonTipShownEvent = new();
private static readonly object s_balloonTipClickedEvent = new();
private static readonly object s_balloonTipClosedEvent = new();
private const int WM_TRAYMOUSEMESSAGE = (int)PInvokeCore.WM_USER + 1024;
private static uint WM_TASKBARCREATED { get; } = PInvoke.RegisterWindowMessage("TaskbarCreated");
private readonly Lock _lock = new();
private Icon? _icon;
private string _text = string.Empty;
private readonly uint _id;
private bool _added;
private NotifyIconNativeWindow _window;
private ContextMenuStrip? _contextMenuStrip;
private ToolTipIcon _balloonTipIcon;
private string _balloonTipText = string.Empty;
private string _balloonTipTitle = string.Empty;
private static uint s_nextId;
private object? _userData;
private bool _doubleClick; // 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;
/// <summary>
/// Initializes a new instance of the <see cref="NotifyIcon"/> class.
/// </summary>
public NotifyIcon()
{
_id = ++s_nextId;
_window = new NotifyIconNativeWindow(this);
UpdateIcon(_visible);
}
/// <summary>
/// Initializes a new instance of the <see cref="NotifyIcon"/> class.
/// </summary>
public NotifyIcon(IContainer container) : this()
{
ArgumentNullException.ThrowIfNull(container);
container.Add(this);
}
/// <summary>
/// Gets or sets the BalloonTip text displayed when
/// the mouse hovers over a system tray icon.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[DefaultValue("")]
[SRDescription(nameof(SR.NotifyIconBalloonTipTextDescr))]
[Editor($"System.ComponentModel.Design.MultilineStringEditor, {AssemblyRef.SystemDesign}", typeof(Drawing.Design.UITypeEditor))]
public string BalloonTipText
{
get
{
return _balloonTipText;
}
set
{
if (value != _balloonTipText)
{
_balloonTipText = value;
}
}
}
/// <summary>
/// Gets or sets the BalloonTip icon displayed when
/// the mouse hovers over a system tray icon.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(ToolTipIcon.None)]
[SRDescription(nameof(SR.NotifyIconBalloonTipIconDescr))]
public ToolTipIcon BalloonTipIcon
{
get
{
return _balloonTipIcon;
}
set
{
// valid values are 0x0 to 0x3
SourceGenerated.EnumValidator.Validate(value);
if (value != _balloonTipIcon)
{
_balloonTipIcon = value;
}
}
}
/// <summary>
/// Gets or sets the BalloonTip title displayed when
/// the mouse hovers over a system tray icon.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[DefaultValue("")]
[SRDescription(nameof(SR.NotifyIconBalloonTipTitleDescr))]
public string BalloonTipTitle
{
get
{
return _balloonTipTitle;
}
set
{
if (value != _balloonTipTitle)
{
_balloonTipTitle = value;
}
}
}
/// <summary>
/// [This event is raised on the NIN_BALLOONUSERCLICK message.]
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.NotifyIconOnBalloonTipClickedDescr))]
public event EventHandler? BalloonTipClicked
{
add => Events.AddHandler(s_balloonTipClickedEvent, value);
remove => Events.RemoveHandler(s_balloonTipClickedEvent, value);
}
/// <summary>
/// [This event is raised on the NIN_BALLOONTIMEOUT message.]
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.NotifyIconOnBalloonTipClosedDescr))]
public event EventHandler? BalloonTipClosed
{
add => Events.AddHandler(s_balloonTipClosedEvent, value);
remove => Events.RemoveHandler(s_balloonTipClosedEvent, value);
}
/// <summary>
/// [This event is raised on the NIN_BALLOONSHOW or NIN_BALLOONHIDE message.]
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.NotifyIconOnBalloonTipShownDescr))]
public event EventHandler? BalloonTipShown
{
add => Events.AddHandler(s_balloonTipShownEvent, value);
remove => Events.RemoveHandler(s_balloonTipShownEvent, value);
}
[DefaultValue(null)]
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.NotifyIconMenuDescr))]
public ContextMenuStrip? ContextMenuStrip
{
get
{
return _contextMenuStrip;
}
set
{
_contextMenuStrip = value;
}
}
/// <summary>
/// Gets or sets the current
/// icon.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[DefaultValue(null)]
[SRDescription(nameof(SR.NotifyIconIconDescr))]
public Icon? Icon
{
get
{
return _icon;
}
set
{
if (_icon != value)
{
_icon = value;
UpdateIcon(_visible);
}
}
}
/// <summary>
/// Gets or sets the ToolTip text displayed when
/// the mouse hovers over a system tray icon.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[DefaultValue("")]
[AllowNull]
[SRDescription(nameof(SR.NotifyIconTextDescr))]
[Editor($"System.ComponentModel.Design.MultilineStringEditor, {AssemblyRef.SystemDesign}", typeof(Drawing.Design.UITypeEditor))]
public string Text
{
get
{
return _text;
}
set
{
value ??= string.Empty;
if (!value.Equals(_text))
{
if (value.Length > MaxTextSize)
{
throw new ArgumentOutOfRangeException(nameof(Text), value, SR.TrayIcon_TextTooLong);
}
_text = value;
if (_added)
{
UpdateIcon(true);
}
}
}
}
/// <summary>
/// Gets or sets a value indicating whether the icon is visible in the Windows System Tray.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[DefaultValue(false)]
[SRDescription(nameof(SR.NotifyIconVisDescr))]
public bool Visible
{
get
{
return _visible;
}
set
{
if (_visible != value)
{
UpdateIcon(value);
_visible = value;
}
}
}
[SRCategory(nameof(SR.CatData))]
[Localizable(false)]
[Bindable(true)]
[SRDescription(nameof(SR.ControlTagDescr))]
[DefaultValue(null)]
[TypeConverter(typeof(StringConverter))]
public object? Tag
{
get
{
return _userData;
}
set
{
_userData = value;
}
}
/// <summary>
/// Occurs when the user clicks the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.ControlOnClickDescr))]
public event EventHandler? Click
{
add => Events.AddHandler(s_clickEvent, value);
remove => Events.RemoveHandler(s_clickEvent, value);
}
/// <summary>
/// Occurs when the user double-clicks the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.ControlOnDoubleClickDescr))]
public event EventHandler? DoubleClick
{
add => Events.AddHandler(s_doubleClickEvent, value);
remove => Events.RemoveHandler(s_doubleClickEvent, value);
}
/// <summary>
/// Occurs when the user clicks the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.NotifyIconMouseClickDescr))]
public event MouseEventHandler? MouseClick
{
add => Events.AddHandler(s_mouseClickEvent, value);
remove => Events.RemoveHandler(s_mouseClickEvent, value);
}
/// <summary>
/// Occurs when the user mouse double clicks the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.NotifyIconMouseDoubleClickDescr))]
public event MouseEventHandler? MouseDoubleClick
{
add => Events.AddHandler(s_mouseDoubleClickEvent, value);
remove => Events.RemoveHandler(s_mouseDoubleClickEvent, value);
}
/// <summary>
/// Occurs when the
/// user presses a mouse button while the pointer is over the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatMouse))]
[SRDescription(nameof(SR.ControlOnMouseDownDescr))]
public event MouseEventHandler? MouseDown
{
add => Events.AddHandler(s_mouseDownEvent, value);
remove => Events.RemoveHandler(s_mouseDownEvent, value);
}
/// <summary>
/// Occurs
/// when the user moves the mouse pointer over the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatMouse))]
[SRDescription(nameof(SR.ControlOnMouseMoveDescr))]
public event MouseEventHandler? MouseMove
{
add => Events.AddHandler(s_mouseMoveEvent, value);
remove => Events.RemoveHandler(s_mouseMoveEvent, value);
}
/// <summary>
/// Occurs when the
/// user releases the mouse button while the pointer
/// is over the icon in the system tray.
/// </summary>
[SRCategory(nameof(SR.CatMouse))]
[SRDescription(nameof(SR.ControlOnMouseUpDescr))]
public event MouseEventHandler? MouseUp
{
add => Events.AddHandler(s_mouseUpEvent, value);
remove => Events.RemoveHandler(s_mouseUpEvent, value);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="NotifyIcon" />
/// and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_window is not null)
{
_icon = null;
Text = string.Empty;
UpdateIcon(showIconInTray: false);
_window.DestroyHandle();
_window = null!;
_contextMenuStrip = null;
}
}
else
{
// This same post is done in ControlNativeWindow's finalize method, so if you change
// it, change it there too.
if (_window is not null && _window.Handle != 0)
{
PInvokeCore.PostMessage(_window, PInvokeCore.WM_CLOSE);
_window.ReleaseHandle();
}
}
base.Dispose(disposing);
}
/// <summary>
/// This method raised the BalloonTipClicked event.
/// </summary>
private void OnBalloonTipClicked()
{
((EventHandler?)Events[s_balloonTipClickedEvent])?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// This method raised the BalloonTipClosed event.
/// </summary>
private void OnBalloonTipClosed()
{
((EventHandler?)Events[s_balloonTipClosedEvent])?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// This method raised the BalloonTipShown event.
/// </summary>
private void OnBalloonTipShown()
{
((EventHandler?)Events[s_balloonTipShownEvent])?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// 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.
/// </summary>
private void OnClick(EventArgs e)
{
((EventHandler?)Events[s_clickEvent])?.Invoke(this, e);
}
/// <summary>
/// Inheriting classes should override this method to handle this event.
/// Call base.onDoubleClick to send this event to any registered event listeners.
/// </summary>
private void OnDoubleClick(EventArgs e)
{
((EventHandler?)Events[s_doubleClickEvent])?.Invoke(this, e);
}
/// <summary>
/// Inheriting classes should override this method to handle this event.
/// Call base.OnMouseClick to send this event to any registered event listeners.
/// </summary>
private void OnMouseClick(MouseEventArgs mea)
{
((MouseEventHandler?)Events[s_mouseClickEvent])?.Invoke(this, mea);
}
/// <summary>
/// Inheriting classes should override this method to handle this event.
/// Call base.OnMouseDoubleClick to send this event to any registered event listeners.
/// </summary>
private void OnMouseDoubleClick(MouseEventArgs mea)
{
((MouseEventHandler?)Events[s_mouseDoubleClickEvent])?.Invoke(this, mea);
}
/// <summary>
/// Raises the <see cref="MouseDown"/> event.
/// Inheriting classes should override this method to handle this event.
/// Call base.onMouseDown to send this event to any registered event listeners.
/// </summary>
private void OnMouseDown(MouseEventArgs e)
{
((MouseEventHandler?)Events[s_mouseDownEvent])?.Invoke(this, e);
}
/// <summary>
/// Inheriting classes should override this method to handle this event.
/// Call base.onMouseMove to send this event to any registered event listeners.
/// </summary>
private void OnMouseMove(MouseEventArgs e)
{
((MouseEventHandler?)Events[s_mouseMoveEvent])?.Invoke(this, e);
}
/// <summary>
/// Inheriting classes should override this method to handle this event.
/// Call base.onMouseUp to send this event to any registered event listeners.
/// </summary>
private void OnMouseUp(MouseEventArgs e)
{
((MouseEventHandler?)Events[s_mouseUpEvent])?.Invoke(this, e);
}
/// <summary>
/// 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.
/// </summary>
public void ShowBalloonTip(int timeout)
{
ShowBalloonTip(timeout, _balloonTipTitle, _balloonTipText, _balloonTipIcon);
}
/// <summary>
/// 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.
/// </summary>
public unsafe void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon tipIcon)
{
ArgumentOutOfRangeException.ThrowIfNegative(timeout);
if (string.IsNullOrEmpty(tipText))
{
throw new ArgumentException(SR.NotifyIconEmptyOrNullTipText);
}
// valid values are 0x0 to 0x3
SourceGenerated.EnumValidator.Validate(tipIcon, nameof(tipIcon));
if (_added)
{
// Bail if in design mode...
if (DesignMode)
{
return;
}
NOTIFYICONDATAW data = new()
{
cbSize = (uint)sizeof(NOTIFYICONDATAW),
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_INFO,
uID = _id,
uTimeoutOrVersion = (uint)timeout
};
if (_window.Handle == IntPtr.Zero)
{
_window.CreateHandle(new CreateParams());
}
data.hWnd = _window.Handle;
data.InfoTitle = tipTitle;
data.Info = tipText;
switch (tipIcon)
{
case ToolTipIcon.Info:
data.dwInfoFlags = NOTIFY_ICON_INFOTIP_FLAGS.NIIF_INFO;
break;
case ToolTipIcon.Warning:
data.dwInfoFlags = NOTIFY_ICON_INFOTIP_FLAGS.NIIF_WARNING;
break;
case ToolTipIcon.Error:
data.dwInfoFlags = NOTIFY_ICON_INFOTIP_FLAGS.NIIF_ERROR;
break;
case ToolTipIcon.None:
data.dwInfoFlags = NOTIFY_ICON_INFOTIP_FLAGS.NIIF_NONE;
break;
}
PInvoke.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_MODIFY, ref data);
}
}
/// <summary>
/// Shows the context menu for the tray icon.
/// </summary>
private void ShowContextMenu()
{
if (_contextMenuStrip is not null)
{
PInvoke.GetCursorPos(out Point pt);
// Summary: the current window must be made the foreground window
// before calling TrackPopupMenuEx, and a task switch must be
// forced after the call.
PInvoke.SetForegroundWindow(_window);
// this will set the context menu strip to be toplevel
// and will allow us to overlap the system tray
_contextMenuStrip.ShowInTaskbar(pt.X, pt.Y);
}
}
/// <summary>
/// Updates the icon in the system tray.
/// </summary>
private unsafe void UpdateIcon(bool showIconInTray)
{
lock (_lock)
{
// Bail if in design mode...
if (DesignMode)
{
return;
}
_window.LockReference(showIconInTray);
NOTIFYICONDATAW data = new()
{
cbSize = (uint)sizeof(NOTIFYICONDATAW),
uCallbackMessage = WM_TRAYMOUSEMESSAGE,
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE,
uID = _id
};
if (showIconInTray)
{
if (_window.Handle == IntPtr.Zero)
{
_window.CreateHandle(new CreateParams());
}
}
data.hWnd = _window.Handle;
if (_icon is not null)
{
data.uFlags |= NOTIFY_ICON_DATA_FLAGS.NIF_ICON;
data.hIcon = _icon.Handle;
}
data.uFlags |= NOTIFY_ICON_DATA_FLAGS.NIF_TIP;
data.Tip = _text;
if (showIconInTray && _icon is not null)
{
if (!_added)
{
PInvoke.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_ADD, ref data);
_added = true;
}
else
{
PInvoke.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_MODIFY, ref data);
}
}
else if (_added)
{
PInvoke.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_DELETE, ref data);
_added = false;
}
}
}
/// <summary>
/// Handles the mouse-down event
/// </summary>
private void WmMouseDown(MouseButtons button, int clicks)
{
if (clicks == 2)
{
OnDoubleClick(new MouseEventArgs(button, 2, 0, 0, 0));
OnMouseDoubleClick(new MouseEventArgs(button, 2, 0, 0, 0));
_doubleClick = true;
}
OnMouseDown(new MouseEventArgs(button, clicks, 0, 0, 0));
}
/// <summary>
/// Handles the mouse-move event
/// </summary>
private void WmMouseMove()
{
OnMouseMove(new MouseEventArgs(Control.MouseButtons, 0, 0, 0, 0));
}
/// <summary>
/// Handles the mouse-up event
/// </summary>
private void WmMouseUp(MouseButtons button)
{
OnMouseUp(new MouseEventArgs(button, 0, 0, 0, 0));
// subhag
if (!_doubleClick)
{
OnClick(new MouseEventArgs(button, 0, 0, 0, 0));
OnMouseClick(new MouseEventArgs(button, 0, 0, 0, 0));
}
_doubleClick = false;
}
private void WmTaskbarCreated()
{
_added = false;
UpdateIcon(_visible);
}
private void WndProc(ref Message msg)
{
switch (msg.MsgInternal)
{
case WM_TRAYMOUSEMESSAGE:
switch ((uint)(nint)msg.LParamInternal)
{
case PInvokeCore.WM_LBUTTONDBLCLK:
WmMouseDown(MouseButtons.Left, 2);
break;
case PInvokeCore.WM_LBUTTONDOWN:
WmMouseDown(MouseButtons.Left, 1);
break;
case PInvokeCore.WM_LBUTTONUP:
WmMouseUp(MouseButtons.Left);
break;
case PInvokeCore.WM_MBUTTONDBLCLK:
WmMouseDown(MouseButtons.Middle, 2);
break;
case PInvokeCore.WM_MBUTTONDOWN:
WmMouseDown(MouseButtons.Middle, 1);
break;
case PInvokeCore.WM_MBUTTONUP:
WmMouseUp(MouseButtons.Middle);
break;
case PInvokeCore.WM_MOUSEMOVE:
WmMouseMove();
break;
case PInvokeCore.WM_RBUTTONDBLCLK:
WmMouseDown(MouseButtons.Right, 2);
break;
case PInvokeCore.WM_RBUTTONDOWN:
WmMouseDown(MouseButtons.Right, 1);
break;
case PInvokeCore.WM_RBUTTONUP:
if (_contextMenuStrip is not null)
{
ShowContextMenu();
}
WmMouseUp(MouseButtons.Right);
break;
case PInvoke.NIN_BALLOONSHOW:
OnBalloonTipShown();
break;
case PInvoke.NIN_BALLOONHIDE:
OnBalloonTipClosed();
break;
case PInvoke.NIN_BALLOONTIMEOUT:
OnBalloonTipClosed();
break;
case PInvoke.NIN_BALLOONUSERCLICK:
OnBalloonTipClicked();
break;
}
break;
case PInvokeCore.WM_COMMAND:
if (msg.LParamInternal == 0)
{
if (Command.DispatchID((int)msg.WParamInternal & 0xFFFF))
{
return;
}
}
else
{
_window.DefWndProc(ref msg);
}
break;
case PInvokeCore.WM_DESTROY:
// Remove the icon from the taskbar
UpdateIcon(false);
break;
case PInvokeCore.WM_INITMENUPOPUP:
default:
if (msg.Msg == (int)WM_TASKBARCREATED)
{
WmTaskbarCreated();
}
_window.DefWndProc(ref msg);
break;
}
}
}
|