在WinForms的工具提示中让动态图像正常工作

后端开发 2026-07-09

我有:

  • 创建了一个自定义类 ImageToolTip,它可以显示带图片的工具提示
  • 当以编程方式将其分配给ToolStrip按钮时,它能工作

我无法让这张图片实现动画效果。 我熟悉使用 ImageAnimator,但在帧变更事件中使控件失效,然后在 OnPaint() 中重绘图像的常用技术,不能应用于工具提示。

此外,我甚至无法通过图片的位置来临时凑合一个解决方案,因为 ToolTip 类根本没有提供它实际绘制位置的任何参照坐标。

我尝试了哪些方法(在把 ImageAnimator.UpdateFrames() 调用放入 OnDraw 事件处理程序之后):

  • Active 设置为false,然后再设回true —— 重新激活工具提示不起作用
  • 使用 Hide(),再用 Show() 也失败

System.InvalidOperationException: 跨线程操作无效:从创建控件的线程以外的线程访问控件 'xyz'。

我还能用哪些横向思维来实现所期望的效果?

编辑:按要求提供代码——我尽量把代码精简到最小,同时保留了它的“本质”以及如何关联在一起的结构。我还保留了关于使用 Active 参数的先前尝试的注释。

你会注意到我在 ImageToolTip 类内部尝试过,以及让它的拥有者来做这件事——两种方式都会抛出同样的异常(如上所述)。

public class ImageToolTip : ToolTip
{
    private const int IMAGE_SIZE = 32;
    private const int MARGIN = 4;
    private const TextFormatFlags TEXT_FORMAT_FLAGS = TextFormatFlags.VerticalCenter
    | TextFormatFlags.Left
    | TextFormatFlags.LeftAndRightPadding
    | TextFormatFlags.NoClipping
    | TextFormatFlags.WordBreak;

    public string ImageFileName { get; set; }

    /// <summary>
    /// NOTES:
    /// 1/. Will be blithely ignored if the owning control is the top tool strip
    /// 2/. We are using the filename, and not the actual image as this also confirms if we need to
    ///     animate it or not
    /// 3/. Code to find the image *will* search all images, not just button related ones; required
    ///     as the image will often be something not normally button related
    /// </summary>
    /// <remarks>
    /// HACK: Rather than shtuff about making our own, derived Popup and Draw handlers (with an
    /// ImageToolTipDetails param), we will stick to a holistic image.
    /// After all, the primary use is for tool strip buttons, which define their own values...
    /// </remarks>
    private Image CurrentImage = null;

    public event EventHandler FrameChanged;

    public ImageToolTip()
    {
        this.OwnerDraw = true;
        this.Popup += new PopupEventHandler(this.OnPopup);
        this.Draw += new DrawToolTipEventHandler(this.OnDraw);
    }

    private void OnPopup(object sender, PopupEventArgs e)
    {
        ImageToolTipDetails deets = DeriveToolTipDetails(e.AssociatedControl);
        if (deets == null)
        {
            // Bug out completely; do not try to show the tool tip.
            e.Cancel = true;
        }
        else
        {
            e.ToolTipSize = DeriveToolTipSize(deets);
        }
    }

    public Size DeriveToolTipSize(ImageToolTipDetails aDetails)
    {
        Size result = TextRenderer.MeasureText(aDetails.Text, SystemFonts.DefaultFont);
        // HACK: With the image/s currently used, we don't seem to need:
        //  1/. Margins above, to the left of the image
        //  2/. A margin between the image, and the text
        result.Height += IMAGE_SIZE + MARGIN;  // For the image above the text.
        // We seem to need to draw the text an extra MARGIN in, hence 3 times.
        result.Width += 3 * MARGIN;

        return result;
    }

    private void OnDraw(object sender, DrawToolTipEventArgs e)
    {
        ImageToolTipDetails deets = DeriveToolTipDetails(e.AssociatedControl);
        if (deets == null)
        {
            // Bug out completely; do not try to show the tool tip.
            // This event may never even get called, as the Popup event cancelled itself, but
            // better safe than sorry...
        }
        else
        {
            e.DrawBackground();
            e.DrawBorder();

            //e.DrawText();
            if (CurrentImage == null)
            {
                CommonFunctions cf = new CommonFunctions();
                CurrentImage = cf.FindButtonImageFilename(deets.ImageFileName, true);
                CurrentControl = e.AssociatedControl;
                if (ImageAnimator.CanAnimate(CurrentImage))
                {
                    ImageAnimator.Animate(CurrentImage, new EventHandler(this.OnFrameChanged));
                }    
            }

            e.Graphics.DrawImage(CurrentImage, e.Bounds.Left, e.Bounds.Top, IMAGE_SIZE, IMAGE_SIZE);
            // The example I based this from used SystemBrushes.ActiveCaptionText, but I perceive that
            // using the ForeColor specified for this control is more accurate.
            e.Graphics.DrawString(deets.Text, SystemFonts.DefaultFont, SystemBrushes.FromSystemColor(this.ForeColor),
                e.Bounds.Left + MARGIN, e.Bounds.Top + IMAGE_SIZE);
        }
    }

