【问题标题】:Why aren't balloon tips shown pointing at the correct control?为什么没有显示指向正确控件的气球提示?
【发布时间】:2025-12-22 14:30:06
【问题描述】:

我在表单上使用ToolTip 控件,但发现即使我的光标位于一个控件上,工具提示也会显示在其他地方。我想在我的光标所在的控件中显示这个。

如上图所示,当我的光标在Textbox3 上时,工具提示显示在Textbox4 上。我希望它指向Textbox3

我目前正在使用以下代码在 3 个不同的事件中显示工具提示:

 private void txtImmediateddest_Enter(object sender, EventArgs e)
 {
     ttpDetail.Show("Ex:111000025", txtImmediateddest);
 }

 private void txtImmediateddest_MouseHover(object sender, EventArgs e)
 {
     ttpDetail.Show("Ex:111000025", txtImmediateddest);
 }

  private void txtImmediateddest_MouseUp(object sender, MouseEventArgs e)
  {
      ttpDetail.Show("Ex:111000025", txtImmediateddest, e.Location);
      //toolTipimmeddest.Show("Required & Must be 9 Digits", txtImmediateddest);
  }

编辑

 private void textBox1_MouseHover(object sender, EventArgs e)
    {
        ttpDetail.AutoPopDelay = 2000;
        ttpDetail.InitialDelay = 1000;
        ttpDetail.ReshowDelay = 500;
        ttpDetail.IsBalloon = true;
        //ttpDetail.SetToolTip(textBox1, "Ex:01(Should be Numeric)");
        ttpDetail.Show("Ex : 01(Should Be Numeric)", textBox1,textBox1.Width, textBox1.Height/10,5000);
    }

这工作正常,但是当鼠标最初放在控件上时,如果我第二次显示正常,它会正常显示

看下面的图片

