在WinForms的工具提示中让动态图像正常工作
我有:
- 创建了一个自定义类
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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。