    private void OnFrameChanged(object o, EventArgs e)
    {
        //Cite: https://learn.microsoft.com/en-us/dotnet/api/system.drawing.imageanimator.animate?view=netframework-4.8.1&viewFallbackFrom=windowsdesktop-10.0
        //Force a call to the Paint event handler.
        //this.Invalidate();
        ImageAnimator.UpdateFrames();
        //this.Active = false;
        //this.Active = true;
        //string text = this.GetToolTip(CurrentControl);
        //this.Hide(CurrentControl);
        //this.Show(text, CurrentControl);
        // Bubble the event up to the parent.
        FrameChanged?.Invoke(this, e);
    }

    /// <summary>
    /// Gets the image and text to use, for the tool tip.
    /// </summary>
    /// <param name="aAssociatedControl"></param>
    /// <returns>Returns null if nothing is to be displayed (as per its BL).</returns>
    private ImageToolTipDetails DeriveToolTipDetails(Control aAssociatedControl)
    {
        ImageToolTipDetails result = null;

        if (aAssociatedControl.GetType() == typeof(MenuStrip))
        {
            // The problem we have is usually the Y co-ord is just beyond the bounds of the tool strip,
            // and we get a null item.  This takes the approx. middle of the strip.
            // PRE: We only ever care about the top strip - hence we use MenuStrip, and not ToolStrip!
            // HACK: We may need to amend co-ords if the overall window is not maximised?
            ToolStripItem item = ((MenuStrip)aAssociatedControl).GetItemAt(Cursor.Position.X, 20);
            if (item != null)
            {
                if (item.Tag != null)
                {
                    result = (ImageToolTipDetails)item.Tag;
                }
                // else do nothing; do NOT show the tooltip at all (let the button's ToolTipText handle it).
            }
            // else do nothing; user is either hovering over blank space or it is not a button (menu item, etc).
        }
        else  // Something else; handle as per normal.
        {
            // The Draw caller has the text value, but the Popup one doesn't; easier to simply get it here, rather
            // than make the callers provide it, as another function param.
            result = new ImageToolTipDetails(ImageFileName, GetToolTip(aAssociatedControl));
        }

        return result;
    }
}

来自一个使用上述自定义类的表单片段:

public frmThing()
{
    InitializeComponent();
    imageToolTip1 = new ImageToolTip();
    imageTollTip1.ImageFileName = "animatedImage.gif";
    // Try direct shows/hides.
    //imageToolTip1.SetToolTip("blah!", btnFoo);
    imageToolTip1.FrameChanged += new System.EventHandler(ToolTipFrameChanged);
}

private void btnFoo_MouseEnter(object sender, EventArgs e)
{
    // I exist to try doing all Shows/Hides explicitly, rather than leverage ShowToolTip().
    imageToolTip1.Show("blah!", btnFoo);
}

private void btnFoo_MouseLeave(object sender, EventArgs e)
{
    // I exist to try doing all Shows/Hides explicitly, rather than leverage ShowToolTip().
    imageToolTip1.Hide();
}

protected void ToolTipFrameChanged(object sender, EventArgs e)
{
    //imageToolTip1.Active = false;
    //imageToolTip1.Active = true;
    imageToolTip1.Hide();
    imageToolTip1.Show("blah!", btnFoo);
}

解决方案

我终于找到了自己的答案!

  • 新增一个属性,用来保存工具提示的句柄:
    private IntPtr Handle = IntPtr.Zero;
  • OnDraw 事件中,我们用来判断是否需要动画的条件(为清晰起见,这里给出条件)
    if (ImageAnimator.CanAnimate(CurrentImage))
    {
        // Cite: https://stackoverflow.com/questions/42535141/winform-tooltip-location-setting
        // Note: This also covers how to move the window, but not directly applicable for our context.
        ToolTip t = (ToolTip)sender;
        PropertyInfo h = t.GetType().GetProperty("Handle",
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        Handle = (IntPtr)h.GetValue(t);
        ImageAnimator.Animate(CurrentImage, new EventHandler(this.OnFrameChanged));
    }
  • The OnFrameChanged 函数变为:
    private void OnFrameChanged(object o, EventArgs e)
    {
        ImageAnimator.UpdateFrames();
        Graphics g = Graphics.FromHwnd(Handle);
        g.DrawImage(CurrentImage, 0, 0, IMAGE_SIZE, IMAGE_SIZE);
    }
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章