// COPYRIGHT (C) Tom. ALL RIGHTS RESERVED. // THE AntdUI PROJECT IS AN WINFORM LIBRARY LICENSED UNDER THE Apache-2.0 License. // LICENSED UNDER THE Apache License, VERSION 2.0 (THE "License") // YOU MAY NOT USE THIS FILE EXCEPT IN COMPLIANCE WITH THE License. // YOU MAY OBTAIN A COPY OF THE LICENSE AT // // http://www.apache.org/licenses/LICENSE-2.0 // // UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, SOFTWARE // DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED. // SEE THE LICENSE FOR THE SPECIFIC LANGUAGE GOVERNING PERMISSIONS AND // LIMITATIONS UNDER THE License. // GITEE: https://gitee.com/antdui/AntdUI // GITHUB: https://github.com/AntdUI/AntdUI // CSDN: https://blog.csdn.net/v_132 // QQ: 17379620 using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Design; using System.Drawing.Drawing2D; using System.Threading; using System.Windows.Forms; namespace AntdUI { /// /// Tooltip 文字提示 /// /// 简单的文字提示气泡框。 [Description("Tooltip 文字提示")] [ToolboxItem(true)] public class Tooltip : IControl, ITooltip { #region 参数 int radius = 6; /// /// 圆角 /// [Description("圆角"), Category("外观"), DefaultValue(6)] public int Radius { get => radius; set { if (radius == value) return; radius = value; Invalidate(); } } /// /// 箭头大小 /// [Description("箭头大小"), Category("外观"), DefaultValue(8)] public int ArrowSize { get; set; } = 8; /// /// 箭头方向 /// [Description("箭头方向"), Category("外观"), DefaultValue(TAlign.Top)] public TAlign ArrowAlign { get; set; } = TAlign.Top; /// /// 自定义宽度 /// [Description("自定义宽度"), Category("外观"), DefaultValue(null)] public int? CustomWidth { get; set; } #endregion #region 渲染 readonly StringFormat s_c = Helper.SF_NoWrap(), s_l = Helper.SF(lr: StringAlignment.Near); protected override void OnPaint(PaintEventArgs e) { var rect = ClientRectangle; var g = e.Graphics.High(); MaximumSize = MinimumSize = this.RenderMeasure(g, out var multiline); this.Render(g, rect, multiline, s_c, s_l); base.OnPaint(e); } #endregion #region 静态方法 /// /// Tooltip 文字提示 /// /// 所属控件 /// 文本 /// 箭头方向 public static Form? open(Control control, string text, TAlign ArrowAlign = TAlign.Top) { return open(new Config(control, text) { ArrowAlign = ArrowAlign }); } /// /// Tooltip 文字提示 /// /// 所属控件 /// 文本 /// 偏移量 /// 箭头方向 public static Form? open(Control control, string text, Rectangle rect, TAlign ArrowAlign = TAlign.Top) { return open(new Config(control, text) { Offset = rect, ArrowAlign = ArrowAlign }); } /// /// Tooltip 文字提示 /// /// 配置 public static Form? open(Config config) { if (config.Control.IsHandleCreated) { if (config.Control.InvokeRequired) { Form? form = null; config.Control.Invoke(new Action(() => { form = open(config); })); return form; } var popover = new TooltipForm(config.Control, config.Text, config); popover.Show(config.Control); return popover; } return null; } /// /// 配置 /// public class Config : ITooltipConfig { /// /// Tooltip 配置 /// /// 所属控件 /// 文本 public Config(Control control, string text) { Font = control.Font; Control = control; Text = text; } /// /// 所属控件 /// public Control Control { get; set; } /// /// 偏移量 /// public object? Offset { get; set; } = null; /// /// 字体 /// public Font? Font { get; set; } /// /// 文本 /// public string Text { get; set; } /// /// 圆角 /// public int Radius { get; set; } = 6; /// /// 箭头大小 /// public int ArrowSize { get; set; } = 8; /// /// 箭头方向 /// public TAlign ArrowAlign { get; set; } = TAlign.Top; /// /// 自定义宽度 /// public int? CustomWidth { get; set; } } #endregion } internal class TooltipForm : ILayeredFormOpacity, ITooltip { readonly Control? ocontrol = null; bool multiline = false; public TooltipForm(Control control, string txt, ITooltipConfig component) { ocontrol = control; control.Parent.SetTopMost(Handle); Text = txt; if (component.Font != null) Font = component.Font; else if (Config.Font != null) Font = Config.Font; ArrowSize = component.ArrowSize; Radius = component.Radius; ArrowAlign = component.ArrowAlign; CustomWidth = component.CustomWidth; Helper.GDI(g => { SetSize(this.RenderMeasure(g, out multiline)); }); var point = control.PointToScreen(Point.Empty); if (component is Tooltip.Config config) { if (config.Offset is RectangleF rectf) SetLocation(ArrowAlign.AlignPoint(new Rectangle(point.X + (int)rectf.X, point.Y + (int)rectf.Y, (int)rectf.Width, (int)rectf.Height), TargetRect.Width, TargetRect.Height)); else if (config.Offset is Rectangle rect) SetLocation(ArrowAlign.AlignPoint(new Rectangle(point.X + rect.X, point.Y + rect.Y, rect.Width, rect.Height), TargetRect.Width, TargetRect.Height)); else SetLocation(ArrowAlign.AlignPoint(point, control.Size, TargetRect.Width, TargetRect.Height)); } else SetLocation(ArrowAlign.AlignPoint(point, control.Size, TargetRect.Width, TargetRect.Height)); control.LostFocus += Control_LostFocus; control.MouseLeave += Control_LostFocus; } public TooltipForm(Control control, Rectangle rect, string txt, ITooltipConfig component) { ocontrol = control; control.SetTopMost(Handle); Text = txt; if (component.Font != null) Font = component.Font; else if (Config.Font != null) Font = Config.Font; ArrowSize = component.ArrowSize; Radius = component.Radius; ArrowAlign = component.ArrowAlign; CustomWidth = component.CustomWidth; Helper.GDI(g => { SetSize(this.RenderMeasure(g, out multiline)); }); SetLocation(ArrowAlign.AlignPoint(rect, TargetRect)); } public void SetText(Rectangle rect, string text) { Text = text; Helper.GDI(g => { SetSize(this.RenderMeasure(g, out multiline)); }); SetLocation(ArrowAlign.AlignPoint(rect, TargetRect)); Print(); } private void Control_LostFocus(object? sender, EventArgs e) { IClose(); } #region 参数 /// /// 圆角 /// [Description("圆角"), Category("外观"), DefaultValue(6)] public int Radius { get; set; } = 6; /// /// 箭头大小 /// [Description("箭头大小"), Category("外观"), DefaultValue(8)] public int ArrowSize { get; set; } = 8; /// /// 箭头方向 /// [Description("箭头方向"), Category("外观"), DefaultValue(TAlign.Top)] public TAlign ArrowAlign { get; set; } = TAlign.Top; /// /// 自定义宽度 /// [Description("自定义宽度"), Category("外观"), DefaultValue(null)] public int? CustomWidth { get; set; } #endregion #region 渲染 readonly StringFormat s_c = Helper.SF_NoWrap(), s_l = Helper.SF(lr: StringAlignment.Near); public override Bitmap PrintBit() { var rect = TargetRectXY; Bitmap original_bmp = new Bitmap(rect.Width, rect.Height); using (var g = Graphics.FromImage(original_bmp).High()) { this.Render(g, rect, multiline, s_c, s_l); } return original_bmp; } #endregion protected override void Dispose(bool disposing) { base.Dispose(disposing); if (ocontrol == null) return; ocontrol.LostFocus -= Control_LostFocus; ocontrol.MouseLeave -= Control_LostFocus; } } [ProvideProperty("Tip", typeof(Control)), Description("提示")] public partial class TooltipComponent : Component, IExtenderProvider, ITooltipConfig { public bool CanExtend(object target) => target is Control; #region 属性 /// /// 字体 /// [Description("字体"), DefaultValue(null)] public Font? Font { get; set; } = null; /// /// 圆角 /// [Description("圆角"), Category("外观"), DefaultValue(6)] public int Radius { get; set; } = 6; /// /// 箭头大小 /// [Description("箭头大小"), Category("外观"), DefaultValue(8)] public int ArrowSize { get; set; } = 8; /// /// 箭头方向 /// [Description("箭头方向"), Category("外观"), DefaultValue(TAlign.Top)] public TAlign ArrowAlign { get; set; } = TAlign.Top; /// /// 自定义宽度 /// [Description("自定义宽度"), Category("外观"), DefaultValue(null)] public int? CustomWidth { get; set; } #endregion readonly Dictionary dic = new Dictionary(); [Description("设置是否提示"), DefaultValue(null)] [Editor(typeof(System.ComponentModel.Design.MultilineStringEditor), typeof(UITypeEditor))] public string? GetTip(Control item) { if (dic.TryGetValue(item, out string? value)) return value; return null; } public void SetTip(Control control, string? val) { if (val == null) { if (dic.ContainsKey(control)) { dic.Remove(control); control.MouseEnter -= Control_Enter; control.MouseLeave -= Control_Leave; control.Leave -= Control_Leave; } return; } if (dic.ContainsKey(control)) dic[control] = val.Trim(); else { dic.Add(control, val.Trim()); control.MouseEnter += Control_Enter; control.MouseLeave += Control_Leave; control.Leave -= Control_Leave; } } readonly List dic_in = new List(); private void Control_Leave(object? sender, EventArgs e) { if (sender != null && sender is Control obj) lock (dic_in) dic_in.Remove(obj); } private void Control_Enter(object? sender, EventArgs e) { if (sender != null && sender is Control obj) { lock (dic_in) dic_in.Add(obj); ITask.Run(() => { Thread.Sleep(500); if (dic_in.Contains(obj)) { obj.BeginInvoke(new Action(() => { new TooltipForm(obj, dic[obj], this).Show(); })); } }); } } } #region 核心渲染 internal static class ITooltipLib { #region 渲染 public static Size RenderMeasure(this ITooltip core, Graphics g, out bool multiline) { multiline = core.Text.Contains("\n"); int padding = (int)Math.Ceiling(20 * Config.Dpi); var font_size = g.MeasureString(core.Text, core.Font).Size(); if (core.CustomWidth.HasValue) { int width = (int)Math.Ceiling(core.CustomWidth.Value * Config.Dpi); if (font_size.Width > width) { font_size = g.MeasureString(core.Text, core.Font, width).Size(); multiline = true; } } if (core.ArrowAlign == TAlign.None) return new Size(font_size.Width + padding, font_size.Height + padding); if (core.ArrowAlign == TAlign.Bottom || core.ArrowAlign == TAlign.BL || core.ArrowAlign == TAlign.BR || core.ArrowAlign == TAlign.Top || core.ArrowAlign == TAlign.TL || core.ArrowAlign == TAlign.TR) return new Size(font_size.Width + padding, font_size.Height + padding + core.ArrowSize); else return new Size(font_size.Width + padding + core.ArrowSize, font_size.Height + padding); } public static void Render(this ITooltip core, Graphics g, Rectangle rect, bool multiline, StringFormat s_c, StringFormat s_l) { int gap = (int)Math.Ceiling(5 * Config.Dpi), padding = gap * 2, padding2 = padding * 2; using (var brush = new SolidBrush(Config.Mode == TMode.Dark ? Color.FromArgb(66, 66, 66) : Color.FromArgb(38, 38, 38))) { if (core.ArrowAlign == TAlign.None) { var rect_read = new Rectangle(rect.X + 5, rect.Y + 5, rect.Width - 10, rect.Height - 10); using (var path = rect_read.RoundPath(core.Radius)) { DrawShadow(core, g, rect, rect_read, 3, path); g.FillPath(brush, path); } RenderText(core, g, rect_read, multiline, padding, padding2, s_c, s_l); } else { Rectangle rect_text; switch (core.ArrowAlign.AlignMini()) { case TAlignMini.Top: rect_text = new Rectangle(rect.X, rect.Y, rect.Width, rect.Height - core.ArrowSize); break; case TAlignMini.Bottom: rect_text = new Rectangle(rect.X, rect.Y + core.ArrowSize, rect.Width, rect.Height - core.ArrowSize); break; case TAlignMini.Left: //左 rect_text = new Rectangle(rect.X, rect.Y, rect.Width - core.ArrowSize, rect.Height); break; default: //右 rect_text = new Rectangle(rect.X + core.ArrowSize, rect.Y, rect.Width - core.ArrowSize, rect.Height); break; } var rect_read = new Rectangle(rect_text.X + gap, rect_text.Y + gap, rect_text.Width - padding, rect_text.Height - padding); using (var path = rect_read.RoundPath(core.Radius)) { DrawShadow(core, g, rect, rect_read, 3, path); g.FillPath(brush, path); } g.FillPolygon(brush, core.ArrowAlign.AlignLines(core.ArrowSize, rect, rect_read)); RenderText(core, g, rect_text, multiline, padding, padding2, s_c, s_l); } } } static void RenderText(ITooltip core, Graphics g, Rectangle rect, bool multiline, int padding, int padding2, StringFormat s_c, StringFormat s_l) { if (multiline) g.DrawStr(core.Text, core.Font, Brushes.White, new Rectangle(rect.X + padding, rect.Y + padding, rect.Width - padding2, rect.Height - padding2), s_l); else g.DrawStr(core.Text, core.Font, Brushes.White, rect, s_c); } static void DrawShadow(this ITooltip core, Graphics _g, Rectangle brect, Rectangle rect, int size, GraphicsPath path2) { using (var bmp = new Bitmap(brect.Width, brect.Height)) { using (var g = Graphics.FromImage(bmp)) { int size2 = size * 2; using (var path = new Rectangle(rect.X - size, rect.Y - size + 2, rect.Width + size2, rect.Height + size2).RoundPath(core.Radius)) { path.AddPath(path2, false); using (var brush = new PathGradientBrush(path)) { brush.CenterColor = Color.Black; brush.SurroundColors = new Color[] { Color.Transparent }; g.FillPath(brush, path); } } } _g.DrawImage(bmp, brect); } } #endregion } public class TooltipConfig : ITooltipConfig { public Font? Font { get; set; } public int Radius { get; set; } = 6; public int ArrowSize { get; set; } = 8; public TAlign ArrowAlign { get; set; } = TAlign.Top; public int? CustomWidth { get; set; } } internal interface ITooltipConfig { /// /// 字体 /// Font? Font { get; set; } /// /// 圆角 /// int Radius { get; set; } /// /// 箭头大小 /// int ArrowSize { get; set; } /// /// 箭头方向 /// TAlign ArrowAlign { get; set; } /// /// 设定宽度 /// int? CustomWidth { get; set; } } internal interface ITooltip { /// /// 文本 /// string Text { get; set; } /// /// 字体 /// Font Font { get; set; } /// /// 圆角 /// int Radius { get; set; } /// /// 箭头大小 /// int ArrowSize { get; set; } /// /// 箭头方向 /// TAlign ArrowAlign { get; set; } /// /// 设定宽度 /// int? CustomWidth { get; set; } } #endregion }