【问题讨论】:

    标签: c# .net winforms textbox tooltip


    【解决方案1】:

    您看到的问题是因为您的ToolTip 控件的IsBalloon property 设置为“True”。设置此属性后,ToolTip 不会更改其相对位置,从而导致气球的箭头指向错误的控件。

    以下是展示这种现象的并排比较:

            

    显然,简单的解决方法是通过将IsBalloon 属性设置为“False”来禁用它。控件将恢复为显示标准的矩形工具提示窗口,该窗口看起来正确对齐。

    如果您无法接受,则必须指定您希望工具提示气球出现的确切位置。不幸的是,ToolTip 控件中似乎存在一个错误,导致它在第一次附加到控件时无法正确显示。这通常可以通过使用空字符串调用一次Show 方法来解决。例如,使用以下代码:

    private void txtImmediateddest_Enter(object sender, EventArgs e)
    {
        ttpDetail.Show(string.Empty, textBox3, 0);
        ttpDetail.Show("Ex:111000025", textBox3, textBox3.Width / 2, textBox3.Height, 5000);
    }
    

    产生这个结果:

      

    当然,走这条路,你的运气也可能会有所不同。我一般不会使用内置的ToolTip 控件进行编辑控件(例如文本框和组合框)。我发现 P/Invoke SendMessage 更可靠,指定 EM_SHOWBALLOONTIPEDITBALLOONTIP structure 包含有关我要显示的工具提示的信息。我将查找适当的定义并编写包装器代码作为读者练习,因为这个答案已经太长了。

    【讨论】:

    • 这显示黑色工具提示而不是消息,并且气球提示也没有显示
    • @Dorababu:“这个”并没有告诉我太多。我的回答中有 3 种不同的可能解决方法:您尝试了哪一种?另外,我知道它们都可以工作,因为我自己测试了它们以截取屏幕截图。您可以使用用于比较目的的代码更新您的问题吗?
    • @Cody Gray 我刚刚使用了你的代码并将 isBallon 设置为 false
    • @Dorababu:如果您使用第二个代码 sn-p,则不必将 IsBalloon 设置为 false。但我不知道为什么它不适合你。我在 Server 2008 R2 上编写并测试了它,然后在 Windows XP VM 上再次测试(截屏)。两种环境中的一切都很稳定。您添加的代码或项目中的其他地方一定有问题。
    • 嘿,小小帮助,我需要在特定文本框的 MouseHover 上使用相同的方法,您可以发布吗
    【解决方案2】:

    经过大量故障排除后,我发现下面的代码比内置气球 ToolTip 更胜一筹。通过取消注释清单文件中的依赖项来确保启用视觉样式。

    在 TextBox 上创建一个 BalloonTip,如下所示:

    new BalloonTip("Title", "Message", textBox1, BalloonTip.ICON.INFO, 5000);
    

    并像这样实现BalloonTip

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    
    namespace Lib.Windows
    {
        class BalloonTip
        {
            private System.Timers.Timer timer = new System.Timers.Timer();
            private SemaphoreSlim semaphore = new SemaphoreSlim(1);
            private IntPtr hWnd;
    
            public BalloonTip(string text, Control control)
            {
                Show("", text, control);
            }
    
            public BalloonTip(string title, string text, Control control, ICON icon = 0, double timeOut = 0, bool focus = false)
            {
                Show(title, text, control, icon, timeOut, focus);
            }
    
            void Show(string title, string text, Control control, ICON icon = 0, double timeOut = 0, bool focus = false, short x = 0, short y = 0)
            {
                if (x == 0 && y == 0)
                {
                    x = (short)(control.RectangleToScreen(control.ClientRectangle).Left + control.Width / 2);
                    y = (short)(control.RectangleToScreen(control.ClientRectangle).Top + control.Height / 2);
                }
                TOOLINFO toolInfo = new TOOLINFO();
                toolInfo.cbSize = (uint)Marshal.SizeOf(toolInfo);
                toolInfo.uFlags = 0x20; // TTF_TRACK
                toolInfo.lpszText = text;
                IntPtr pToolInfo = Marshal.AllocCoTaskMem(Marshal.SizeOf(toolInfo));
                Marshal.StructureToPtr(toolInfo, pToolInfo, false);
                byte[] buffer = Encoding.UTF8.GetBytes(title);
                buffer = buffer.Concat(new byte[] { 0 }).ToArray();
                IntPtr pszTitle = Marshal.AllocCoTaskMem(buffer.Length);
                Marshal.Copy(buffer, 0, pszTitle, buffer.Length);
                hWnd = User32.CreateWindowEx(0x8, "tooltips_class32", "", 0xC3, 0, 0, 0, 0, control.Parent.Handle, (IntPtr)0, (IntPtr)0, (IntPtr)0);
                User32.SendMessage(hWnd, 1028, (IntPtr)0, pToolInfo); // TTM_ADDTOOL
                User32.SendMessage(hWnd, 1042, (IntPtr)0, (IntPtr)((ushort)x | ((ushort)y << 16))); // TTM_TRACKPOSITION
                //User32.SendMessage(hWnd, 1043, (IntPtr)0, (IntPtr)0); // TTM_SETTIPBKCOLOR
                //User32.SendMessage(hWnd, 1044, (IntPtr)0xffff, (IntPtr)0); // TTM_SETTIPTEXTCOLOR
                User32.SendMessage(hWnd, 1056, (IntPtr)icon, pszTitle); // TTM_SETTITLE 0:None, 1:Info, 2:Warning, 3:Error, >3:assumed to be an hIcon. ; 1057 for Unicode
                User32.SendMessage(hWnd, 1048, (IntPtr)0, (IntPtr)500); // TTM_SETMAXTIPWIDTH
                User32.SendMessage(hWnd, 0x40c, (IntPtr)0, pToolInfo); // TTM_UPDATETIPTEXT; 0x439 for Unicode
                User32.SendMessage(hWnd, 1041, (IntPtr)1, pToolInfo); // TTM_TRACKACTIVATE
                Marshal.FreeCoTaskMem(pszTitle);
                Marshal.DestroyStructure(pToolInfo, typeof(TOOLINFO));
                Marshal.FreeCoTaskMem(pToolInfo);
                if (focus)
                    control.Focus();
                // uncomment bellow to make balloon close when user changes focus,
                // starts typing, resizes/moves parent window, minimizes parent window, etc
                // adjust which control events to subscribe to depending on the control over which the balloon tip is shown
    
                /*control.Click += control_Event;
                control.Leave += control_Event;
                control.TextChanged += control_Event;
                control.LocationChanged += control_Event;
                control.SizeChanged += control_Event;
                control.VisibleChanged += control_Event;
                Control parent = control.Parent;
                while(parent != null)
                {
                    parent.VisibleChanged += control_Event;
                    parent = parent.Parent;
                }
                control.TopLevelControl.LocationChanged += control_Event;
                ((Form)control.TopLevelControl).Deactivate += control_Event;*/
    
                timer.AutoReset = false;
                timer.Elapsed += timer_Elapsed;
                if (timeOut > 0)
                {
                    timer.Interval = timeOut;
                    timer.Start();
                }
            }
    
            void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                Close();
            }
    
            void control_Event(object sender, EventArgs e)
            {
                Close();
            }
    
            void Close()
            {
                if (!semaphore.Wait(0)) // ensures one time only execution
                    return;
                timer.Elapsed -= timer_Elapsed;
                timer.Close();
                User32.SendMessage(hWnd, 0x0010, (IntPtr)0, (IntPtr)0); // WM_CLOSE
                //User32.SendMessage(hWnd, 0x0002, (IntPtr)0, (IntPtr)0); // WM_DESTROY
                //User32.SendMessage(hWnd, 0x0082, (IntPtr)0, (IntPtr)0); // WM_NCDESTROY
            }
    
            [StructLayout(LayoutKind.Sequential)]
            struct TOOLINFO
            {
                public uint cbSize;
                public uint uFlags;
                public IntPtr hwnd;
                public IntPtr uId;
                public RECT rect;
                public IntPtr hinst;
                [MarshalAs(UnmanagedType.LPStr)]
                public string lpszText;
                public IntPtr lParam;
            }
            [StructLayout(LayoutKind.Sequential)]
            struct RECT
            {
                public int Left;
                public int Top;
                public int Right;
                public int Bottom;
            }
    
            public enum ICON
            {
                NONE,
                INFO,
                WARNING,
                ERROR
            }
        }
    
        static class User32
        {
            [DllImportAttribute("user32.dll")]
            public static extern int SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
            [DllImportAttribute("user32.dll")]
            public static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr LPVOIDlpParam);
        }
    }
    

    看起来是这样的:

    【讨论】:

    • @miroxlav xy 在 Show() 方法的开头根据控件的坐标计算。它们是屏幕坐标(不相对于窗口的左上角)并且通过发送一个窗口消息TTM_TRACKPOSITION 来设置。打破xy 并确保以像素为单位的有效屏幕坐标。我不知道为什么坐标会失败。我认为它必须与从客户端到屏幕坐标的转换有关。您可以添加 xy 作为 Show() 方法的函数参数。将它们设为short 而不是ushort,以便与多显示器一起使用。
    • 你从哪里得到 SemaphoreSlim 类?
    • 啊。 4.5.设法只使用信号量。不过我很好奇...如何在单击实际工具提示时关闭它?
    • 我对此表示赞同。工具提示的最佳代码,它肯定胜过常规工具提示。谢谢克里斯!
    【解决方案3】:

    您是否尝试在 MouseOver 事件中仅使用 SetToolTip 方法(不调用 show 方法)

    ttpTemp.SetToolTip(txtTemp, "Ex:01(应该是数字)");

    这对我来说很好用(我使用托管 C++,但我认为它是一样的)。

    【讨论】:

      【解决方案4】:

      感谢Chris' answer,我在这里发布VB.NET 端口:

      Imports System.Collections.Generic
      Imports System
      Imports System.Linq
      Imports System.Text
      Imports System.Windows.Forms
      Imports System.Runtime.InteropServices
      
      Namespace [Lib].Windows
          Class BalloonTip
              Private timer As New System.Timers.Timer()
              Private semaphore As New System.Threading.SemaphoreSlim(1)
              Private hWnd As IntPtr
      
              Public Sub New(text As String, control As Control)
                  Show("", text, control)
              End Sub
      
              Public Sub New(title As String, text As String, control As Control, Optional icon As ICON = 0, Optional timeOut As Double = 0, Optional focus As Boolean = False)
                  Show(title, text, control, icon, timeOut, focus)
              End Sub
      
              Private Sub Show(title As String, text As String, control As Control, Optional icon As ICON = 0, Optional timeout As Double = 0, Optional focus As Boolean = False)
                  Dim x As UShort = CType(control.RectangleToScreen(control.ClientRectangle).Left + control.Width / 2, UShort)
                  Dim y As UShort = CType(control.RectangleToScreen(control.ClientRectangle).Top + control.Height / 2, UShort)
                  Dim toolInfo As New TOOLINFO()
                  toolInfo.cbSize = CType(Marshal.SizeOf(toolInfo), UInteger)
                  toolInfo.uFlags = &H20
                  ' TTF_TRACK
                  toolInfo.lpszText = text
                  Dim pToolInfo As IntPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(toolInfo))
                  Marshal.StructureToPtr(toolInfo, pToolInfo, False)
                  Dim buffer As Byte() = Encoding.UTF8.GetBytes(title)
                  buffer = buffer.Concat(New Byte() {0}).ToArray()
                  Dim pszTitle As IntPtr = Marshal.AllocCoTaskMem(buffer.Length)
                  Marshal.Copy(buffer, 0, pszTitle, buffer.Length)
                  hWnd = User32.CreateWindowEx(&H8, "tooltips_class32", "", &HC3, 0, 0, _
                      0, 0, control.Parent.Handle, CType(0, IntPtr), CType(0, IntPtr), CType(0, IntPtr))
                  User32.SendMessage(hWnd, 1028, CType(0, IntPtr), pToolInfo)
                  ' TTM_ADDTOOL
                  'User32.SendMessage(hWnd, 1043, CType(0, IntPtr), CType(0, IntPtr); ' TTM_SETTIPBKCOLOR
                  'User32.SendMessage(hWnd, 1044, CType(&HFFFF, IntPtr), CType(0, IntPtr); ' TTM_SETTIPTEXTCOLOR
                  User32.SendMessage(hWnd, 1056, CType(icon, IntPtr), pszTitle)
                  ' TTM_SETTITLE 0:None, 1:Info, 2:Warning, 3:Error, >3:assumed to be an hIcon. ; 1057 for Unicode
                  User32.SendMessage(hWnd, 1048, CType(0, IntPtr), CType(500, IntPtr))
                  ' TTM_SETMAXTIPWIDTH
                  User32.SendMessage(hWnd, &H40C, CType(0, IntPtr), pToolInfo)
                  ' TTM_UPDATETIPTEXT; 0x439 for Unicode
                  User32.SendMessage(hWnd, 1042, CType(0, IntPtr), CType(x Or (CUInt(y) << 16), IntPtr))
                  ' TTM_TRACKPOSITION
                  User32.SendMessage(hWnd, 1041, CType(1, IntPtr), pToolInfo)
                  ' TTM_TRACKACTIVATE
                  Marshal.FreeCoTaskMem(pszTitle)
                  Marshal.DestroyStructure(pToolInfo, GetType(TOOLINFO))
                  Marshal.FreeCoTaskMem(pToolInfo)
                  If focus Then
                      control.Focus()
                  End If
      
                  ' uncomment below to make balloon close when user changes focus,
                  ' starts typing, resizes/moves parent window, minimizes parent window, etc
                  ' adjust which control events to subscribe to depending on the control over which the balloon tip is shown
                  'AddHandler control.Click, AddressOf control_Event
                  'AddHandler control.Leave, AddressOf control_Event
                  'AddHandler control.TextChanged, AddressOf control_Event
                  'AddHandler control.LocationChanged, AddressOf control_Event
                  'AddHandler control.SizeChanged, AddressOf control_Event
                  'AddHandler control.VisibleChanged, AddressOf control_Event
                  'Dim parent As Control = control.Parent
                  'While Not (parent Is Nothing)
                  '    AddHandler parent.VisibleChanged, AddressOf control_Event
                  '    parent = parent.Parent
                  'End While
                  'AddHandler control.TopLevelControl.LocationChanged, AddressOf control_Event
                  'AddHandler DirectCast(control.TopLevelControl, Form).Deactivate, AddressOf control_Event
                  timer.AutoReset = False
                  RemoveHandler timer.Elapsed, AddressOf timer_Elapsed
                  If timeout > 0 Then
                      timer.Interval = timeout
                      timer.Start()
                  End If
              End Sub
      
              Private Sub timer_Elapsed(sender As Object, e As System.Timers.ElapsedEventArgs)
                  Close()
              End Sub
      
              Private Sub control_Event(sender As Object, e As EventArgs)
                  Close()
              End Sub
      
              Sub Close()
                  If Not semaphore.Wait(0) Then
                      ' ensures one time only execution
                      Return
                  End If
                  RemoveHandler timer.Elapsed, AddressOf timer_Elapsed
                  timer.Close()
                  User32.SendMessage(hWnd, &H10, CType(0, IntPtr), CType(0, IntPtr))
                  ' WM_CLOSE
                  'User32.SendMessage(hWnd, &H0002, CType(0, IntPtr), CType(0, IntPtr)); ' WM_DESTROY
                  'User32.SendMessage(hWnd, &H0082, CType(0, IntPtr), CType(0, IntPtr)); ' WM_NCDESTROY
              End Sub
      
              <StructLayout(LayoutKind.Sequential)> _
              Private Structure TOOLINFO
                  Public cbSize As UInteger
                  Public uFlags As UInteger
                  Public hwnd As IntPtr
                  Public uId As IntPtr
                  Public rect As RECT
                  Public hinst As IntPtr
                  <MarshalAs(UnmanagedType.LPStr)> _
                  Public lpszText As String
                  Public lParam As IntPtr
              End Structure
              <StructLayout(LayoutKind.Sequential)> _
              Private Structure RECT
                  Public Left As Integer
                  Public Top As Integer
                  Public Right As Integer
                  Public Bottom As Integer
              End Structure
      
              Public Enum ICON
                  NONE
                  INFO
                  WARNING
                  [ERROR]
              End Enum
          End Class
      
          NotInheritable Class User32
              Private Sub New()
              End Sub
              <DllImportAttribute("user32.dll")> _
              Public Shared Function SendMessage(hWnd As IntPtr, Msg As UInt32, wParam As IntPtr, lParam As IntPtr) As Integer
              End Function
              <DllImportAttribute("user32.dll")> _
              Public Shared Function CreateWindowEx(dwExStyle As UInteger, lpClassName As String, lpWindowName As String, dwStyle As UInteger, x As Integer, y As Integer, _
                  nWidth As Integer, nHeight As Integer, hWndParent As IntPtr, hMenu As IntPtr, hInstance As IntPtr, LPVOIDlpParam As IntPtr) As IntPtr
              End Function
          End Class
      End Namespace
      

      【讨论】:

      • 我们这样使用它:Dim btt As New [Lib].Windows.BalloonTip("Title", "Message", sender, [Lib].Windows.BalloonTip.ICON.INFO, 5000)
      【解决方案5】:

      嘿,我终于得到了这段代码

      鼠标离开时

             public class MouseLeave
          {
              public void mouseLeave(TextBox txtTemp, ToolTip ttpTemp)
              {
                  ttpTemp.Hide(txtTemp);
              }
          }
      

      鼠标悬停时

          public class MouseOver
          {
              public void mouseOver(TextBox txtTemp, ToolTip ttpTemp)
              {
                  switch (txtTemp.Name)
                  {
                      case "textBox1":
                          {
      
                              ttpTemp.AutoPopDelay = 2000;
                              ttpTemp.InitialDelay = 1000;
                              ttpTemp.ReshowDelay = 500;
                              ttpTemp.IsBalloon = true;
                              ttpTemp.SetToolTip(txtTemp, "Ex:01(Should be Numeric)");
                              ttpTemp.Show("Ex : 01(Should Be Numeric)", txtTemp, txtTemp.Width, txtTemp.Height / 10, 5000);
                          }
                          break;
      
                      case "txtDetail":
                          {
      
                              ttpTemp.AutoPopDelay = 2000;
                              ttpTemp.InitialDelay = 1000;
                              ttpTemp.ReshowDelay = 500;
                              ttpTemp.IsBalloon = true;
                              ttpTemp.SetToolTip(txtTemp, "Ex:01(Should be Numeric)");
                              ttpTemp.Show("Ex : 01(Should Be Numeric)", txtTemp, txtTemp.Width, txtTemp.Height / 10, 5000);
                          }
                          break;
                  }
              }
          }
      

      【讨论】: