Search code examples
c#winformstooltipmouseleave

Emulate Tooltip functionality with ToolStripDropDown


I created a custom UserControl that displays itself in a ToolStripDropDown to emulate ToolTip functionality. It mostly works fine for any Control when I subscribe to their MouseEnter and MouseLeave events.

I want to also use it for custom objects (not Controls). I've created an interface that defines MouseEnter and MouseLeave events so that I can subscribe any object (such as custom-drawn primitives) to this tooltip. These classes do their own work to determine when to trigger MouseEnter and MouseLeave.

My problem is that when the tooltip is shown, my UserControls which contain the custom objects do not receive MouseMove events, even though the Tooltip is being shown off to the side and not under the mouse. I am generating my own MouseLeave event based on checking in MouseMove if the mouse is no longer over the object in question. But obviously without MouseMove events, MouseLeave never fires.

When I show the tooltip on a Control, the same thing happens (no MouseMove events) except that MouseLeave still fires.

1) How can I emulate this MouseLeave functionality? Do I have to use p/invoke to SetCapture mouse movement, or does anyone know an easier way?

2) When the tooltip shows, even though neither the ToolStripDropDown or my UserControl inside it fire a "GotFocus" event, I still lose keyboard focus as long as the tooltip is shown which is also not desirable tooltip behavior. Can I avoid that?

Basically I want it to be a completely non-focusable, non-interfering tooltip. I have looked at a sample project called SuperTooltip but it had the same flawed functionality. I have tried setting ControlStyles.Selectable to false and did not notice any change.

Here is the code where I create my tooltip UserControl:

public CustomTooltip()
{
    this.SetStyle(ControlStyles.Selectable, false);

    dropDown = new ToolStripDropDown();
    dropDown.AutoSize = false;
    dropDown.Margin = Padding.Empty;
    dropDown.Padding = Padding.Empty;

    host = new ToolStripControlHost(this);
    host.AutoSize = false;
    host.Margin = Padding.Empty;
    host.Padding = Padding.Empty;

    this.Location = new Point(0, 0);
    dropDown.Items.Add(host);
}

And I show it with:

dropDown.Show(
    new Point(Cursor.Position.X, Cursor.Position.Y + Cursor.Current.Size.Height),
    ToolStripDropDownDirection.BelowRight
);

