|
// 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>
/// Provides a small pop-up window containing a line of text that describes the purpose of a
/// tool or control (usually represented as a graphical object) in a program.
/// </summary>
[ProvideProperty(nameof(ToolTip), typeof(Control))]
[DefaultEvent(nameof(Popup))]
[ToolboxItemFilter("System.Windows.Forms")]
[SRDescription(nameof(SR.DescriptionToolTip))]
public partial class ToolTip : Component, IExtenderProvider, IHandle<HWND>
{
// The actual delay values are based on the user-set double click time.
// These values are initialized using the default double-click time value.
internal const int DefaultDelay = 500;
// These values are copied from the ComCtl32's tooltip.
private const int ReshowRatio = 5;
private const int AutoPopRatio = 10;
private const int InfiniteDelay = short.MaxValue;
private const int BalloonOffsetX = 10;
private const int LocationIndexTop = 0;
private const int LocationIndexRight = 1;
private const int LocationIndexBottom = 2;
private const int LocationIndexLeft = 3;
private const int LocationTotal = 4;
private readonly Dictionary<Control, TipInfo> _tools = [];
private readonly int[] _delayTimes = new int[4];
private bool _auto = true;
private bool _showAlways;
private ToolTipNativeWindow _window;
private Control? _topLevelControl;
private bool _active = true;
private Color _backColor = SystemColors.Info;
private Color _foreColor = SystemColors.InfoText;
private bool _isBalloon;
private bool _isDisposing;
private string _toolTipTitle = string.Empty;
private ToolTipIcon _toolTipIcon = ToolTipIcon.None;
private ToolTipTimer? _timer;
private readonly Dictionary<HWND, Control> _owners = [];
private bool _stripAmpersands;
private bool _useAnimation = true;
private bool _useFading = true;
private int _originalPopupDelay;
/// <summary>
/// Setting TTM_TRACKPOSITION will cause redundant POP and Draw Messages.
/// Hence we guard against this by having this private flag.
/// </summary>
private bool _trackPosition;
private PopupEventHandler? _onPopup;
private DrawToolTipEventHandler? _onDraw;
/// <summary>
/// Adding a tool twice breaks the ToolTip, so we need to track which
/// tools are created to prevent this.
/// </summary>
private readonly HashSet<Control> _created = [];
private bool _cancelled;
/// <summary>
/// Initializes a new instance of the <see cref="ToolTip"/> class, given the container.
/// </summary>
public ToolTip(IContainer cont)
: this()
{
ArgumentNullException.ThrowIfNull(cont);
cont.Add(this);
IsPersistent = OsVersion.IsWindows11_OrGreater();
}
/// <summary>
/// Initializes a new instance of the <see cref="ToolTip"/> class in its default state.
/// </summary>
public ToolTip()
{
_window = new ToolTipNativeWindow(this);
_auto = true;
_delayTimes[(int)PInvoke.TTDT_AUTOMATIC] = DefaultDelay;
IsPersistent = OsVersion.IsWindows11_OrGreater();
AdjustBaseFromAuto();
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="ToolTip"/> control is currently active.
/// </summary>
[SRDescription(nameof(SR.ToolTipActiveDescr))]
[DefaultValue(true)]
public bool Active
{
get => _active;
set
{
if (_active != value)
{
_active = value;
// Don't activate the tooltip if we're in the designer.
if (!DesignMode && GetHandleCreated())
{
PInvokeCore.SendMessage(this, PInvoke.TTM_ACTIVATE, (WPARAM)(BOOL)value);
}
}
}
}
internal void HideToolTip(IKeyboardToolTip currentTool)
{
IWin32Window? ownerWindow = currentTool.GetOwnerWindow();
if (ownerWindow is not null)
{
Hide(ownerWindow);
}
}
/// <summary>
/// Gets or sets the time (in milliseconds) that passes before the <see cref="ToolTip"/> appears.
/// </summary>
[RefreshProperties(RefreshProperties.All)]
[SRDescription(nameof(SR.ToolTipAutomaticDelayDescr))]
[DefaultValue(DefaultDelay)]
public int AutomaticDelay
{
get => _delayTimes[(int)PInvoke.TTDT_AUTOMATIC];
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
SetDelayTime((int)PInvoke.TTDT_AUTOMATIC, value);
}
}
internal string? GetCaptionForTool(Control tool)
{
return _tools.TryGetValue(tool, out TipInfo? tipInfo) ? tipInfo.Caption : null;
}
/// <summary>
/// Gets or sets the initial delay for the <see cref="ToolTip"/> control.
/// </summary>
[RefreshProperties(RefreshProperties.All)]
[SRDescription(nameof(SR.ToolTipAutoPopDelayDescr))]
public int AutoPopDelay
{
get => _delayTimes[(int)PInvoke.TTDT_AUTOPOP];
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
SetDelayTime(PInvoke.TTDT_AUTOPOP, value);
}
}
/// <summary>
/// Gets or sets the BackColor for the <see cref="ToolTip"/> control.
/// </summary>
[SRDescription(nameof(SR.ToolTipBackColorDescr))]
[DefaultValue(typeof(Color), "Info")]
public Color BackColor
{
get => _backColor;
set
{
_backColor = value;
if (GetHandleCreated())
{
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTIPBKCOLOR, (WPARAM)_backColor);
}
}
}
/// <summary>
/// The CreateParams to create the window.
/// </summary>
protected virtual CreateParams CreateParams
{
get
{
CreateParams cp = new();
if (TopLevelControl is not null && !TopLevelControl.IsDisposed)
{
cp.Parent = TopLevelControl.Handle;
}
cp.ClassName = PInvoke.TOOLTIPS_CLASS;
if (_showAlways)
{
cp.Style = (int)PInvoke.TTS_ALWAYSTIP;
}
if (_isBalloon)
{
cp.Style |= (int)PInvoke.TTS_BALLOON;
}
if (!_stripAmpersands)
{
cp.Style |= (int)PInvoke.TTS_NOPREFIX;
}
if (!_useAnimation)
{
cp.Style |= (int)PInvoke.TTS_NOANIMATE;
}
if (!_useFading)
{
cp.Style |= (int)PInvoke.TTS_NOFADE;
}
cp.ExStyle = 0;
cp.Caption = null;
return cp;
}
}
/// <summary>
/// Gets or sets the ForeColor for the <see cref="ToolTip"/> control.
/// </summary>
[SRDescription(nameof(SR.ToolTipForeColorDescr))]
[DefaultValue(typeof(Color), "InfoText")]
public Color ForeColor
{
get => _foreColor;
set
{
if (value.IsEmpty)
{
throw new ArgumentException(string.Format(SR.ToolTipEmptyColor, nameof(ForeColor)), nameof(value));
}
_foreColor = value;
if (GetHandleCreated())
{
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTIPTEXTCOLOR, (WPARAM)_foreColor);
}
}
}
HWND IHandle<HWND>.Handle => HWND;
internal HWND HWND => (HWND)Handle;
internal IntPtr Handle
{
get
{
if (!GetHandleCreated())
{
CreateHandle();
}
return _window.Handle;
}
}
/// <summary>
/// Shows if the keyboard tooltip is currently active.
/// </summary>
internal bool IsActivatedByKeyboard { get; set; }
/// <summary>
/// Gets or sets the IsBalloon for the <see cref="ToolTip"/> control.
/// </summary>
[SRDescription(nameof(SR.ToolTipIsBalloonDescr))]
[DefaultValue(false)]
public bool IsBalloon
{
get => _isBalloon;
set
{
if (_isBalloon != value)
{
_isBalloon = value;
if (GetHandleCreated())
{
RecreateHandle();
}
}
}
}
private bool IsWindowActive(IWin32Window window)
{
// We want to enter in the if block only if ShowParams does not return SW_SHOWNOACTIVATE.
// for ToolStripDropDown ShowParams returns SW_SHOWNOACTIVATE, in which case we don't
// want to check IsWindowActive and hence return true.
if (window is Control control &&
(control.ShowParams & (SHOW_WINDOW_CMD)0xF) != SHOW_WINDOW_CMD.SW_SHOWNOACTIVATE)
{
HWND hwnd = PInvoke.GetActiveWindow();
HWND rootHwnd = PInvoke.GetAncestor(control, GET_ANCESTOR_FLAGS.GA_ROOT);
if (hwnd != rootHwnd)
{
if (_tools.TryGetValue(control, out TipInfo? tt) && (tt.TipType & TipInfo.Type.SemiAbsolute) != 0)
{
_tools.Remove(control);
DestroyRegion(control);
}
return false;
}
}
return true;
}
/// <summary>
/// Gets or sets the initial delay for the <see cref="ToolTip"/> control.
/// </summary>
[RefreshProperties(RefreshProperties.All)]
[SRDescription(nameof(SR.ToolTipInitialDelayDescr))]
public int InitialDelay
{
get => _delayTimes[(int)PInvoke.TTDT_INITIAL];
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
SetDelayTime(PInvoke.TTDT_INITIAL, value);
}
}
/// <summary>
/// Indicates whether the ToolTip will be drawn by the system or the user.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.ToolTipOwnerDrawDescr))]
public bool OwnerDraw { get; set; }
/// <summary>
/// Gets or sets the length of time (in milliseconds) that it takes subsequent ToolTip
/// instances to appear as the mouse pointer moves from one ToolTip region to another.
/// </summary>
[RefreshProperties(RefreshProperties.All)]
[SRDescription(nameof(SR.ToolTipReshowDelayDescr))]
public int ReshowDelay
{
get => _delayTimes[(int)PInvoke.TTDT_RESHOW];
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
SetDelayTime(PInvoke.TTDT_RESHOW, value);
}
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="ToolTip"/> appears even when its
/// parent control is not active.
/// </summary>
[DefaultValue(false)]
[SRDescription(nameof(SR.ToolTipShowAlwaysDescr))]
public bool ShowAlways
{
get => _showAlways;
set
{
if (_showAlways != value)
{
_showAlways = value;
if (GetHandleCreated())
{
RecreateHandle();
}
}
}
}
/// <summary>
/// When set to true, any ampersands in the Text property are not displayed.
/// </summary>
[SRDescription(nameof(SR.ToolTipStripAmpersandsDescr))]
[Browsable(true)]
[DefaultValue(false)]
public bool StripAmpersands
{
get => _stripAmpersands;
set
{
if (_stripAmpersands != value)
{
_stripAmpersands = value;
if (GetHandleCreated())
{
RecreateHandle();
}
}
}
}
[SRCategory(nameof(SR.CatData))]
[Localizable(false)]
[Bindable(true)]
[SRDescription(nameof(SR.ControlTagDescr))]
[DefaultValue(null)]
[TypeConverter(typeof(StringConverter))]
public object? Tag { get; set; }
/// <summary>
/// Gets or sets an Icon on the ToolTip.
/// </summary>
[DefaultValue(ToolTipIcon.None)]
[SRDescription(nameof(SR.ToolTipToolTipIconDescr))]
public ToolTipIcon ToolTipIcon
{
get => _toolTipIcon;
set
{
if (_toolTipIcon != value)
{
SourceGenerated.EnumValidator.Validate(value);
_toolTipIcon = value;
if (_toolTipIcon > 0 && GetHandleCreated())
{
// If the title is null/empty, the icon won't display.
string title = !string.IsNullOrEmpty(_toolTipTitle) ? _toolTipTitle : " ";
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTITLEW, (uint)_toolTipIcon, title);
// Tooltip need to be updated to reflect the changes in the icon because
// this operation directly affects the size of the tooltip.
PInvokeCore.SendMessage(this, PInvoke.TTM_UPDATE);
}
}
}
}
/// <summary>
/// Gets or sets the title of the ToolTip.
/// </summary>
[DefaultValue("")]
[SRDescription(nameof(SR.ToolTipTitleDescr))]
[AllowNull]
public string ToolTipTitle
{
get => _toolTipTitle;
set
{
value ??= string.Empty;
if (_toolTipTitle != value)
{
_toolTipTitle = value;
if (GetHandleCreated())
{
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTITLEW, (uint)_toolTipIcon, _toolTipTitle);
// Tooltip need to be updated to reflect the changes in the title text because
// this operation directly affects the size of the tooltip.
PInvokeCore.SendMessage(this, PInvoke.TTM_UPDATE);
}
}
}
}
private Control? TopLevelControl
{
get
{
if (_topLevelControl is not null)
{
return _topLevelControl;
}
Control? currentTopLevel = null;
Control[] controls = [.. _tools.Keys];
for (int i = 0; i < controls.Length; i++)
{
var control = controls[i];
currentTopLevel = control.TopLevelControlInternal;
if (currentTopLevel is not null)
{
break;
}
if (control.IsActiveX)
{
currentTopLevel = control;
break;
}
// In the designer, baseVar can be null since the Parent is not a TopLevel control
if (currentTopLevel is null)
{
if (control is not null && control.ParentInternal is not null)
{
while (control.ParentInternal is not null)
{
control = control.ParentInternal;
}
currentTopLevel = control;
if (currentTopLevel is not null)
{
break;
}
}
}
}
_topLevelControl = currentTopLevel;
if (currentTopLevel is not null)
{
currentTopLevel.HandleCreated += TopLevelCreated;
currentTopLevel.HandleDestroyed += TopLevelDestroyed;
if (currentTopLevel.IsHandleCreated)
{
TopLevelCreated(currentTopLevel, EventArgs.Empty);
}
currentTopLevel.ParentChanged += OnTopLevelPropertyChanged;
}
return currentTopLevel;
}
}
/// <summary>
/// When set to true, animations are used when tooltip is shown or hidden.
/// </summary>
[SRDescription(nameof(SR.ToolTipUseAnimationDescr))]
[Browsable(true)]
[DefaultValue(true)]
public bool UseAnimation
{
get => _useAnimation;
set
{
if (_useAnimation != value)
{
_useAnimation = value;
if (GetHandleCreated())
{
RecreateHandle();
}
}
}
}
/// <summary>
/// When set to true, a fade effect is used when tooltips are shown or hidden.
/// </summary>
[SRDescription(nameof(SR.ToolTipUseFadingDescr))]
[Browsable(true)]
[DefaultValue(true)]
public bool UseFading
{
get => _useFading;
set
{
if (_useFading != value)
{
_useFading = value;
if (GetHandleCreated())
{
RecreateHandle();
}
}
}
}
/// <summary>
/// Fires in OwnerDraw mode when the tooltip needs to be drawn.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.ToolTipDrawEventDescr))]
public event DrawToolTipEventHandler? Draw
{
add => _onDraw += value;
remove => _onDraw -= value;
}
/// <summary>
/// Fires when the tooltip is just about to be shown.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.ToolTipPopupEventDescr))]
public event PopupEventHandler? Popup
{
add => _onPopup += value;
remove => _onPopup -= value;
}
/// <summary>
/// Adjusts the other delay values based on the Automatic value.
/// </summary>
private void AdjustBaseFromAuto()
{
int delay = _delayTimes[(int)PInvoke.TTDT_AUTOMATIC];
_delayTimes[(int)PInvoke.TTDT_RESHOW] = delay / ReshowRatio;
_delayTimes[(int)PInvoke.TTDT_AUTOPOP] = delay * AutoPopRatio;
_delayTimes[(int)PInvoke.TTDT_INITIAL] = delay;
}
/// <summary>
/// Screen reader announces ToolTip text for an element.
/// </summary>
private void AnnounceText(Control? tool, string text)
{
if (tool is null || !tool.IsAccessibilityObjectCreated)
{
return;
}
tool.AccessibilityObject.InternalRaiseAutomationNotification(
Automation.AutomationNotificationKind.ActionCompleted,
Automation.AutomationNotificationProcessing.All,
$"{ToolTipTitle} {text}");
}
private void HandleCreated(object? sender, EventArgs eventargs)
{
// Reset the top level control when the owner's handle is recreated.
ClearTopLevelControlEvents();
_topLevelControl = null;
Control control = (Control)sender!;
CreateRegion(control);
SetToolTipToControl(control);
KeyboardToolTipStateMachine.Instance.Hook(control, this);
}
private void HandleDestroyed(object? sender, EventArgs eventargs)
{
Control control = (Control)sender!;
DestroyRegion(control);
KeyboardToolTipStateMachine.Instance.Unhook(control, this);
}
/// <summary>
/// Raises the Draw event.
/// </summary>
private void OnDraw(DrawToolTipEventArgs e) => _onDraw?.Invoke(this, e);
/// <summary>
/// Raises the Popup event.
/// </summary>
private void OnPopup(PopupEventArgs e) => _onPopup?.Invoke(this, e);
private void TopLevelCreated(object? sender, EventArgs eventargs)
{
CreateHandle();
CreateAllRegions();
}
private void TopLevelDestroyed(object? sender, EventArgs eventargs)
{
DestroyAllRegions();
DestroyHandle();
}
/// <summary>
/// Returns true if the tooltip can offer an extender property to the specified target component.
/// </summary>
public bool CanExtend(object target) => target is Control;
private void ClearTopLevelControlEvents()
{
if (_topLevelControl is null)
{
return;
}
_topLevelControl.ParentChanged -= OnTopLevelPropertyChanged;
_topLevelControl.HandleCreated -= TopLevelCreated;
_topLevelControl.HandleDestroyed -= TopLevelDestroyed;
}
/// <summary>
/// Creates the handle for the control.
/// </summary>
private unsafe void CreateHandle()
{
if (GetHandleCreated())
{
return;
}
using (ThemingScope scope = new(Application.UseVisualStyles))
{
PInvoke.InitCommonControlsEx(new INITCOMMONCONTROLSEX
{
dwSize = (uint)sizeof(INITCOMMONCONTROLSEX),
dwICC = INITCOMMONCONTROLSEX_ICC.ICC_TAB_CLASSES
});
// Avoid reentrant call to CreateHandle.
CreateParams cp = CreateParams;
if (GetHandleCreated())
{
return;
}
_window.CreateHandle(cp);
if (SystemInformation.HighContrast)
{
PInvoke.SetWindowTheme(HWND, string.Empty, string.Empty);
}
}
// If in OwnerDraw mode, we don't want the default border.
if (OwnerDraw)
{
int style = unchecked((int)((long)PInvokeCore.GetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE)));
style &= ~(int)WINDOW_STYLE.WS_BORDER;
PInvokeCore.SetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);
}
// Setting the max width has the added benefit of enabling multiline tool tips.
PInvokeCore.SendMessage(this, PInvoke.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width);
if (_auto)
{
// AutomaticDelay property should overwrite other delay timer values, _auto field indicates that
// AutomaticDelay had been set more recently than other delays.
if (_delayTimes[(int)PInvoke.TTDT_AUTOMATIC] != DefaultDelay)
{
SetDelayTime(PInvoke.TTDT_AUTOMATIC, _delayTimes[(int)PInvoke.TTDT_AUTOMATIC]);
}
else
{
_delayTimes[(int)PInvoke.TTDT_AUTOPOP] = GetDelayTime(PInvoke.TTDT_AUTOPOP);
_delayTimes[(int)PInvoke.TTDT_INITIAL] = GetDelayTime(PInvoke.TTDT_INITIAL);
_delayTimes[(int)PInvoke.TTDT_RESHOW] = GetDelayTime(PInvoke.TTDT_RESHOW);
}
}
else
{
int delayTime = _delayTimes[(int)PInvoke.TTDT_AUTOPOP];
if (delayTime is >= 1 and not (DefaultDelay * AutoPopRatio))
{
SetDelayTime(PInvoke.TTDT_AUTOPOP, delayTime);
}
delayTime = _delayTimes[(int)PInvoke.TTDT_INITIAL];
if (delayTime >= 1)
{
SetDelayTime(PInvoke.TTDT_INITIAL, delayTime);
}
delayTime = _delayTimes[(int)PInvoke.TTDT_RESHOW];
if (delayTime >= 1)
{
SetDelayTime(PInvoke.TTDT_RESHOW, delayTime);
}
}
// Set active status.
PInvokeCore.SendMessage(this, PInvoke.TTM_ACTIVATE, (WPARAM)(BOOL)_active);
if (BackColor != SystemColors.Info)
{
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTIPBKCOLOR, (WPARAM)BackColor);
}
if (ForeColor != SystemColors.InfoText)
{
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTIPTEXTCOLOR, (WPARAM)ForeColor);
}
if (_toolTipIcon > 0 || !string.IsNullOrEmpty(_toolTipTitle))
{
// If the title is null/empty, the icon won't display.
string title = !string.IsNullOrEmpty(_toolTipTitle) ? _toolTipTitle : " ";
PInvokeCore.SendMessage(this, PInvoke.TTM_SETTITLEW, (WPARAM)(int)_toolTipIcon, title);
}
}
private void CreateAllRegions()
{
Control[] controls = [.. _tools.Keys];
foreach (Control control in controls)
{
CreateRegion(control);
}
}
private void DestroyAllRegions()
{
Control[] controls = [.. _tools.Keys];
foreach (Control control in controls)
{
// DataGridView manages its own tool tip.
if (control is DataGridView)
{
return;
}
DestroyRegion(control);
}
}
private void SetToolInfo(Control control, string? caption)
{
// Don't add a tool for the TabControl itself. It's not needed because:
// 1. TabControl already relays all mouse events to its tooltip:
// https://docs.microsoft.com/windows/win32/controls/tab-controls#default-tab-control-message-processing
// 2. Hit-testing against TabControl detects only TabPages which are separate tools added by TabControl.
// This prevents a bug when a TabPage tool is placed before the TabControl tool after reordering caused
// by a tool deletion.Also prevents double handling of mouse messages which is caused by subclassing
// altogether with the message relaying from the TabControl internals.
if (control is TabControl)
{
return;
}
IntPtr result = GetTOOLINFO(control, caption).SendMessage(this, PInvoke.TTM_ADDTOOLW);
if ((control is TreeView tv && tv.ShowNodeToolTips)
|| (control is ListView lv && lv.ShowItemToolTips))
{
return;
}
if (result == IntPtr.Zero)
{
throw new InvalidOperationException(SR.ToolTipAddFailed);
}
}
private void CreateRegion(Control control)
{
string? caption = GetToolTip(control);
bool handlesCreated = control.IsHandleCreated
&& TopLevelControl is not null
&& TopLevelControl.IsHandleCreated;
if (!_created.Contains(control)
&& !string.IsNullOrEmpty(caption)
&& handlesCreated && !DesignMode)
{
// Call the SendMessage through a function.
SetToolInfo(control, caption);
_created.Add(control);
}
if (control.IsHandleCreated && _topLevelControl is null)
{
// Remove first to purge any duplicates.
control.MouseMove -= MouseMove;
control.MouseMove += MouseMove;
}
}
private void MouseMove(object? sender, MouseEventArgs me)
{
if (sender is not Control control)
{
return;
}
if (!_created.Contains(control) && control.IsHandleCreated && TopLevelControl is not null)
{
CreateRegion(control);
}
if (_created.Contains(control))
{
control.MouseMove -= MouseMove;
}
}
/// <summary>
/// Destroys the handle for this control.
/// Required by Label to destroy the handle for the toolTip added for AutoEllipses.
/// </summary>
internal void DestroyHandle()
{
if (GetHandleCreated())
{
_window.DestroyHandle();
}
}
private void DestroyRegion(Control control)
{
// When the toplevelControl is a form and is Modal, the Handle of the tooltip is released
// before we come here. In such a case the tool won't get deleted from the tooltip.
// So we don't check "Handle" in the handlesCreate but check it only for non-modal dialogs later.
bool handlesCreated = control.IsHandleCreated
&& _topLevelControl is not null
&& _topLevelControl.IsHandleCreated
&& !_isDisposing;
if (_topLevelControl is not Form topForm || (topForm is not null && !topForm.Modal))
{
handlesCreated = handlesCreated && GetHandleCreated();
}
if (_created.Contains(control) && handlesCreated && !DesignMode)
{
new ToolInfoWrapper<Control>(control).SendMessage(this, PInvoke.TTM_DELTOOLW);
_created.Remove(control);
control.RemoveToolTip(this);
}
}
/// <summary>
/// Disposes of the <see cref="ToolTip"/> component.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_isDisposing = true;
try
{
ClearTopLevelControlEvents();
StopTimer();
// Always destroy the handle.
DestroyHandle();
RemoveAll();
_window = null!;
// Unhook the DeactivateEvent. Find the Form for associated Control and hook
// up to the Deactivated event to Hide the Shown tooltip
if (TopLevelControl is Form baseFrom)
{
baseFrom.Deactivate -= BaseFormDeactivate;
}
}
finally
{
_isDisposing = false;
}
}
base.Dispose(disposing);
}
/// <summary>
/// Returns the delay time based on the NativeMethods.TTDT_* values.
/// </summary>
internal int GetDelayTime(uint type)
{
if (!GetHandleCreated())
{
return _delayTimes[(int)type];
}
return (int)PInvokeCore.SendMessage(this, PInvoke.TTM_GETDELAYTIME, (WPARAM)type);
}
internal bool GetHandleCreated() => _window is not null && _window.Handle != IntPtr.Zero;
/// <summary>
/// Returns a detailed TOOLINFO_TOOLTIP structure that represents the specified region.
/// </summary>
private unsafe ToolInfoWrapper<Control> GetTOOLINFO(Control control, string? caption)
{
TOOLTIP_FLAGS flags = TOOLTIP_FLAGS.TTF_TRANSPARENT | TOOLTIP_FLAGS.TTF_SUBCLASS;
// RightToLeft reading order.
if (TopLevelControl?.RightToLeft == RightToLeft.Yes && !control.IsMirrored)
{
// Indicates that the ToolTip text will be displayed in the opposite direction
// to the text in the parent window.
flags |= TOOLTIP_FLAGS.TTF_RTLREADING;
}
return control.GetToolInfoWrapper(flags, caption, this);
}
private ToolInfoWrapper<HandleRef<HWND>> GetWinTOOLINFO(IWin32Window window)
{
TOOLTIP_FLAGS flags = TOOLTIP_FLAGS.TTF_TRANSPARENT | TOOLTIP_FLAGS.TTF_SUBCLASS;
// RightToLeft reading order
if (TopLevelControl?.RightToLeft == RightToLeft.Yes)
{
// Indicates that the ToolTip text will be displayed in the opposite direction
// to the text in the parent window.
if (!window.GetExtendedStyle().HasFlag(WINDOW_EX_STYLE.WS_EX_LAYOUTRTL))
{
flags |= TOOLTIP_FLAGS.TTF_RTLREADING;
}
}
return new(Control.GetSafeHandle(window), flags);
}
/// <summary>
/// Retrieves the <see cref="ToolTip"/> text associated with the specified control.
/// </summary>
[DefaultValue("")]
[Localizable(true)]
[SRDescription(nameof(SR.ToolTipToolTipDescr))]
[Editor($"System.ComponentModel.Design.MultilineStringEditor, {AssemblyRef.SystemDesign}", typeof(Drawing.Design.UITypeEditor))]
public string? GetToolTip(Control? control)
{
if (control is null)
{
return string.Empty;
}
return _tools.TryGetValue(control, out TipInfo? tipInfo)
? tipInfo.Caption
: string.Empty;
}
/// <summary>
/// Returns the HWND of the window that is at the specified point. This handles special
/// cases where one Control owns multiple HWNDs (i.e. ComboBox).
/// </summary>
private HWND GetWindowFromPoint(Point screenCoords, ref bool success)
{
Control? current = TopLevelControl;
// Special case ActiveX Controls.
if (current is not null && current.IsActiveX)
{
// Find the matching HWnd matching the ScreenCoord and find if the Control has a Tooltip.
HWND hwndControl = PInvoke.WindowFromPoint(screenCoords);
if (!hwndControl.IsNull)
{
Control? currentControl = Control.FromHandle(hwndControl);
if (currentControl is not null &&
_tools.ContainsKey(currentControl))
{
return hwndControl;
}
}
return HWND.Null;
}
HWND baseHwnd = current?.HWND ?? HWND.Null;
HWND hwnd = HWND.Null;
bool finalMatch = false;
while (!finalMatch)
{
Point pt = screenCoords;
if (current is not null)
{
pt = current.PointToClient(screenCoords);
}
HWND found = PInvoke.ChildWindowFromPointEx(baseHwnd, pt, CWP_FLAGS.CWP_SKIPINVISIBLE);
if (found == baseHwnd)
{
hwnd = found;
finalMatch = true;
}
else if (found.IsNull)
{
finalMatch = true;
}
else
{
current = Control.FromHandle(found);
if (current is null)
{
current = Control.FromChildHandle(found);
if (current is not null)
{
hwnd = current.HWND;
}
finalMatch = true;
}
else
{
baseHwnd = current.HWND;
}
}
}
if (!hwnd.IsNull)
{
Control? control = Control.FromHandle(hwnd);
if (control is not null)
{
current = control;
while (current is not null && current.Visible)
{
current = current.ParentInternal;
}
if (current is not null)
{
hwnd = HWND.Null;
}
success = true;
}
}
return hwnd;
}
private void OnTopLevelPropertyChanged(object? s, EventArgs e)
{
ClearTopLevelControlEvents();
_topLevelControl = null;
// We must re-acquire this control. If the existing top level control's handle
// was never created, but the new parent has a handle, if we don't re-get
// the top level control here we won't ever create the tooltip handle.
_topLevelControl = TopLevelControl;
}
private void RecreateHandle()
{
if (DesignMode)
{
return;
}
if (GetHandleCreated())
{
DestroyHandle();
}
_created.Clear();
CreateHandle();
CreateAllRegions();
}
/// <summary>
/// Removes all of the tooltips currently associated with the <see cref="ToolTip"/> control.
/// </summary>
public void RemoveAll()
{
Control[] controls = [.. _tools.Keys];
foreach (Control control in controls)
{
if (control.IsHandleCreated)
{
DestroyRegion(control);
}
control.HandleCreated -= HandleCreated;
control.HandleDestroyed -= HandleDestroyed;
KeyboardToolTipStateMachine.Instance.Unhook(control, toolTip: this);
}
_created.Clear();
_tools.Clear();
ClearTopLevelControlEvents();
_topLevelControl = null;
KeyboardToolTipStateMachine.Instance.ResetStateMachine(this);
}
/// <summary>
/// Sets the delay time of TTDT_* values.
/// </summary>
private void SetDelayTime(uint type, int time)
{
_auto = type == PInvoke.TTDT_AUTOMATIC;
_delayTimes[(int)type] = time;
if (GetHandleCreated() && time >= 0)
{
PInvokeCore.SendMessage(this, PInvoke.TTM_SETDELAYTIME, (WPARAM)type, (LPARAM)time);
if (type == PInvoke.TTDT_AUTOPOP && time != InfiniteDelay)
{
IsPersistent = false;
}
// Update everyone else if automatic is set. we need to do this
// to preserve value in case of handle recreation.
if (_auto)
{
_delayTimes[(int)PInvoke.TTDT_AUTOPOP] = GetDelayTime(PInvoke.TTDT_AUTOPOP);
_delayTimes[(int)PInvoke.TTDT_INITIAL] = GetDelayTime(PInvoke.TTDT_INITIAL);
_delayTimes[(int)PInvoke.TTDT_RESHOW] = GetDelayTime(PInvoke.TTDT_RESHOW);
}
}
else if (_auto)
{
AdjustBaseFromAuto();
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> text with the specified control.
/// </summary>
public void SetToolTip(Control control, string? caption)
{
TipInfo info = new(caption, TipInfo.Type.Auto);
SetToolTipInternal(control, info);
}
/// <summary>
/// Associates <see cref="ToolTip"/> text with the specified information
/// </summary>
private void SetToolTipInternal(Control control, TipInfo info)
{
ArgumentNullException.ThrowIfNull(control);
bool exists = _tools.ContainsKey(control);
bool empty = info is null || string.IsNullOrEmpty(info.Caption);
if (exists && empty)
{
_tools.Remove(control);
}
else if (!empty)
{
_tools[control] = info!;
}
if (!empty && !exists)
{
control.HandleCreated += HandleCreated;
control.HandleDestroyed += HandleDestroyed;
if (control.IsHandleCreated)
{
HandleCreated(control, EventArgs.Empty);
}
}
else
{
bool handlesCreated = control.IsHandleCreated
&& TopLevelControl is not null
&& TopLevelControl.IsHandleCreated;
if (exists && !empty && handlesCreated && !DesignMode)
{
ToolInfoWrapper<Control> toolInfo = GetTOOLINFO(control, info!.Caption);
toolInfo.SendMessage(this, PInvoke.TTM_SETTOOLINFOW);
SetToolTipToControl(control);
}
else if (empty && exists && !DesignMode)
{
control.HandleCreated -= HandleCreated;
control.HandleDestroyed -= HandleDestroyed;
if (control.IsHandleCreated)
{
HandleDestroyed(control, EventArgs.Empty);
}
_created.Remove(control);
}
}
}
private void SetToolTipToControl(Control associatedControl)
{
if (GetHandleCreated())
{
associatedControl.SetToolTip(this);
}
}
/// <summary>
/// Persistent tooltip is not dismissed automatically by a timer. It is dismissed by mouse
/// movements outside this tooltip or by WM_KEYUP for CONTROL or ESCAPE key.
/// Windows 11 considers tooltip persistent if AuoPopDelay had never been set or
/// was set to infinity.
/// </summary>
internal bool IsPersistent { get; set; }
/// <summary>
/// Returns true if the AutomaticDelay property should be persisted.
/// </summary>
private bool ShouldSerializeAutomaticDelay() => _auto && AutomaticDelay != DefaultDelay;
/// <summary>
/// Returns true if the AutoPopDelay property should be persisted.
/// </summary>
private bool ShouldSerializeAutoPopDelay() => !_auto;
/// <summary>
/// Returns true if the InitialDelay property should be persisted.
/// </summary>
private bool ShouldSerializeInitialDelay() => !_auto;
/// <summary>
/// Returns true if the ReshowDelay property should be persisted.
/// </summary>
private bool ShouldSerializeReshowDelay() => !_auto;
/// <summary>
/// Shows a tooltip for specified text, window, and hotspot
/// </summary>
private void ShowTooltip(string? text, IWin32Window window, int duration)
{
ArgumentNullException.ThrowIfNull(window);
if (window is Control associatedControl)
{
PInvokeCore.GetWindowRect(associatedControl, out var rect);
_ = Cursor.Current;
Point cursorLocation = Cursor.Position;
Point p = cursorLocation;
Screen screen = Screen.FromPoint(cursorLocation);
// Place the tool tip on the associated control if its not already there.
if (cursorLocation.X < rect.left || cursorLocation.X > rect.right ||
cursorLocation.Y < rect.top || cursorLocation.Y > rect.bottom)
{
// Calculate the dimensions of the visible rectangle which
// is used to estimate the upper x,y of the tooltip placement.
RECT visibleRect = new RECT
{
left = (rect.left < screen.WorkingArea.Left) ? screen.WorkingArea.Left : rect.left,
top = (rect.top < screen.WorkingArea.Top) ? screen.WorkingArea.Top : rect.top,
right = (rect.right > screen.WorkingArea.Right) ? screen.WorkingArea.Right : rect.right,
bottom = (rect.bottom > screen.WorkingArea.Bottom) ? screen.WorkingArea.Bottom : rect.bottom
};
p.X = visibleRect.left + visibleRect.Width / 2;
p.Y = visibleRect.top + visibleRect.Height / 2;
associatedControl.PointToClient(p);
SetTrackPosition(p.X, p.Y);
SetTool(window, text, TipInfo.Type.SemiAbsolute, p);
if (duration > 0)
{
StartTimer(_window, duration);
}
}
else
{
if (!_tools.TryGetValue(associatedControl, out TipInfo? tipInfo))
{
tipInfo = new TipInfo(text, TipInfo.Type.SemiAbsolute);
}
else
{
tipInfo.TipType |= TipInfo.Type.SemiAbsolute;
tipInfo.Caption = text;
}
tipInfo.Position = p;
if (duration > 0)
{
if (_originalPopupDelay == 0)
{
_originalPopupDelay = AutoPopDelay;
}
AutoPopDelay = duration;
}
SetToolTipInternal(associatedControl, tipInfo);
}
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> with the specified control and displays it.
/// </summary>
public void Show(string? text, IWin32Window window)
{
// Check if the foreground window is the TopLevelWindow
if (IsWindowActive(window))
{
ShowTooltip(text, window, duration: 0);
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> with the specified control and displays it for the
/// specified duration.
/// </summary>
public void Show(string? text, IWin32Window window, int duration)
{
ArgumentNullException.ThrowIfNull(window);
ArgumentOutOfRangeException.ThrowIfNegative(duration);
if (IsWindowActive(window))
{
ShowTooltip(text, window, duration);
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> with the specified control and displays it.
/// </summary>
public void Show(string? text, IWin32Window window, Point point)
{
ArgumentNullException.ThrowIfNull(window);
if (IsWindowActive(window))
{
// Set the ToolTips.
PInvokeCore.GetWindowRect(Control.GetSafeHandle(window), out var r);
int pointX = r.left + point.X;
int pointY = r.top + point.Y;
SetTrackPosition(pointX, pointY);
SetTool(window, text, TipInfo.Type.Absolute, new Point(pointX, pointY));
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> with the specified control and displays it.
/// </summary>
public void Show(string? text, IWin32Window window, Point point, int duration)
{
ArgumentNullException.ThrowIfNull(window);
ArgumentOutOfRangeException.ThrowIfNegative(duration);
if (IsWindowActive(window))
{
// Set the ToolTips.
PInvokeCore.GetWindowRect(Control.GetSafeHandle(window), out var r);
int pointX = r.left + point.X;
int pointY = r.top + point.Y;
SetTrackPosition(pointX, pointY);
SetTool(window, text, TipInfo.Type.Absolute, new Point(pointX, pointY));
StartTimer(window, duration);
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> with the specified control and displays it.
/// </summary>
public void Show(string? text, IWin32Window window, int x, int y)
{
ArgumentNullException.ThrowIfNull(window);
if (IsWindowActive(window))
{
PInvokeCore.GetWindowRect(Control.GetSafeHandle(window), out var r);
int pointX = r.left + x;
int pointY = r.top + y;
SetTrackPosition(pointX, pointY);
SetTool(window, text, TipInfo.Type.Absolute, new Point(pointX, pointY));
}
}
/// <summary>
/// Associates <see cref="ToolTip"/> with the specified control and displays it.
/// </summary>
public void Show(string? text, IWin32Window window, int x, int y, int duration)
{
ArgumentNullException.ThrowIfNull(window);
ArgumentOutOfRangeException.ThrowIfNegative(duration);
if (IsWindowActive(window))
{
PInvokeCore.GetWindowRect(Control.GetSafeHandle(window), out var r);
int pointX = r.left + x;
int pointY = r.top + y;
SetTrackPosition(pointX, pointY);
SetTool(window, text, TipInfo.Type.Absolute, new Point(pointX, pointY));
StartTimer(window, duration);
}
}
internal void ShowKeyboardToolTip(string? text, IKeyboardToolTip tool, int duration)
{
ArgumentNullException.ThrowIfNull(tool);
ArgumentOutOfRangeException.ThrowIfNegative(duration);
Rectangle toolRectangle = tool.GetNativeScreenRectangle();
// At first, place the tooltip at the middle of the tool (default location).
int pointX = (toolRectangle.Left + toolRectangle.Right) / 2;
int pointY = (toolRectangle.Top + toolRectangle.Bottom) / 2;
var ownerWindow = tool.GetOwnerWindow();
if (ownerWindow is null)
{
return;
}
SetTool(ownerWindow, text, TipInfo.Type.Absolute, new Point(pointX, pointY));
// Then look for a better ToolTip location.
if (TryGetBubbleSize(ownerWindow, out Size bubbleSize))
{
Point optimalPoint = GetOptimalToolTipPosition(tool, toolRectangle, bubbleSize.Width, bubbleSize.Height);
// The optimal point should be used as a tracking position.
pointX = optimalPoint.X;
pointY = optimalPoint.Y;
// Update TipInfo for the tool with optimal position.
if ((tool is Control toolAsControl && _tools.TryGetValue(toolAsControl, out TipInfo? tipInfo)) ||
(ownerWindow is Control ownerWindowAsControl && _tools.TryGetValue(ownerWindowAsControl, out tipInfo)))
{
tipInfo.Position = new Point(pointX, pointY);
}
// Ensure that the tooltip bubble is moved to the optimal position even when a mouse tooltip is being
// replaced with a keyboard tooltip.
Reposition(optimalPoint, bubbleSize);
}
SetTrackPosition(pointX, pointY);
IsActivatedByKeyboard = true;
if (!IsPersistent)
{
StartTimer(ownerWindow, duration);
}
}
private bool TryGetBubbleSize(IWin32Window ownerWindow, out Size bubbleSize)
{
// Get bubble size to use it for optimal position calculation. Requesting the bubble
// size will AV if there isn't a current tool window.
HWND hwnd = GetCurrentToolHwnd();
LRESULT result = default;
if (!hwnd.IsNull)
{
ToolInfoWrapper<HandleRef<HWND>> info = new(Control.GetSafeHandle(ownerWindow));
result = info.SendMessage(this, PInvoke.TTM_GETBUBBLESIZE);
}
if (result == 0)
{
bubbleSize = Size.Empty;
return false;
}
bubbleSize = new Size(result.LOWORD, result.HIWORD);
return true;
}
private Point GetOptimalToolTipPosition(IKeyboardToolTip tool, Rectangle toolRectangle, int width, int height)
{
// Possible tooltip locations are tied to the tool rectangle bounds.
int centeredX = toolRectangle.Left + toolRectangle.Width / 2 - width / 2; // tooltip will be aligned with tool vertically
int centeredY = toolRectangle.Top + toolRectangle.Height / 2 - height / 2; // tooltip will be aligned with tool horizontally
Rectangle[] possibleLocations =
[
new Rectangle(centeredX, toolRectangle.Top - height, width, height),
new Rectangle(toolRectangle.Right, centeredY, width, height),
new Rectangle(centeredX, toolRectangle.Bottom, width, height),
new Rectangle(toolRectangle.Left - width, centeredY, width, height),
];
// Neighboring tools should not be overlapped (ideally) by tooltip.
IList<Rectangle> neighboringToolsRectangles = tool.GetNeighboringToolsRectangles();
// Weights are used to determine which one of the possible location overlaps least area of the neighboring tools.
long[] locationWeights = new long[LocationTotal];
// Check if the possible locations intersect with the neighboring tools.
for (int i = 0; i < possibleLocations.Length; i++)
{
foreach (Rectangle neighboringToolRectangle in neighboringToolsRectangles)
{
Rectangle intersection = Rectangle.Intersect(possibleLocations[i], neighboringToolRectangle);
checked
{
locationWeights[i] += Math.Abs((long)intersection.Width * intersection.Height); // Intersection is a weight
}
}
}
// Calculate clipped area of possible locations i.e. area which is located outside the screen area.
Rectangle screenBounds = SystemInformation.VirtualScreen;
long[] locationClippedAreas = new long[LocationTotal];
for (int i = 0; i < possibleLocations.Length; i++)
{
Rectangle locationAreaWithinScreen = Rectangle.Intersect(screenBounds, possibleLocations[i]);
checked
{
locationClippedAreas[i] = (Math.Abs((long)possibleLocations[i].Width) - Math.Abs((long)locationAreaWithinScreen.Width))
* (Math.Abs((long)possibleLocations[i].Height) - Math.Abs((long)locationAreaWithinScreen.Height));
}
}
// Calculate area of possible locations within top level control rectangle
long[] locationWithinTopControlAreas = new long[LocationTotal];
Rectangle topContainerBounds = ((IKeyboardToolTip?)TopLevelControl)?.GetNativeScreenRectangle() ?? Rectangle.Empty;
if (!topContainerBounds.IsEmpty)
{
for (int i = 0; i < possibleLocations.Length; i++)
{
Rectangle locationWithinTopControlRectangle = Rectangle.Intersect(topContainerBounds, possibleLocations[i]);
checked
{
locationWithinTopControlAreas[i] = Math.Abs((long)locationWithinTopControlRectangle.Height * locationWithinTopControlRectangle.Width);
}
}
}
// Pick optimal location.
long leastWeight = locationWeights[0];
long leastClippedArea = locationClippedAreas[0];
long biggestAreaWithinTopControl = locationWithinTopControlAreas[0];
int locationIndex = 0;
Rectangle optimalLocation = possibleLocations[0];
bool rtlEnabled = tool.HasRtlModeEnabled();
for (int i = 1; i < possibleLocations.Length; i++)
{
if (IsCompetingLocationBetter(leastClippedArea, leastWeight, biggestAreaWithinTopControl, locationIndex,
locationClippedAreas[i], locationWeights[i], locationWithinTopControlAreas[i], i,
rtlEnabled))
{
leastWeight = locationWeights[i];
leastClippedArea = locationClippedAreas[i];
biggestAreaWithinTopControl = locationWithinTopControlAreas[i];
locationIndex = i;
optimalLocation = possibleLocations[i];
}
}
return new Point(optimalLocation.Left, optimalLocation.Top);
}
private static bool IsCompetingLocationBetter(
long originalLocationClippedArea,
long originalLocationWeight,
long originalLocationAreaWithinTopControl,
int originalIndex,
long competingLocationClippedArea,
long competingLocationWeight,
long competingLocationAreaWithinTopControl,
int competingIndex,
bool rtlEnabled)
{
if (competingLocationClippedArea < originalLocationClippedArea)
{
// Prefer location with less clipped area.
return true;
}
else if (competingLocationWeight < originalLocationWeight)
{
// Otherwise prefer location with less weight.
return true;
}
else if (competingLocationWeight == originalLocationWeight && competingLocationClippedArea == originalLocationClippedArea)
{
// Prefer locations located within top level control.
if (competingLocationAreaWithinTopControl > originalLocationAreaWithinTopControl)
{
return true;
}
else if (competingLocationAreaWithinTopControl == originalLocationAreaWithinTopControl)
{
switch (originalIndex)
{
case LocationIndexTop:
// Top location is the least preferred location.
return true;
case LocationIndexBottom:
// Right and Left locations are preferred instead of Bottom location.
if (competingIndex is LocationIndexLeft or LocationIndexRight)
{
return true;
}
break;
case LocationIndexRight:
// When RTL is enabled Left location is preferred.
if (rtlEnabled && competingIndex == LocationIndexLeft)
{
return true;
}
break;
case LocationIndexLeft:
// When RTL is disabled Right location is preferred.
if (!rtlEnabled && competingIndex == LocationIndexRight)
{
return true;
}
break;
default:
throw new NotSupportedException("Unsupported location index value");
}
}
}
return false;
}
/// <summary>
/// Private Function to encapsulate TTM_TRACKPOSITION so that this doesn't fire an extra POP event
/// </summary>
private void SetTrackPosition(int pointX, int pointY)
{
try
{
_trackPosition = true;
PInvokeCore.SendMessage(this, PInvoke.TTM_TRACKPOSITION, 0, PARAM.FromLowHigh(pointX, pointY));
}
finally
{
_trackPosition = false;
}
}
/// <summary>
/// Hides <see cref="ToolTip"/> with the specified control.
/// </summary>
public void Hide(IWin32Window win)
{
ArgumentNullException.ThrowIfNull(win);
if (_window is null)
{
return;
}
if (win is Control control && control.IsDisposed)
{
Debug.Fail("The passed in control is disposed.");
return;
}
if (GetHandleCreated())
{
ToolInfoWrapper<HandleRef<HWND>> info = new(Control.GetSafeHandle(win));
info.SendMessage(this, PInvoke.TTM_TRACKACTIVATE);
info.SendMessage(this, PInvoke.TTM_DELTOOLW);
}
StopTimer();
// Check if the passed in IWin32Window is a Control.
if (win is not Control tool)
{
_owners.Remove((HWND)win.Handle);
}
else
{
if (_tools.ContainsKey(tool))
{
SetToolInfo(tool, GetToolTip(tool));
}
else
{
_owners.Remove((HWND)win.Handle);
}
// Find the Form for associated Control and hook up to the Deactivated event
// to hide the shown tooltip.
Form? baseFrom = tool.FindForm();
if (baseFrom is not null)
{
baseFrom.Deactivate -= BaseFormDeactivate;
}
}
// Clear off the toplevel control.
ClearTopLevelControlEvents();
_topLevelControl = null;
IsActivatedByKeyboard = false;
}
private void BaseFormDeactivate(object? sender, EventArgs e)
{
HideAllToolTips();
KeyboardToolTipStateMachine.Instance.NotifyAboutFormDeactivation(this);
}
private void HideAllToolTips()
{
Control[] controls = [.. _owners.Values];
for (int i = 0; i < controls.Length; i++)
{
Hide(controls[i]);
}
}
private void SetTool(IWin32Window window, string? text, TipInfo.Type type, Point position)
{
Control? tool = window as Control;
if (tool is not null && _tools.ContainsKey(tool))
{
ToolInfoWrapper<Control> toolInfo = new(tool);
if (toolInfo.SendMessage(this, PInvoke.TTM_GETTOOLINFOW) != IntPtr.Zero)
{
TOOLTIP_FLAGS flags = TOOLTIP_FLAGS.TTF_TRACK;
if (type is TipInfo.Type.Absolute or TipInfo.Type.SemiAbsolute)
{
flags |= TOOLTIP_FLAGS.TTF_ABSOLUTE;
}
toolInfo.Info.uFlags |= flags;
toolInfo.Text = text;
}
if (!_tools.TryGetValue(tool, out TipInfo? tipInfo))
{
tipInfo = new TipInfo(text, type);
}
else
{
tipInfo.TipType |= type;
tipInfo.Caption = text;
}
tipInfo.Position = position;
_tools[tool] = tipInfo;
IntPtr result = toolInfo.SendMessage(this, PInvoke.TTM_SETTOOLINFOW);
result = toolInfo.SendMessage(this, PInvoke.TTM_TRACKACTIVATE, true);
}
else
{
Hide(window);
// Need to do this BEFORE we call GetWinTOOLINFO, since it relies on the tools array to be populated
// in order to find the top level parent.
if (window is Control windowAsControl)
{
if (!_tools.TryGetValue(windowAsControl, out TipInfo? tipInfo))
{
tipInfo = new TipInfo(text, type);
}
else
{
tipInfo.TipType |= type;
tipInfo.Caption = text;
}
tipInfo.Position = position;
_tools[windowAsControl] = tipInfo;
_owners[windowAsControl.HWND] = windowAsControl;
}
var toolInfo = GetWinTOOLINFO(window);
toolInfo.Info.uFlags |= TOOLTIP_FLAGS.TTF_TRACK;
if (type is TipInfo.Type.Absolute or TipInfo.Type.SemiAbsolute)
{
toolInfo.Info.uFlags |= TOOLTIP_FLAGS.TTF_ABSOLUTE;
}
toolInfo.Text = text;
IntPtr result = toolInfo.SendMessage(this, PInvoke.TTM_ADDTOOLW);
result = toolInfo.SendMessage(this, PInvoke.TTM_TRACKACTIVATE, true);
}
if (tool is not null)
{
// Lets find the Form for associated Control
// and hook up to the Deactivated event to Hide the Shown tooltip.
Form? baseFrom = tool.FindForm();
if (baseFrom is not null)
{
baseFrom.Deactivate += BaseFormDeactivate;
}
}
}
/// <summary>
/// Starts the timer hiding Positioned ToolTips.
/// </summary>
private void StartTimer(IWin32Window owner, int interval)
{
if (_timer is null)
{
_timer = new ToolTipTimer(owner);
// Add the timer handler
_timer.Tick += TimerHandler;
}
_timer.Interval = interval;
_timer.Start();
}
/// <summary>
/// Stops the timer for hiding Positioned ToolTips.
/// </summary>
protected void StopTimer()
{
// Hold a local ref to timer so that a posted message doesn't null this out during disposal.
ToolTipTimer? timerRef = _timer;
if (timerRef is not null)
{
timerRef.Stop();
timerRef.Dispose();
_timer = null;
}
}
/// <summary>
/// Generates updown events when the timer calls this function.
/// </summary>
private void TimerHandler(object? source, EventArgs args)
{
Hide(((ToolTipTimer)source!).Host);
}
/// <summary>
/// Finalizes garbage collection.
/// </summary>
~ToolTip() => DestroyHandle();
/// <summary>
/// Returns a string representation for this control.
/// </summary>
public override string ToString()
{
string s = base.ToString();
return $"{s} InitialDelay: {InitialDelay}, ShowAlways: {ShowAlways}";
}
private void Reposition(Point tipPosition, Size tipSize)
{
Point moveToLocation = tipPosition;
Screen screen = Screen.FromPoint(moveToLocation);
// Re-adjust the X position of the tool tip if it bleeds off the screen working area
if (moveToLocation.X + tipSize.Width > screen.WorkingArea.Right)
{
moveToLocation.X = screen.WorkingArea.Right - tipSize.Width;
}
if (moveToLocation.X < screen.WorkingArea.Left)
{
moveToLocation.X = screen.WorkingArea.Left;
}
// re-adjust the Y position of the tool tip if it bleeds off the screen working area.
if (moveToLocation.Y + tipSize.Height > screen.WorkingArea.Bottom)
{
moveToLocation.Y = screen.WorkingArea.Bottom - tipSize.Height;
}
PInvoke.SetWindowPos(
this,
HWND.HWND_TOPMOST,
moveToLocation.X,
moveToLocation.Y,
tipSize.Width,
tipSize.Height,
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOOWNERZORDER);
}
private HWND GetCurrentToolHwnd()
{
ToolInfoWrapper<Control> toolInfo = default;
if (toolInfo.SendMessage(this, PInvoke.TTM_GETCURRENTTOOLW) != 0)
{
return toolInfo.Info.hwnd;
}
return default;
}
private Control? GetCurrentToolWindow()
{
HWND hwnd = GetCurrentToolHwnd();
return _owners.TryGetValue(hwnd, out Control? control) ? control : Control.FromHandle(hwnd);
}
/// <summary>
/// Handles the WM_MOVE message.
/// </summary>
private void WmMove()
{
IWin32Window? window = GetCurrentToolWindow();
if (window is not Control windowAsControl || !_tools.TryGetValue(windowAsControl, out TipInfo? tipInfo))
{
return;
}
// Reposition the tooltip when its about to be shown since the tooltip can go out of screen
// working area bounds Reposition would check the bounds for us.
PInvokeCore.GetWindowRect(this, out var rectangle);
if (tipInfo.Position != Point.Empty)
{
Reposition(tipInfo.Position, rectangle.Size);
}
}
/// <summary>
/// Handles the WM_MOUSEACTIVATE message.
/// </summary>
private void WmMouseActivate(ref Message message)
{
IWin32Window? window = GetCurrentToolWindow();
if (window is null)
{
return;
}
PInvokeCore.GetWindowRect(Control.GetSafeHandle(window), out var r);
Point cursorLocation = Cursor.Position;
// Do not activate the mouse if its within the bounds of the
// the associated tool.
if (cursorLocation.X >= r.left && cursorLocation.X <= r.right &&
cursorLocation.Y >= r.top && cursorLocation.Y <= r.bottom)
{
message.ResultInternal = (LRESULT)(nint)PInvoke.MA_NOACTIVATE;
}
}
/// <summary>
/// Handles the WM_WINDOWFROMPOINT message.
/// </summary>
private unsafe void WmWindowFromPoint(ref Message message)
{
var lpPoint = (Point*)(nint)message.LParamInternal;
bool result = false;
message.ResultInternal = (LRESULT)(nint)GetWindowFromPoint(*lpPoint, ref result);
}
/// <summary>
/// Handles the TTN_SHOW message.
/// </summary>
private void WmShow()
{
IWin32Window? window = GetCurrentToolWindow();
if (window is null)
{
return;
}
// Get the bounds.
PInvokeCore.GetWindowRect(this, out var rect);
Control? toolControl = window as Control;
Size currentTooltipSize = rect.Size;
PopupEventArgs e = new(window, toolControl, IsBalloon, currentTooltipSize);
OnPopup(e);
if (toolControl is DataGridView dataGridView && dataGridView.CancelToolTipPopup(this))
{
// The dataGridView cancelled the tooltip.
e.Cancel = true;
}
if (!e.Cancel)
{
// Use GetWindowText instead of GetCaptionForTool to retrieve the actual caption.
// GetCaptionForTool doesn't work correctly when the text for a tool is retrieved
// by TTN_NEEDTEXT notification (e.g. TabPages of TabControl).
AnnounceText(toolControl, PInvokeCore.GetWindowText(this));
}
// We need to re-get the rectangle of the tooltip here because
// any of the tooltip attributes/properties could have been updated
// during the popup event; in which case the size of the tooltip is
// affected. e.ToolTipSize is respected over rect.Size
PInvokeCore.GetWindowRect(this, out rect);
currentTooltipSize = (e.ToolTipSize == currentTooltipSize) ? rect.Size : e.ToolTipSize;
if (IsBalloon)
{
// Get the text display rectangle
PInvokeCore.SendMessage(this, PInvoke.TTM_ADJUSTRECT, (WPARAM)(BOOL)true, ref rect);
if (rect.Height > currentTooltipSize.Height)
{
currentTooltipSize.Height = rect.Height;
}
}
// Set the max possible size of the tooltip to the size we received.
// This prevents the operating system from drawing incorrect rectangles
// when determining the correct display rectangle
// Set the MaxWidth only if user has changed the width.
if (currentTooltipSize != rect.Size)
{
Screen screen = Screen.FromPoint(Cursor.Position);
int maxwidth = (IsBalloon)
? Math.Min(currentTooltipSize.Width - 2 * BalloonOffsetX, screen.WorkingArea.Width)
: Math.Min(currentTooltipSize.Width, screen.WorkingArea.Width);
PInvokeCore.SendMessage(this, PInvoke.TTM_SETMAXTIPWIDTH, 0, maxwidth);
}
if (e.Cancel)
{
_cancelled = true;
PInvoke.SetWindowPos(
this,
HWND.HWND_TOPMOST,
0, 0, 0, 0,
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOOWNERZORDER);
}
else
{
_cancelled = false;
// Only width/height changes are respected, so set top,left to what we got earlier
PInvoke.SetWindowPos(
this,
HWND.HWND_TOPMOST,
rect.left,
rect.top,
currentTooltipSize.Width,
currentTooltipSize.Height,
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOOWNERZORDER);
}
}
/// <summary>
/// Handles the WM_WINDOWPOSCHANGED message.
/// We need to Hide the window since the native tooltip actually calls SetWindowPos in its TTN_SHOW even if we cancel showing the
/// tooltip. Hence we need to listen to the WindowPosChanged message can hide the window ourselves.
/// </summary>
private bool WmWindowPosChanged()
{
if (_cancelled)
{
PInvoke.ShowWindow(this, SHOW_WINDOW_CMD.SW_HIDE);
return true;
}
return false;
}
/// <summary>
/// Handles the WM_WINDOWPOSCHANGING message.
/// </summary>
private unsafe void WmWindowPosChanging(ref Message message)
{
if (_cancelled || _isDisposing)
{
return;
}
WINDOWPOS* wp = (WINDOWPOS*)(nint)message.LParamInternal;
Cursor? currentCursor = Cursor.Current;
Point cursorPos = Cursor.Position;
if (GetCurrentToolWindow() is not { } window)
{
message.ResultInternal = (LRESULT)0;
return;
}
TipInfo? tipInfo = null;
if (window is Control windowAsControl && !_tools.TryGetValue(windowAsControl, out tipInfo))
{
return;
}
// Treeview handles its own ToolTips.
if (window is TreeView treeView && treeView.ShowNodeToolTips)
{
return;
}
if (IsBalloon)
{
wp->cx += 2 * BalloonOffsetX;
return;
}
if ((tipInfo!.TipType & TipInfo.Type.Auto) != 0 && _window is not null)
{
_window.DefWndProc(ref message);
return;
}
if (((tipInfo.TipType & TipInfo.Type.SemiAbsolute) != 0) && tipInfo.Position == Point.Empty)
{
Screen screen = Screen.FromPoint(cursorPos);
if (currentCursor is not null)
{
wp->x = cursorPos.X;
wp->y = cursorPos.Y;
if (wp->y + wp->cy + currentCursor.Size.Height - currentCursor.HotSpot.Y > screen.WorkingArea.Bottom)
{
wp->y = cursorPos.Y - wp->cy;
}
else
{
wp->y = cursorPos.Y + currentCursor.Size.Height - currentCursor.HotSpot.Y;
}
}
if (wp->x + wp->cx > screen.WorkingArea.Right)
{
wp->x = screen.WorkingArea.Right - wp->cx;
}
}
else if ((tipInfo.TipType & TipInfo.Type.SemiAbsolute) != 0 && tipInfo.Position != Point.Empty)
{
Screen screen = Screen.FromPoint(tipInfo.Position);
wp->x = tipInfo.Position.X;
if (wp->x + wp->cx > screen.WorkingArea.Right)
{
wp->x = screen.WorkingArea.Right - wp->cx;
}
wp->y = tipInfo.Position.Y;
if (wp->y + wp->cy > screen.WorkingArea.Bottom)
{
wp->y = screen.WorkingArea.Bottom - wp->cy;
}
}
message.ResultInternal = (LRESULT)0;
}
/// <summary>
/// Called just before the tooltip is hidden.
/// </summary>
private void WmPop()
{
if (GetCurrentToolWindow() is not IWin32Window window)
{
return;
}
if (window is not Control control || !_tools.TryGetValue(control, out TipInfo? tipInfo))
{
return;
}
// Must reset the maxwidth to the screen size.
if ((tipInfo.TipType & TipInfo.Type.Auto) != 0 || (tipInfo.TipType & TipInfo.Type.SemiAbsolute) != 0)
{
Screen screen = Screen.FromPoint(Cursor.Position);
PInvokeCore.SendMessage(this, PInvoke.TTM_SETMAXTIPWIDTH, 0, screen.WorkingArea.Width);
}
// For non-auto tips (those shown through the show(.) methods, we need to
// disassociate them from the tip control.
if ((tipInfo.TipType & TipInfo.Type.Auto) == 0)
{
_tools.Remove(control);
_owners.Remove((HWND)window.Handle);
control.HandleCreated -= HandleCreated;
control.HandleDestroyed -= HandleDestroyed;
_created.Remove(control);
if (_originalPopupDelay != 0)
{
AutoPopDelay = _originalPopupDelay;
_originalPopupDelay = 0;
}
}
else
{
// Clear all other flags except for the Auto flag to ensure automatic tips can still show
tipInfo.TipType = TipInfo.Type.Auto;
tipInfo.Position = Point.Empty;
_tools[control] = tipInfo;
}
}
private unsafe void WndProc(ref Message message)
{
switch (message.Msg)
{
case (int)(MessageId.WM_REFLECT_NOTIFY):
var nmhdr = (NMHDR*)(nint)message.LParamInternal;
if (nmhdr->code == PInvoke.TTN_SHOW && !_trackPosition)
{
WmShow();
}
else if (nmhdr->code == PInvoke.TTN_POP)
{
WmPop();
_window?.DefWndProc(ref message);
}
break;
case (int)PInvokeCore.WM_WINDOWPOSCHANGING:
WmWindowPosChanging(ref message);
break;
case (int)PInvokeCore.WM_WINDOWPOSCHANGED:
if (!WmWindowPosChanged() && _window is not null)
{
_window.DefWndProc(ref message);
}
break;
case (int)PInvokeCore.WM_MOUSEACTIVATE:
WmMouseActivate(ref message);
break;
case (int)PInvokeCore.WM_MOVE:
WmMove();
break;
case (int)PInvoke.TTM_WINDOWFROMPOINT:
WmWindowFromPoint(ref message);
break;
case (int)PInvokeCore.WM_PRINTCLIENT:
goto case (int)PInvokeCore.WM_PAINT;
case (int)PInvokeCore.WM_PAINT:
if (OwnerDraw && !_isBalloon && !_trackPosition)
{
using BeginPaintScope paintScope = new(HWND);
Rectangle bounds = paintScope.PaintRectangle;
if (bounds == Rectangle.Empty)
{
return;
}
using Graphics graphics = paintScope.HDC.CreateGraphics();
if (GetCurrentToolWindow() is IWin32Window window)
{
Font font;
try
{
font = Font.FromHfont(PInvokeCore.SendMessage(this, PInvokeCore.WM_GETFONT));
}
catch (ArgumentException)
{
// If the current default tooltip font is a non-TrueType font, then
// Font.FromHfont throws this exception, so fall back to the default control font.
font = Control.DefaultFont;
}
Control? control = window as Control ?? Control.FromHandle(window.Handle);
OnDraw(
new DrawToolTipEventArgs(
graphics,
window,
control,
bounds,
GetToolTip(control),
BackColor,
ForeColor,
font));
break;
}
}
// If not OwnerDraw, fall through.
goto default;
default:
_window?.DefWndProc(ref message);
break;
}
}
}
|