// 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.ComponentModel; using System.Drawing; using System.Drawing.Design; using System.Windows.Forms; namespace AntdUI { /// /// CollapseGroup 折叠分组面板 /// /// 可以折叠/展开的内容区域。 [Description("CollapseGroup 折叠分组面板")] [ToolboxItem(true)] [DefaultProperty("Items")] [DefaultEvent("ItemClick")] public class CollapseGroup : IControl { #region 属性 /// /// 悬停背景颜色 /// [Description("悬停背景颜色"), Category("外观"), DefaultValue(null)] public Color? BackHover { get; set; } /// /// 激活背景颜色 /// [Description("激活背景颜色"), Category("外观"), DefaultValue(null)] public Color? BackActive { get; set; } Color? fore; /// /// 文字颜色 /// [Description("文字颜色"), Category("外观"), DefaultValue(null)] public new Color? ForeColor { get => fore; set { if (fore == value) fore = value; fore = value; Invalidate(); } } /// /// 激活字体颜色 /// [Description("激活字体颜色"), Category("外观"), DefaultValue(null)] public Color? ForeActive { get; set; } /// /// 只保持一个子菜单的展开 /// [Description("只保持一个子菜单的展开"), Category("外观"), DefaultValue(false)] public bool Unique { get; set; } int radius = 6; /// /// 圆角 /// [Description("圆角"), Category("外观"), DefaultValue(6)] public int Radius { get => radius; set { if (radius == value) return; radius = value; Invalidate(); } } int columnCount = 6; /// /// 列数量 /// [Description("列数量"), Category("外观"), DefaultValue(6)] public int ColumnCount { get => columnCount; set { if (columnCount == value) return; columnCount = value; ChangeList(); Invalidate(); } } CollapseGroupItemCollection? items; /// /// 集合 /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Description("集合"), Category("数据")] public CollapseGroupItemCollection Items { get { items ??= new CollapseGroupItemCollection(this); return items; } set => items = value.BindData(this); } 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(); } } } /// /// 滚动条 /// [Browsable(false)] public ScrollBar ScrollBar; #endregion #region 布局 protected override void OnSizeChanged(EventArgs e) { var rect = ChangeList(); ScrollBar.SizeChange(rect); base.OnSizeChanged(e); } internal Rectangle ChangeList() { var _rect = ClientRectangle; if (pauseLayout || items == null || items.Count == 0 || (_rect.Width == 0 || _rect.Height == 0)) return _rect; var rect = ClientRectangle.DeflateRect(Padding); int y = rect.Y; Helper.GDI(g => { var size = g.MeasureString(Config.NullText, Font).Size(); int gap = (int)(4 * Config.Dpi), csize = (rect.Width - (gap * (columnCount - 1))) / columnCount, icon_size = csize / 2, height = size.Height + gap * 2; foreach (var it in items) { it.PARENT = this; it.SetRect(g, new Rectangle(rect.X, y, rect.Width, height), size.Height, gap); y += height; if (it.CanExpand) { int y_item = y; ChangeList(g, rect, it, it.Sub, ref y, size.Height, csize, icon_size, gap); it.SubY = y_item; it.SubHeight = y - y_item; if ((it.Expand || it.ExpandThread) && it.ExpandProg > 0) { it.ExpandHeight = y - y_item; y = y_item + (int)(it.ExpandHeight * it.ExpandProg); } else if (!it.Expand) y = y_item; } } }); ScrollBar.SetVrSize(0, y); return _rect; } void ChangeList(Graphics g, Rectangle rect, CollapseGroupItem Parent, CollapseGroupSubCollection items, ref int y, int font_height, int csize, int icon_size, int gap) { int hasI = 0, tmp = 0; foreach (var it in items) { it.PARENT = this; it.PARENTITEM = Parent; var size = g.MeasureString(it.Text, Font, csize, s_c).Size(); int xc = size.Height - font_height; if (xc > 0 && tmp < xc) tmp = xc; it.SetRect(g, new Rectangle(rect.X + ((csize + gap) * hasI), y, csize, csize), font_height, xc, icon_size); hasI++; if (hasI > columnCount - 1) { y += csize + gap + tmp; hasI = tmp = 0; } } if (hasI > 0) y += csize + gap + tmp; } #endregion #region 渲染 public CollapseGroup() { ScrollBar = new ScrollBar(this, true, true); } 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(); float _radius = radius * Config.Dpi; int sx = ScrollBar.ValueX, sy = ScrollBar.ValueY; g.TranslateTransform(-sx, -sy); using (var brush_fore = new SolidBrush(fore ?? Style.Db.TextBase)) using (var brush_fore_active = new SolidBrush(ForeActive ?? Style.Db.Primary)) using (var brush_hover = new SolidBrush(BackHover ?? Style.Db.FillSecondary)) using (var brush_active = new SolidBrush(BackActive ?? Style.Db.PrimaryBg)) using (var brush_TextQuaternary = new SolidBrush(Style.Db.TextQuaternary)) { PaintItem(g, rect, sx, sy, items, brush_fore, brush_fore_active, brush_hover, brush_active, brush_TextQuaternary, _radius); } g.ResetTransform(); ScrollBar.Paint(g); this.PaintBadge(g); base.OnPaint(e); } void PaintItem(Graphics g, Rectangle rect, int sx, int sy, CollapseGroupItemCollection items, SolidBrush fore, SolidBrush fore_active, SolidBrush hover, SolidBrush active, SolidBrush brush_TextQuaternary, float radius) { foreach (var it in items) { if (it.Show) { PaintArrow(g, it, fore, sx, sy); g.DrawStr(it.Text, Font, fore, it.txt_rect, s_l); if ((it.Expand || it.ExpandThread) && it.items != null && it.items.Count > 0) { if (it.ExpandThread) g.SetClip(new RectangleF(rect.X, it.rect.Bottom, rect.Width, it.ExpandHeight * it.ExpandProg)); foreach (var sub in it.items) { if (sub.Show) { if (sub.Enabled) { if (sub.Select) { PaintBack(g, sub, active); if (sub.AnimationHover) { using (var brush = new SolidBrush(Helper.ToColorN(sub.AnimationHoverValue, hover.Color))) { PaintBack(g, sub, brush); } } else if (sub.Hover) PaintBack(g, sub, hover); if (sub.Icon != null) g.DrawImage(sub.Icon, sub.ico_rect); else if (sub.IconSvg != null) g.GetImgExtend(sub.IconSvg, sub.ico_rect, fore_active.Color); g.DrawStr(sub.Text, Font, fore_active, sub.txt_rect, s_c); } else { if (sub.AnimationHover) { using (var brush = new SolidBrush(Helper.ToColorN(sub.AnimationHoverValue, hover.Color))) { PaintBack(g, sub, brush); } } else if (sub.Hover) PaintBack(g, sub, hover); if (sub.Icon != null) g.DrawImage(sub.Icon, sub.ico_rect); else if (sub.IconSvg != null) g.GetImgExtend(sub.IconSvg, sub.ico_rect, fore.Color); g.DrawStr(sub.Text, Font, fore, sub.txt_rect, s_c); } } else { if (sub.Icon != null) g.DrawImage(sub.Icon, sub.ico_rect); else if (sub.IconSvg != null) g.GetImgExtend(sub.IconSvg, sub.ico_rect, brush_TextQuaternary.Color); g.DrawStr(sub.Text, Font, brush_TextQuaternary, sub.txt_rect, s_c); } } } g.ResetClip(); } } } } void PaintBack(Graphics g, CollapseGroupSub sub, SolidBrush brush) { if (radius > 0) { using (var path = sub.rect.RoundPath(radius)) { g.FillPath(brush, path); } } else g.FillRectangle(brush, sub.rect); } readonly StringFormat s_c = Helper.SF(tb: StringAlignment.Near), s_l = Helper.SF_ALL(lr: StringAlignment.Near); internal PointF[] PaintArrow(RectangleF rect) { float size = rect.Height * 0.15F, size2 = rect.Height * 0.2F, size3 = rect.Height * 0.26F; return new PointF[] { new PointF(rect.X+size,rect.Y+rect.Height/2), new PointF(rect.X+rect.Width*0.4F,rect.Y+(rect.Height-size3)), new PointF(rect.X+rect.Width-size2,rect.Y+size2), }; } void PaintArrow(Graphics g, CollapseGroupItem item, SolidBrush color, int sx, int sy) { using (var pen = new Pen(color, 2F)) { pen.StartCap = pen.EndCap = System.Drawing.Drawing2D.LineCap.Round; if (item.ExpandThread) g.DrawLines(pen, item.arr_rect.TriangleLines(-(1F - (2F * item.ExpandProg)), .4F)); else if (item.Expand) g.DrawLines(pen, item.arr_rect.TriangleLines(1, .4F)); else g.DrawLines(pen, item.arr_rect.TriangleLines(-1, .4F)); } } void PaintArrow(Graphics g, CollapseGroupItem item, Pen pen, int sx, int sy, float rotate) { int size = item.arr_rect.Width, size_arrow = size / 2; g.TranslateTransform(item.arr_rect.X + size_arrow, item.arr_rect.Y + size_arrow); g.RotateTransform(rotate); g.DrawLines(pen, new Rectangle(-size_arrow, -size_arrow, item.arr_rect.Width, item.arr_rect.Height).TriangleLines(-1, .4F)); g.ResetTransform(); g.TranslateTransform(-sx, -sy); } #endregion #region 鼠标 object? MDown = null; protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); MDown = null; if (ScrollBar.MouseDownY(e.Location) && ScrollBar.MouseDownX(e.Location)) { if (items == null || items.Count == 0) return; int y = ScrollBar.ValueY; OnTouchDown(e.X, e.Y); foreach (var it in items) { if (it.rect.Contains(e.X, e.Y + y)) { MDown = it; return; } else if (it.Expand && it.items != null && it.items.Count > 0) { foreach (var sub in it.items) { if (sub.Show && sub.Enabled && sub.rect.Contains(e.X, e.Y + y)) { MDown = sub; return; } } } } } } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (ScrollBar.MouseMoveY(e.Location) && ScrollBar.MouseMoveX(e.Location)) { int hand = 0; if (items == null || items.Count == 0) return; int y = ScrollBar.ValueY; if (OnTouchMove(e.X, e.Y)) { foreach (var it in items) { if (it.rect.Contains(e.X, e.Y + y)) hand++; else if (it.Expand && it.items != null && it.items.Count > 0) { foreach (var sub in it.items) { sub.Hover = sub.Show && sub.Enabled && sub.rect.Contains(e.X, e.Y + y); if (sub.Hover) hand++; } } } } SetCursor(hand > 0); } else ILeave(); } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); if (ScrollBar.MouseUpY() && ScrollBar.MouseUpX() && OnTouchUp()) { if (items == null || items.Count == 0 || MDown == null) return; int y = ScrollBar.ValueY; foreach (var it in items) { if (it == MDown) { if (it.rect.Contains(e.X, e.Y + y)) { it.Expand = !it.Expand; if (it.Expand && Unique) { foreach (var it2 in items) { if (it2 != it) it2.Expand = false; } } } MDown = null; return; } if (it.items != null && it.items.Count > 0) { foreach (var sub in it.items) { if (MDown == sub) { if (sub.rect.Contains(e.X, e.Y + y)) { sub.Select = true; ItemClick?.Invoke(this, new ItemClickEventArgs(sub, new Rectangle(sub.rect.X, sub.rect.Y - y, sub.rect.Width, sub.rect.Height), e)); } MDown = null; return; } } } } } } internal void IUSelect() { if (items == null || items.Count == 0) return; foreach (var it in items) { if (it.items != null && it.items.Count > 0) { foreach (var sub in it.items) sub.Select = false; } } } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); ScrollBar.Leave(); ILeave(); } void ILeave() { if (items == null || items.Count == 0) return; foreach (var it in items) { if (it.items != null && it.items.Count > 0) { foreach (var sub in it.items) sub.Hover = false; } } } protected override void OnMouseWheel(MouseEventArgs e) { ScrollBar.MouseWheel(e.Delta); base.OnMouseWheel(e); } protected override bool OnTouchScrollX(int value) => ScrollBar.MouseWheelX(value); protected override bool OnTouchScrollY(int value) => ScrollBar.MouseWheelY(value); #endregion #region 事件 public class ItemClickEventArgs : VMEventArgs { public ItemClickEventArgs(CollapseGroupSub item, Rectangle rect, MouseEventArgs e) : base(item, e) { Rect = rect; } public Rectangle Rect { get; private set; } } public delegate void ItemClickEventHandler(object sender, ItemClickEventArgs e); /// /// 点击项事件 /// [Description("点击项事件"), Category("行为")] public event ItemClickEventHandler? ItemClick = null; #endregion protected override void Dispose(bool disposing) { ScrollBar.Dispose(); base.Dispose(disposing); } } public class CollapseGroupItemCollection : iCollection { public CollapseGroupItemCollection(CollapseGroup it) { BindData(it); } public CollapseGroupItemCollection(CollapseGroupItem it) { BindData(it); } internal CollapseGroupItemCollection BindData(CollapseGroup it) { action = render => { if (render) it.ChangeList(); it.Invalidate(); }; return this; } internal CollapseGroupItemCollection BindData(CollapseGroupItem it) { action = render => { if (it.PARENT == null) return; if (render) it.PARENT.ChangeList(); it.PARENT.Invalidate(); }; return this; } } public class CollapseGroupItem : NotifyProperty { public CollapseGroupItem() { } public CollapseGroupItem(string text) { Text = text; } string? text = null; /// /// 文本 /// [Description("文本"), Category("外观"), DefaultValue(null)] public string? Text { get => text; set { if (text == value) return; text = value; Invalidates(); } } /// /// 用户定义数据 /// [Description("用户定义数据"), Category("数据"), DefaultValue(null)] public object? Tag { get; set; } internal CollapseGroupSubCollection? items; /// /// 获取列表中所有列表项的集合 /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Description("子集合"), Category("外观")] public CollapseGroupSubCollection Sub { get { items ??= new CollapseGroupSubCollection(this); return items; } set => items = value.BindData(this); } #region 展开 ITask? ThreadExpand = null; bool expand = false; /// /// 展开 /// [Description("展开"), Category("行为"), DefaultValue(false)] public bool Expand { get => expand; set { if (expand == value) return; expand = value; if (items != null && items.Count > 0) { if (PARENT != null && PARENT.IsHandleCreated && Config.Animation) { ThreadExpand?.Dispose(); float oldval = -1; if (ThreadExpand?.Tag is float oldv) oldval = oldv; ExpandThread = true; if (value) { int t = Animation.TotalFrames(10, 200); ThreadExpand = new ITask(false, 10, t, oldval, AnimationType.Ball, (i, val) => { ExpandProg = val; Invalidates(); }, () => { ExpandProg = 1F; ExpandThread = false; Invalidates(); }); } else { var t = Animation.TotalFrames(10, 200); ThreadExpand = new ITask(true, 10, t, oldval, AnimationType.Ball, (i, val) => { ExpandProg = val; Invalidates(); }, () => { ExpandProg = 1F; ExpandThread = false; Invalidates(); }); } } else { ExpandProg = 1F; Invalidates(); } } else { ExpandProg = 1F; Invalidates(); } } } [Description("是否可以展开"), Category("行为"), DefaultValue(false)] public bool CanExpand { get => items != null && items.Count > 0; } #endregion #region 样式 Color? fore; /// /// 文本颜色 /// [Description("文本颜色"), Category("外观"), DefaultValue(null)] public Color? Fore { get => fore; set { if (fore == value) return; fore = value; Invalidate(); } } #endregion void Invalidate() { PARENT?.Invalidate(); } void Invalidates() { if (PARENT == null) return; PARENT.ChangeList(); PARENT.Invalidate(); } internal float SubY { get; set; } internal float SubHeight { get; set; } internal float ExpandHeight { get; set; } internal float ExpandProg { get; set; } internal bool ExpandThread { get; set; } internal bool Show { get; set; } internal CollapseGroup? PARENT { get; set; } internal void SetRect(Graphics g, Rectangle _rect, int icon_size, int gap) { rect = _rect; int x = _rect.X + gap, y = _rect.Y + (_rect.Height - icon_size) / 2; arr_rect = new Rectangle(x, y, icon_size, icon_size); x += icon_size + gap; txt_rect = new Rectangle(x, _rect.Y, _rect.Width - x - gap, _rect.Height); Show = true; } internal Rectangle rect { get; set; } internal Rectangle arr_rect { get; set; } internal Rectangle txt_rect { get; set; } public override string? ToString() => text; } public class CollapseGroupSubCollection : iCollection { public CollapseGroupSubCollection(CollapseGroup it) { BindData(it); } public CollapseGroupSubCollection(CollapseGroupItem it) { BindData(it); } internal CollapseGroupSubCollection BindData(CollapseGroup it) { action = render => { if (render) it.ChangeList(); it.Invalidate(); }; return this; } internal CollapseGroupSubCollection BindData(CollapseGroupItem it) { action = render => { if (it.PARENT == null) return; if (render) it.PARENT.ChangeList(); it.PARENT.Invalidate(); }; return this; } } public class CollapseGroupSub : NotifyProperty { public CollapseGroupSub() { } public CollapseGroupSub(string text) { Text = text; } public CollapseGroupSub(string text, Image? icon) { Text = text; Icon = icon; } Image? icon = null; /// /// 图标 /// [Description("图标"), Category("外观"), DefaultValue(null)] public Image? Icon { get => icon; set { if (icon == value) return; icon = value; OnPropertyChanged("Icon"); } } string? iconSvg = null; /// /// 图标 /// [Description("图标SVG"), Category("外观"), DefaultValue(null)] public string? IconSvg { get => iconSvg; set { if (iconSvg == value) return; iconSvg = value; OnPropertyChanged("IconSvg"); } } /// /// 是否包含图片 /// internal bool HasIcon { get => iconSvg != null || Icon != null; } /// /// 名称 /// [Description("名称"), Category("数据"), DefaultValue(null)] public string? Name { get; set; } string? text = null; /// /// 文本 /// [Description("文本"), Category("外观"), DefaultValue(null)] public string? Text { get => text; set { if (text == value) return; text = value; Invalidates(); } } /// /// 用户定义数据 /// [Description("用户定义数据"), Category("数据"), DefaultValue(null)] public object? Tag { get; set; } #region 禁用 bool enabled = true; /// /// 禁掉响应 /// [Description("禁掉响应"), Category("行为"), DefaultValue(true)] public bool Enabled { get => enabled; set { if (enabled == value) return; enabled = value; Invalidate(); } } #endregion #region 样式 Color? fore; /// /// 文本颜色 /// [Description("文本颜色"), Category("外观"), DefaultValue(null)] public Color? Fore { get => fore; set { if (fore == value) return; fore = value; Invalidate(); } } Color? back; /// /// 背景颜色 /// [Description("背景颜色"), Category("外观"), DefaultValue(null)] public Color? Back { get => back; set { if (back == value) return; back = value; Invalidate(); } } #endregion void Invalidate() { PARENT?.Invalidate(); } void Invalidates() { if (PARENT == null) return; PARENT.ChangeList(); PARENT.Invalidate(); } bool hover = false; /// /// 是否移动 /// internal bool Hover { get => hover; set { if (hover == value) return; hover = value; if (Config.Animation) { ThreadHover?.Dispose(); AnimationHover = true; var t = Animation.TotalFrames(20, 200); if (value) { ThreadHover = new ITask((i) => { AnimationHoverValue = Animation.Animate(i, t, 1F, AnimationType.Ball); Invalidate(); return true; }, 20, t, () => { AnimationHover = false; AnimationHoverValue = 1; Invalidate(); }); } else { ThreadHover = new ITask((i) => { AnimationHoverValue = 1F - Animation.Animate(i, t, 1F, AnimationType.Ball); Invalidate(); return true; }, 20, t, () => { AnimationHover = false; AnimationHoverValue = 0; Invalidate(); }); } } else Invalidate(); } } bool select = false; [Description("激活状态"), Category("行为"), DefaultValue(false)] public bool Select { get => select; set { if (select == value) return; if (value) PARENT?.IUSelect(); select = value; Invalidate(); } } internal CollapseGroup? PARENT { get; set; } public CollapseGroupItem? PARENTITEM { get; set; } internal void SetRect(Graphics g, Rectangle rect_read, int font_height, int xc, int icon_size) { rect = rect_read; int sp = (int)(font_height * .25F), t_x = rect_read.Y + ((rect_read.Height - (font_height + icon_size + sp)) / 2); txt_rect = new Rectangle(rect_read.X, t_x + icon_size + sp, rect_read.Width, rect_read.Height); ico_rect = new Rectangle(rect_read.X + (rect_read.Width - icon_size) / 2, t_x, icon_size, icon_size); if (xc > 0) rect = new Rectangle(rect_read.X, rect_read.Y, rect_read.Width, rect_read.Height + xc); Show = true; } internal bool Show { get; set; } internal Rectangle rect { get; set; } internal bool Contains(Point point, int x, int y) { if (rect.Contains(point.X + x, point.Y + y)) { Hover = true; return true; } else { Hover = false; return false; } } internal float AnimationHoverValue = 0; internal bool AnimationHover = false; ITask? ThreadHover = null; internal Rectangle txt_rect { get; set; } internal Rectangle ico_rect { get; set; } public override string? ToString() => text; } }