// 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.Windows.Forms; namespace AntdUI { /// /// Steps 步骤条 /// /// 引导用户按照流程完成任务的导航条。 [Description("Steps 步骤条")] [ToolboxItem(true)] [DefaultProperty("Current")] [DefaultEvent("ItemClick")] public class Steps : IControl { #region 属性 Color? fore; /// /// 文字颜色 /// [Description("文字颜色"), Category("外观"), DefaultValue(null)] [Editor(typeof(Design.ColorEditor), typeof(UITypeEditor))] public new Color? ForeColor { get => fore; set { if (fore == value) fore = value; fore = value; Invalidate(); } } int current = 0; /// /// 指定当前步骤,从 0 开始记数。在子 Step 元素中,可以通过 status 属性覆盖状态 /// [Description("指定当前步骤"), Category("外观"), DefaultValue(0)] public int Current { get => current; set { if (current == value) return; current = value; Invalidate(); } } TStepState status = TStepState.Process; /// /// 指定当前步骤的状态 /// [Description("指定当前步骤的状态"), Category("外观"), DefaultValue(TStepState.Process)] public TStepState Status { get => status; set { if (status == value) return; status = value; Invalidate(); } } bool vertical = false; /// /// 垂直方向 /// [Description("垂直方向"), Category("外观"), DefaultValue(false)] public bool Vertical { get => vertical; set { if (vertical == value) return; vertical = value; ChangeList(); Invalidate(); } } StepsItemCollection? items; /// /// 集合 /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Description("集合"), Category("数据")] public StepsItemCollection Items { get { items ??= new StepsItemCollection(this); return items; } set => items = value.BindData(this); } protected override void OnFontChanged(EventArgs e) { ChangeList(); base.OnFontChanged(e); } protected override void OnSizeChanged(EventArgs e) { ChangeList(); base.OnSizeChanged(e); } bool pauseLayout = false; [Browsable(false), Description("暂停布局"), Category("行为"), DefaultValue(false)] public bool PauseLayout { get => pauseLayout; set { if (pauseLayout == value) return; pauseLayout = value; if (!value) { ChangeList(); Invalidate(); } } } internal void ChangeList() { var rect = ClientRectangle.DeflateRect(Padding); if (pauseLayout || items == null || items.Count == 0) return; if (rect.Width == 0 || rect.Height == 0) return; Helper.GDI(g => { int gap = (int)(8F * Config.Dpi), split = (int)(1F * Config.Dpi); var _splits = new List(items.Count); using (var font_description = new Font(Font.FontFamily, Font.Size * 0.875F)) { int gap2 = gap * 2; if (vertical) { int t_height_one = rect.Height / items.Count; int i = 0; foreach (StepsItem it in items) { it.PARENT = this; it.TitleSize = g.MeasureString(it.Title, Font).Size(); int ico_size = (int)(it.TitleSize.Height * 1.6F); it.pen_w = it.TitleSize.Height * 0.136F; int width_one = it.TitleSize.Width + gap, height_one = ico_size, width_ex = 0; if (it.showSub) { it.SubTitleSize = g.MeasureString(it.SubTitle, Font).Size(); height_one += it.SubTitleSize.Height; } if (it.showDescription) { it.DescriptionSize = g.MeasureString(it.Description, font_description).Size(); width_ex = it.DescriptionSize.Width; } int centery = rect.Y + t_height_one * i + t_height_one / 2;//居中X it.title_rect = new Rectangle(rect.X + gap + ico_size, centery - height_one / 2, it.TitleSize.Width, height_one); int read_y = it.title_rect.Y - gap - ico_size; it.ico_rect = new Rectangle(rect.X, it.title_rect.Y + (it.title_rect.Height - ico_size) / 2, ico_size, ico_size); int tmp_max_width = it.title_rect.Width, tmp_max_height = it.ico_rect.Height, tmp_max_wr = it.title_rect.Right; if (it.showSub) { it.subtitle_rect = new Rectangle(it.title_rect.X + it.TitleSize.Width, it.title_rect.Y, it.SubTitleSize.Width, height_one); tmp_max_width = it.subtitle_rect.Width + it.title_rect.Width; tmp_max_wr = it.subtitle_rect.Right; } if (it.showDescription) { it.description_rect = new Rectangle(it.title_rect.X, it.title_rect.Y + (height_one - it.TitleSize.Height) / 2 + it.TitleSize.Height + gap / 2, it.DescriptionSize.Width, it.DescriptionSize.Height); if (it.description_rect.Width > tmp_max_width) { tmp_max_width = it.description_rect.Width; tmp_max_wr = it.description_rect.Right; } tmp_max_height += it.DescriptionSize.Height; } it.rect = new Rectangle(it.ico_rect.X - gap, it.ico_rect.Y - gap, tmp_max_wr - it.ico_rect.X + gap2, tmp_max_height + gap2); if (i > 0) { var old = items[i - 1]; if (old != null) _splits.Add(new RectangleF(it.ico_rect.X + (ico_size - split) / 2F, old.ico_rect.Bottom + gap, split, it.ico_rect.Y - old.ico_rect.Bottom - gap2)); } i++; } } else { //横向 int read_width = MaxHeight(g, font_description, gap, out var maxHeight); int i = 0; int count = items.Count; int sp = (rect.Width - read_width) / count, spline = sp - gap; int has_x = rect.X + sp / 2; count -= 1; foreach (StepsItem it in items) { int icon_size = it.IconSize ?? (int)(it.TitleSize.Height * 1.6F); int y = rect.Y + (rect.Height - maxHeight) / 2; it.ico_rect = new Rectangle(has_x, y + (it.TitleSize.Height - icon_size) / 2, icon_size, icon_size); it.title_rect = new Rectangle(it.ico_rect.Right + gap, y, it.TitleSize.Width, it.TitleSize.Height); int tmp_max_height = it.ico_rect.Height; if (it.showSub) it.subtitle_rect = new Rectangle(it.title_rect.X + it.TitleSize.Width, it.title_rect.Y, it.SubTitleSize.Width, it.title_rect.Height); if (it.showDescription) { it.description_rect = new Rectangle(it.title_rect.X, it.title_rect.Bottom + gap / 2, it.DescriptionSize.Width, it.DescriptionSize.Height); tmp_max_height += it.DescriptionSize.Height; } it.rect = new Rectangle(it.ico_rect.X - gap, it.ico_rect.Y - gap, it.ReadWidth + gap2, tmp_max_height + gap2); if (spline > 0 && i != count) _splits.Add(new RectangleF(it.rect.Right - gap, it.ico_rect.Y + (it.ico_rect.Height - split) / 2F, spline, split)); has_x += it.ReadWidth + sp; i++; } } } splits = _splits.ToArray(); }); return; } int MaxHeight(Graphics g, Font font_description, int gap, out int height) { int w = 0, temp_t = 0, temp = 0; foreach (StepsItem it in Items) { it.PARENT = this; #region 计算 it.TitleSize = g.MeasureString(it.Title, Font).Size(); if (it.showSub) it.SubTitleSize = g.MeasureString(it.SubTitle, Font).Size(); if (it.showDescription) it.DescriptionSize = g.MeasureString(it.Description, font_description).Size(); int icon_size = it.IconSize ?? (int)(it.TitleSize.Height * 1.6F); int width_top = it.TitleSize.Width + (it.showSub ? it.SubTitleSize.Width : 0), width_buttom = (it.showDescription ? it.DescriptionSize.Width : 0); it.ReadWidth = icon_size + gap + (width_top > width_buttom ? width_top : width_buttom); #endregion it.pen_w = it.TitleSize.Height * 0.136F; w += it.ReadWidth; if (temp_t == 0) temp_t = it.TitleSize.Height; if (temp == 0 && it.showDescription) temp = it.DescriptionSize.Height + gap / 2; } height = temp_t + temp; return w; } RectangleF[] splits = new RectangleF[0]; #endregion #region 渲染 readonly StringFormat stringLeft = Helper.SF(lr: StringAlignment.Near); readonly StringFormat stringCenter = Helper.SF_NoWrap(); protected override void OnPaint(PaintEventArgs e) { if (items == null || items.Count == 0) return; var rect = ClientRectangle; if (rect.Width == 0 || rect.Height == 0) return; var g = e.Graphics.High(); Color color_fore = fore ?? Style.Db.Text; using (var brush_fore = new SolidBrush(color_fore)) using (var brush_primarybg = new SolidBrush(Style.Db.PrimaryBg)) using (var brush_primary = new SolidBrush(Style.Db.Primary)) using (var brush_primary_fore = new SolidBrush(Style.Db.PrimaryColor)) using (var brush_dotback = new SolidBrush(Style.Db.BgBase)) using (var brush_fore2 = new SolidBrush(Style.Db.TextTertiary)) using (var brush_fore3 = new SolidBrush(Style.Db.TextSecondary)) using (var brush_bg2 = new SolidBrush(Style.Db.FillSecondary)) using (var font_description = new Font(Font.FontFamily, Font.Size * 0.875F)) { using (var brush_split = new SolidBrush(Style.Db.Split)) { for (int sp = 0; sp < splits.Length; sp++) { if (sp < current) g.FillRectangle(brush_primary, splits[sp]); else g.FillRectangle(brush_split, splits[sp]); } } int i = 0; foreach (StepsItem it in items) { if (it.Visible) { Color ccolor; if (i == current) { switch (status) { case TStepState.Finish: g.DrawStr(it.Title, Font, brush_fore, it.title_rect, stringLeft); g.DrawStr(it.SubTitle, Font, brush_fore2, it.subtitle_rect, stringLeft); g.DrawStr(it.Description, font_description, brush_fore2, it.description_rect, stringLeft); ccolor = brush_primary.Color; break; case TStepState.Wait: g.DrawStr(it.Title, Font, brush_fore2, it.title_rect, stringLeft); g.DrawStr(it.SubTitle, Font, brush_fore2, it.subtitle_rect, stringLeft); g.DrawStr(it.Description, font_description, brush_fore2, it.description_rect, stringLeft); ccolor = brush_fore2.Color; break; case TStepState.Error: using (var brush_error = new SolidBrush(Style.Db.Error)) { g.DrawStr(it.Title, Font, brush_error, it.title_rect, stringLeft); g.DrawStr(it.SubTitle, Font, brush_fore2, it.subtitle_rect, stringLeft); g.DrawStr(it.Description, font_description, brush_error, it.description_rect, stringLeft); ccolor = brush_error.Color; } break; case TStepState.Process: default: g.DrawStr(it.Title, Font, brush_fore, it.title_rect, stringLeft); g.DrawStr(it.SubTitle, Font, brush_fore2, it.subtitle_rect, stringLeft); g.DrawStr(it.Description, font_description, brush_fore, it.description_rect, stringLeft); ccolor = brush_fore.Color; break; } } else if (i < current) { //过 g.DrawStr(it.Title, Font, brush_fore, it.title_rect, stringLeft); g.DrawStr(it.SubTitle, Font, brush_fore2, it.subtitle_rect, stringLeft); g.DrawStr(it.Description, font_description, brush_fore2, it.description_rect, stringLeft); ccolor = brush_fore.Color; } else { //未 g.DrawStr(it.Title, Font, brush_fore2, it.title_rect, stringLeft); g.DrawStr(it.SubTitle, Font, brush_fore2, it.subtitle_rect, stringLeft); g.DrawStr(it.Description, font_description, brush_fore2, it.description_rect, stringLeft); ccolor = brush_fore2.Color; } if (PaintIcon(g, it, ccolor)) { if (i == current) { switch (status) { case TStepState.Finish: g.PaintIconCore(it.ico_rect, SvgDb.IcoSuccess, brush_primary.Color, brush_primarybg.Color); break; case TStepState.Wait: g.FillEllipse(brush_bg2, it.ico_rect); g.DrawStr((i + 1).ToString(), font_description, brush_fore3, it.ico_rect, stringCenter); break; case TStepState.Error: g.PaintIconCore(it.ico_rect, SvgDb.IcoError, Style.Db.ErrorColor, Style.Db.Error); break; case TStepState.Process: default: g.FillEllipse(brush_primary, it.ico_rect); g.DrawStr((i + 1).ToString(), font_description, brush_primary_fore, it.ico_rect, stringCenter); break; } } else if (i < current) g.PaintIconCore(it.ico_rect, SvgDb.IcoSuccess, brush_primary.Color, brush_primarybg.Color); else { g.FillEllipse(brush_bg2, it.ico_rect); g.DrawStr((i + 1).ToString(), font_description, brush_fore3, it.ico_rect, stringCenter); } } } i++; } } this.PaintBadge(g); base.OnPaint(e); } bool PaintIcon(Graphics g, StepsItem it, Color fore) { if (it.Icon != null) { g.DrawImage(it.Icon, it.ico_rect); return false; } else if (it.IconSvg != null) { if (g.GetImgExtend(it.IconSvg, it.ico_rect, fore)) return false; } return true; } #endregion #region 事件 /// /// 点击项时发生 /// [Description("点击项时发生"), Category("行为")] public event StepsItemEventHandler? ItemClick = null; protected override void OnMouseClick(MouseEventArgs e) { base.OnMouseClick(e); if (items == null || items.Count == 0 || ItemClick == null) return; for (int i = 0; i < items.Count; i++) { var it = items[i]; if (it != null) { if (it.rect.Contains(e.Location)) { ItemClick(this, new StepsItemEventArgs(it, e)); return; } } } } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (items == null || items.Count == 0 || ItemClick == null) return; for (int i = 0; i < items.Count; i++) { var it = items[i]; if (it != null) { if (it.rect.Contains(e.Location)) { SetCursor(true); return; } } } SetCursor(false); } #endregion } public class StepsItemCollection : iCollection { public StepsItemCollection(Steps it) { BindData(it); } internal StepsItemCollection BindData(Steps it) { action = render => { if (render) it.ChangeList(); it.Invalidate(); }; return this; } } public class StepsItem { public StepsItem() { } public StepsItem(string title) { Title = title; } public StepsItem(string title, string subTitle) { Title = title; SubTitle = subTitle; } public StepsItem(string title, string subTitle, string description) { Title = title; SubTitle = subTitle; Description = description; } Image? icon = null; /// /// 图标,可选 /// [Description("图标,可选"), Category("外观"), DefaultValue(null)] public Image? Icon { get => icon; set { if (icon == value) return; icon = value; PARENT?.Invalidate(); } } string? iconSvg = null; /// /// 图标SVG,可选 /// [Description("图标SVG,可选"), Category("外观"), DefaultValue(null)] public string? IconSvg { get => iconSvg; set { if (iconSvg == value) return; iconSvg = value; PARENT?.Invalidate(); } } int? iconsize = null; /// /// 图标的大小,可选 /// [Description("图标的大小,可选"), Category("外观"), DefaultValue(null)] public int? IconSize { get => iconsize; set { if (iconsize == value) return; iconsize = value; Invalidate(); } } internal int ReadWidth { get; set; } /// /// 名称 /// [Description("名称"), Category("数据"), DefaultValue(null)] public string? Name { get; set; } string title = "Title"; /// /// 标题 /// [Description("标题"), Category("外观"), DefaultValue("Title")] public string Title { get => title; set { if (title == value) return; title = value; Invalidate(); } } internal Size TitleSize { get; set; } string? subTitle = null; internal bool showSub = false; /// /// 子标题 /// [Description("子标题"), Category("外观"), DefaultValue(null)] public string? SubTitle { get => subTitle; set { if (string.IsNullOrEmpty(value)) value = null; if (subTitle == value) return; subTitle = value; showSub = subTitle != null; Invalidate(); } } internal Size SubTitleSize { get; set; } string? description = null; internal bool showDescription = false; /// /// 详情描述,可选 /// [Description("详情描述,可选"), Category("外观"), DefaultValue(null)] public string? Description { get => description; set { if (string.IsNullOrEmpty(value)) value = null; if (description == value) return; description = value; showDescription = description != null; Invalidate(); } } internal Size DescriptionSize { get; set; } bool visible = true; /// /// 是否显示 /// [Description("是否显示"), Category("外观"), DefaultValue(true)] public bool Visible { get => visible; set { if (visible == value) return; visible = value; Invalidate(); } } /// /// 用户定义数据 /// [Description("用户定义数据"), Category("数据"), DefaultValue(null)] public object? Tag { get; set; } void Invalidate() { if (PARENT == null) return; PARENT.ChangeList(); PARENT.Invalidate(); } internal Steps? PARENT { get; set; } internal float pen_w { get; set; } = 3F; internal Rectangle rect { get; set; } internal Rectangle title_rect { get; set; } internal Rectangle subtitle_rect { get; set; } internal Rectangle description_rect { get; set; } internal Rectangle ico_rect { get; set; } public override string ToString() => title + " " + subTitle; } }