Solution

  • I found I was able to accomplish this by deriving from Form and not using ToolStripDropDown at all. This class emulates a tooltip's functionality and allows custom fade in/fade out parameters. You can either subscribe a control or any class that defines and implements ITooltipTarget for MouseEnter and MouseLeave.

    public abstract class CustomTooltip : Form
    {
        #region Static
        protected static readonly int FadeInterval = 25;
        protected static readonly IntPtr HWND_TOPMOST = (IntPtr)(-1);
        private const int SWP_NOSIZE = 0x0001;
        private const int SWP_NOMOVE = 0x0002;
        private const int SWP_NOACTIVATE = 0x0010;
        private const int WS_POPUP = unchecked((int)0x80000000);
        private const int WS_EX_TOPMOST = 0x00000008;
        private const int WS_EX_NOACTIVATE = 0x08000000;
    
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
        #endregion
    
        protected Dictionary<object, object> subscriptions;
        protected Timer popupTimer;
        protected Timer fadeTimer;
        protected bool isFading = false;
        protected int fadeDirection = 1;
    
        [DefaultValue(500)]
        /// <summary>
        /// Delay in milliseconds before the tooltip is shown.  0 means no delay.
        /// </summary>
        public int PopupDelay
        {
            get
            {
                return _popupDelay;
            }
            set
            {
                _popupDelay = value;
    
                if (value > 0)
                    popupTimer.Interval = value;
                else
                    popupTimer.Interval = 1;
            }
        }
        private int _popupDelay = 500;
    
        [DefaultValue(0)]
        /// <summary>
        /// How long to spend fading in and out in milliseconds.  0 means no fade.
        /// </summary>
        public int FadeTime
        {
            get
            {
                return _fadeTime;
            }
            set
            {
                _fadeTime = value;
            }
        }
        private int _fadeTime = 0;
    
        public virtual new object Tag
        {
            get
            {
                return base.Tag;
            }
            set
            {
                base.Tag = value;
    
                OnTagChanged(EventArgs.Empty);
            }
        }
    
        public CustomTooltip()
        {
            this.SetStyle(ControlStyles.Selectable, false);
    
            subscriptions = new Dictionary<object, object>();
    
            popupTimer = new Timer();
            popupTimer.Interval = PopupDelay;
            popupTimer.Tick += new EventHandler(popupTimer_Tick);
    
            fadeTimer = new Timer();
            fadeTimer.Interval = FadeInterval;
            fadeTimer.Tick += new EventHandler(fadeTimer_Tick);
    
            this.Visible = false;
            this.ShowInTaskbar = false;
            this.FormBorderStyle = FormBorderStyle.None;
            this.ControlBox = false;
            this.StartPosition = FormStartPosition.Manual;
        }
    
        protected override CreateParams CreateParams
        {
            get
            {
                CreateParams cp = base.CreateParams;
    
                cp.Style |= WS_POPUP;
                cp.ExStyle |= WS_EX_TOPMOST | WS_EX_NOACTIVATE;
    
                return cp;
            }
        }
    
        protected override bool ShowWithoutActivation
        {
            get
            {
                return true;
            }
        }
    
        protected virtual void Subscribe(Control control, object tag)
        {
            subscriptions.Add(control, tag);
            control.MouseEnter += new EventHandler(Item_MouseEnter);
            control.MouseLeave += new EventHandler(Item_MouseLeave);
        }
    
        protected virtual void Subscribe(ITooltipTarget item, object tag)
        {
            subscriptions.Add(item, tag);
            item.MouseEnter += new EventHandler(Item_MouseEnter);
            item.MouseLeave += new EventHandler(Item_MouseLeave);
        }
    
        public virtual void Unsubscribe(Control control)
        {
            control.MouseEnter -= new EventHandler(Item_MouseEnter);
            control.MouseLeave -= new EventHandler(Item_MouseLeave);
            subscriptions.Remove(control);
        }
    
        public virtual void Unsubcribe(ITooltipTarget item)
        {
            item.MouseEnter -= new EventHandler(Item_MouseEnter);
            item.MouseLeave -= new EventHandler(Item_MouseLeave);
            subscriptions.Remove(item);
        }
    
        public void ClearSubscriptions()
        {
            foreach (object o in subscriptions.Keys)
            {
                if (o is Control)
                    Unsubscribe((Control)o);
                else if (o is ITooltipTarget)
                    Unsubscribe((ITooltipTarget)o);
            }
        }
    
        protected virtual void OnTagChanged(EventArgs e)
        {
        }
    
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
        }
    
        protected override void OnMouseEnter(EventArgs e)
        {
            base.OnMouseEnter(e);
    
            Item_MouseLeave(null, EventArgs.Empty);
        }
    
        private void Item_MouseEnter(object sender, EventArgs e)
        {
            Tag = subscriptions[sender];
            popupTimer.Start();
        }
    
        private void Item_MouseLeave(object sender, EventArgs e)
        {
            if (FadeTime > 0)
                FadeOut();
            else
                this.Hide();
    
            popupTimer.Stop();
        }
    
        protected virtual void FadeIn()
        {
            isFading = true;
            Opacity = 0;
            fadeDirection = 1;
            fadeTimer.Start();
        }
    
        protected virtual void FadeOut()
        {
            isFading = true;
            Opacity = 1;
            fadeDirection = -1;
            fadeTimer.Start();
        }
    
        private void popupTimer_Tick(object sender, EventArgs e)
        {
            if (isFading)
                this.Hide();
    
            if (FadeTime > 0)
                FadeIn();
    
            Location = new Point(Cursor.Position.X, Cursor.Position.Y + Cursor.Size.Height);
            SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
            Show();
    
            popupTimer.Stop();
        }
    
        private void fadeTimer_Tick(object sender, EventArgs e)
        {
            if (Opacity == 0 && fadeDirection == -1)
            {
                isFading = false;
                fadeTimer.Stop();
                this.Hide();
            }
            else if (Opacity == 1 && fadeDirection == 1)
            {
                fadeTimer.Stop();
                isFading = false;
            }
            else
            {
                double change = ((double)fadeTimer.Interval / (double)FadeTime) * (double)fadeDirection;
                Opacity += change;
            }
        }
    }
    
    public interface ITooltipTarget
    {
        event EventHandler MouseEnter;
        event EventHandler MouseLeave;
    }
    

    To use the above classes, you just need to derive from CustomTooltip to make your own custom drawn tooltip. The derived class would use the Tag property to determine the content displayed. For example, if I want a tooltip that associates an Image with an object and draws that image, I'd do something like:

    public class CustomImageTooltip : CustomTooltip
    {
         public Image Image
         {
            get
            {
                if (Tag is Image)
                    return Tag as Image;
                else
                    return null;
            }
         }
    
        public CustomImageTooltip()
        {
            InitializeComponent();
    
            this.SetStyle(ControlStyles.DoubleBuffer |
                          ControlStyles.AllPaintingInWmPaint |
                          ControlStyles.UserPaint, true);
        }
    
        public void Subscribe(Control control, Image image)
        {
            base.Subscribe(control, image);
        }
    
        public void Subscribe(ITooltipTarget item, Image image)
        {
            base.Subscribe(item, image);
        }
    
        protected override void OnTagChanged(EventArgs e)
        {
            base.OnTagChanged(e);
            if (Image != null)
                this.Size = Image.Size;
        }
    
        protected override void OnPaint(PaintEventArgs e)
        {
            Graphics g = e.Graphics;
    
            g.Clear(Color.White);
    
            if (Image != null)
                g.DrawImage(
                    Image,
                    new RectangleF(0, 0, ClientSize.Width, ClientSize.Height),
                    new RectangleF(0, 0, Image.Size.Width, Image.Size.Height),
                    GraphicsUnit.Pixel
                );
    
            g.DrawRectangle(Pens.Black, 0, 0, ClientRectangle.Width - 1, ClientRectangle.Height - 1);
        }
    }
    

    And in order to use this CustomImageTooltip class in your application, you'd need to just subscribe and unsubscribe to a single instance of the class:

    // Constructor
    customImageTooltip = new CustomImageTooltip();
    
    foreach (CustomObject o in myCustomObjects)
    {
        customImageTooltip.Subscribe(o, o.Image);
    }
    
    // Destructor
    foreach (CustomObject o in myCustomObjects)
    {
        customImageTooltip.Unsubscribe(o);
    }