Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ToolTip.Popup event not firing with ToolTip.Show() in .NET Framework

I have an old WinForms application with a normal TreeView, where I need to show customized tooltips for individual nodes.

I am using a ToolTip component (OwnerDraw=true and IsBalloon=false), and I show the tooltip programmatically with ToolTip.Show() from the MouseMove event of the TreeView. The Popup event handler (correctly) calculates and sets the tooltip size, and the Draw event handler (correctly) draws the tooltip.

My problem is that the Popup event does not fire - only the Draw event, resulting in that the displayed tooltip is much too small for the contents (I need to use a large fixed-width font: Consolas, 12).

I have already asked a question about this, thinking that I was doing something wrong, but I have now found that this works fine (for me) with .NET 8, but not with .NET Framework. I have tested .NET Framework 4.8 (which is what I have to use), 4.7.2, and 4.6.2 - the Popup event is not triggered with any of them.

From the .NET documentation on the ToolTip.Popup event (this is for 4.6.2, but it is the same for the others):

The Popup event is raised whenever a ToolTip is displayed, either through an explicit call to one of the Show methods or when the ToolTip class implicitly displays a ToolTip.

That part about it being raised for "an explicit call to one of the Show methods" does not seem to be true, or am I missing something?

I am not allowed to show my source code, but in his answer to my previous question, @Olivier Jacot-Descombes included the following code, which is very much like how I am doing it, and this is also the code I have been using for my tests since then:

public partial class FormTreeNodeToolTip : Form
{
    private static readonly Font _font = new Font("Consolas", 16, FontStyle.Regular);
    private TreeNode _currentNode;

    public FormTreeNodeToolTip()
    {
        InitializeComponent();

        treeView1.Nodes.Add(new TreeNode("First node"));
        treeView1.Nodes.Add(new TreeNode("Second node with longer text"));
        treeView1.Nodes.Add(new TreeNode("Third node"));
    }

    private void ToolTip1_Draw(object sender, DrawToolTipEventArgs e)
    {
        e.Graphics.Clear(Color.LightYellow);
        e.Graphics.DrawString(e.ToolTipText, _font, Brushes.Blue, e.Bounds);
    }

    private void TreeView1_NodeMouseHover(object sender, TreeNodeMouseHoverEventArgs e)
    {
        _currentNode = e.Node;
        Point cursor = treeView1.PointToClient(Cursor.Position);
        toolTip1.Show(e.Node.Text, treeView1, cursor.X + 20, cursor.Y + 20, 800);
    }

    private void ToolTip1_Popup(object sender, PopupEventArgs e)
    {
        e.ToolTipSize = TextRenderer.MeasureText(_currentNode.Text, _font);
    }

    private void TreeView1_MouseMove(object sender, MouseEventArgs e)
    {
        if (treeView1.GetNodeAt(e.Location) != _currentNode) {
            _currentNode = null;
            toolTip1.Hide(treeView1);
        }
    }

    private void TreeView1_MouseLeave(object sender, EventArgs e)
    {
        toolTip1.Hide(treeView1);
    }
}

I should add that I have tried using different events for showing the tooltip, setting ToolTip.Active to false and back to true when hiding/showing a tooltip, actively removing the "old" tooltip with ToolTip.SetToolTip() and ToolTip.RemoveAll(), and similar. Nothing I do makes any difference - the Popup event still does not fire.

So, is this a bug in .NET Framework, or am I missing something? What (if anything) can I do to make this work?

UPDATE 1:

Adding the form's designer code on request:

private void InitializeComponent()
{
    this.components = new System.ComponentModel.Container();
    this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
    this.treeView1 = new System.Windows.Forms.TreeView();
    this.SuspendLayout();
    // 
    // toolTip1
    // 
    this.toolTip1.OwnerDraw = true;
    this.toolTip1.Draw += new System.Windows.Forms.DrawToolTipEventHandler(this.ToolTip1_Draw);
    this.toolTip1.Popup += new System.Windows.Forms.PopupEventHandler(this.ToolTip1_Popup);
    // 
    // treeView1
    // 
    this.treeView1.Location = new System.Drawing.Point(0, 0);
    this.treeView1.Name = "treeView1";
    this.treeView1.Size = new System.Drawing.Size(306, 231);
    this.treeView1.TabIndex = 0;
    this.treeView1.NodeMouseHover += new System.Windows.Forms.TreeNodeMouseHoverEventHandler(this.TreeView1_NodeMouseHover);
    this.treeView1.MouseLeave += new System.EventHandler(this.TreeView1_MouseLeave);
    this.treeView1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.TreeView1_MouseMove);
    // 
    // Form1
    // 
    this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(710, 304);
    this.Controls.Add(this.treeView1);
    this.Name = "Form1";
    this.Text = "Form1";
    this.ResumeLayout(false);

}

UPDATE 2:

Added a few lines to the constructor in the example to add a couple of nodes in the TreeView. I did not think to mention that this simple example just shows the node's caption as tooltip, so the longer the caption string, the more obvious it becomes that the tooltip has an incorrect size.

Clarification:

This is not how the actual application works. There, the multi-line tooltip text for a node is decided based on other stuff, and needs to be assembled at the moment it is displayed.

It must also be displayed with a fixed-width font (we use Consolas 12, the example uses Consolas 16 to make the problem more visible even with short tooltips).

like image 627
Boise Avatar asked Sep 01 '25 03:09

Boise


1 Answers

Solution 1

TreeView natively supports showing tooltip, see TreeView.ShowNodeToolTips and TreeNode.ToolTipText, so you don't need to use an additional tooltip control.

Reason

This may be a bug*, but it was fixed by this PR anyway:

Before: ((tvhip.flags & TVHT.ONITEM) != 0)

After: tvhip.flags.HasFlag(TVHT.ONITEM)

When you show the tooltip, a WM_NOTIFY+TTN_SHOW message will be sent to the TreeView control first. Due to its ability to show tooltip on its own, a method called WmShowToolTip within the TreeView class will also handle this message. The original intention of this method is to detect the flag TVHT_ONITEM (0x46), but the previous code resulted in the flag TVHT_ONITEMLABEL (0x4) also being considered successful. As a result, the message is processed by the TreeView control, so tooltip fails to trigger the Popup event.

*The intention of the WmShowToolTip method may be to move external the tooltip to the top when the user manually shows them on the TreeView to prevent them from being obscured. If that's the case, perhaps it's not a bug and the current one is.

Solution 2

As explained above, in order to pass this message down, you need to rewrite the default WndProc method:

public class MyTreeView : TreeView
{
    protected override void WndProc(ref Message m)
    {
        const int WM_NOTIFY = 0x4e;
        const int WM_REFLECT = 0x2000;
        const int TTN_SHOW = -521;

        if (m.Msg == WM_NOTIFY)
        {
            var nmhdr = (NMHDR)m.GetLParam(typeof(NMHDR));
            if (nmhdr.code == TTN_SHOW)
            {
                m.Result = SendMessage(nmhdr.hwndFrom, WM_REFLECT + WM_NOTIFY, m.WParam, m.LParam);
                return;
            }
        }

        base.WndProc(ref m);
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr SendMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);

    struct NMHDR
    {
        public IntPtr hwndFrom;
        public IntPtr idFrom;
        public int code;
    }
}

Solution 3

Since this issue only exists in the TreeView control, you can choose to show the tooltip on another control. E.G. you can place the treeview in a panel or UserControl (and set Dock = Fill), and then:

toolTip1.Show(e.Node.Text, panel1, cursor.X + 20, cursor.Y + 20, 800);
like image 157
shingo Avatar answered Sep 02 '25 18:09

shingo