说明
最近无聊花了2天时间写了一个左侧菜单控件,测试648万条数据,数据占内存2.9GB,滚动数据无任何卡顿。
控件是完全绘制的,没有叠加其他的控件。因为只是简单写了一些,所以功能上可能不是很完善,需要自己修改,如果没有能力修改的,那就只能这样使用了。
刚大概浏览了下代码,由于写出来有几天时间了,当时还写了一个功能是:选择框模式,即可以像TreeView一样提供选择功能,功能虽然写了,但在绘制选择框图标时,会多次绘制图标,一直出现内存流的错误,这个没修改过来,就取消了这个功能删除了响应的代码。刚才浏览代码发现还有部分的未删除,但不影响控件的正常运行。
所有代码都在后面。
截图
代码
由于我不是专业的程序员,完全是自学的,写出的代码可能存在命名奇怪,单词错误等,请见谅。
ClickItemEventArgs.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
public class ClickItemEventArgs : EventArgs
{
/// <summary>
/// 点击的鼠标
/// </summary>
public MouseButtons Button { get; set; }
/// <summary>
/// 点击的项
/// </summary>
public TreeMenuItem TreeMenuItem { get; set; }
public ClickItemEventArgs(MouseButtons buttons, TreeMenuItem item)
{
this.Button = buttons;
this.TreeMenuItem = item;
}
}
}
FoldingIcon.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
/// <summary>
/// 折叠项图标样式
/// </summary>
public enum FoldingIcon
{
None,
/// <summary>
/// 实心△图标
/// </summary>
Caret,
/// <summary>
/// 圆圈内有个">"的图标
/// </summary>
ChevronCircle,
/// <summary>
/// ">"符号图标
/// </summary>
Angle,
/// <summary>
/// ">"符号加粗图标
/// </summary>
Chevron,
}
}
ItemStyle.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
/// <summary>
/// 列表项的样式
/// </summary>
public class ItemStyle
{
/// <summary>
/// 列表项的级别
/// </summary>
public int Level { get; private set; } = 1;
/// <summary>
/// 列表项字体格式
/// </summary>
public Font Font { get; set; } = new Font("Microsoft YaHei UI", 9f, unit: GraphicsUnit.Point);
/// <summary>
/// 列表项字体颜色
/// </summary>
public Color ForeColor { get; set; } = Color.Black;
/// <summary>
/// 列表项背景颜色
/// </summary>
public Color BackColor { get; set; } = Color.Transparent;
/// <summary>
/// 圆形矩形的弧度
/// <para>当<=0时,背景或选中都是矩形</para>
/// </summary>
public int Radius { get; set; } = 5;
public ItemStyle(int level = 1)
{
Level = level;
}
public ItemStyle()
{
Level = 1;
}
}
}
ListItem.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
/// <summary>
/// 重定义的列表泛型,此类主要是不能直接添加元素,不能直接删除元素
/// </summary>
/// <typeparam name="T"></typeparam>
public class ListItem<T>:IEnumerable<T>
{
private List<T> items = new List<T>();
public T this[int index] { get => this.items[index]; }
public int Count { get { return this.items.Count; } }
/// <summary>
/// 筛选数据
/// </summary>
/// <param name="predicate"></param>
/// <returns></returns>
public ListItem<T> Where(Func<T, bool> predicate)
{
return new ListItem<T>(this.items.Where(predicate).ToList());
}
/// <summary>
/// ForEach 委托
/// </summary>
/// <param name="action"></param>
public void ForEach(Action<T> action)
{
this.items.ForEach(action);
}
public ListItem() { }
private ListItem(List<T> items)
{
this.items = items;
}
public IEnumerator<T> GetEnumerator()
{
return this.items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
TreeMenuItemCollection.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
public class TreeMenuItemCollection:IList<TreeMenuItem>
{
/// <summary>
/// 定义数组
/// </summary>
private List<TreeMenuItem> _items;
protected TreeMenuItem _root;
internal TreeMenuItem Root { get { return _root; } }
/// <summary>
/// 绑定的控件
/// </summary>
private TreeMenu _menu;
public TreeMenuItemCollection(TreeMenu owner)
{
_menu = owner;
_items = new List<TreeMenuItem>();
_root = new TreeMenuItem("根节点");
_root._indexs = new List<int>();
_root.SubItem = _items;
}
/// <summary>
/// 获取指定索引的项(仅仅能获取一级)
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public TreeMenuItem this[int index] { get => this._items[index];
set {
this._items[index] = value;
this._items[index]._indexs = new List<int>() { index };
this._items[index].ResetIndexs();
}
}
public TreeMenuItem? FindItem(string id)
{
var ids = this.DecomposeId(id);
TreeMenuItem item;
if (this._items.Count < ids[0]) item = this._items[0];
else return null;
for (int i = 1; i < ids.Count; i++)
{
if (item.SubItem.Count < ids[i])
{
item = item.SubItem[i];
}
else return null;
}
return item;
}
/// <summary>
/// 分解Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
protected List<int> DecomposeId(string id)
{
List<int> result = new List<int>();
List<string> ids = id.Split('-').ToList();
ids.ForEach(x => result.Add(int.Parse(x)));
return result;
}
/// <summary>
/// 获取所有子项的数量
/// </summary>
public int Count { get => this._items.Count; }
public bool IsReadOnly => throw new NotImplementedException();
/// <summary>
/// 添加项
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public int Add(TreeMenuItem value)
{
value._indexs = new List<int>() { this._items.Count }; // 这里还没有添加到列表。因此不用+1
value._parent = this._root;
value.ResetIndexs();
this._items.Add(value);
return this._items.Count - 1;
}
/// <summary>
/// 添加项
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="icon"></param>
/// <returns></returns>
public TreeMenuItem Add(string name, string value = "", IconType? icon = null)
{
TreeMenuItem item = new TreeMenuItem(name, value, icon);
this.Add(item);
return item;
}
/// <summary>
/// 清空所有
/// </summary>
public void Clear()
{
this._items.Clear();
}
/// <summary>
/// 判断是否包含,只对Id判断。判断Id是否存在
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public bool Contains(TreeMenuItem value)
{
var item = this.FindItem(value.Id);
return item != null;
}
/// <summary>
/// 复制到Array
/// </summary>
/// <param name="array"></param>
/// <param name="index"></param>
public void CopyTo(Array array, int index)
{
Array arr = this._items.ToArray();
arr.CopyTo(array, index);
}
/// <summary>
/// 复制到TreeMenuItem[]
/// </summary>
/// <param name="array"></param>
/// <param name="arrayIndex"></param>
public void CopyTo(TreeMenuItem[] array, int arrayIndex)
{
this._items.CopyTo(array, arrayIndex);
}
/// <summary>
/// 查找,仅支持查找顶级
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public int IndexOf(TreeMenuItem value)
{
for (int i = 0; i < this._items.Count; i++)
{
if (value.Id == this._items[i].Id) return i;
}
return -1;
}
public void Insert(int index, TreeMenuItem value)
{
this._items.Insert(index, value);
throw new InvalidOperationException();
}
public void Remove(TreeMenuItem? value)
{
throw new NotImplementedException();
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
void ICollection<TreeMenuItem>.Add(TreeMenuItem item)
{
this.Add(item);
}
bool ICollection<TreeMenuItem>.Remove(TreeMenuItem item)
{
this.Remove(item);
return true;
}
public IEnumerator<TreeMenuItem> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
/// <summary>
/// 根据当前项,向后查找下一个需要显示的项(只考虑需要显示的,并不考虑可视和折叠分组问题)
/// </summary>
/// <param name="item"></param>
/// <returns>返回null则是已经查找到最后了</returns>
internal TreeMenuItem? FindNextItem(TreeMenuItem item)
{
TreeMenuItem? newItem;
if (item.HasChlid == true)
{
newItem = item.SubItem.First();
}
else
{
// 如果父类为null 则已经找到root级别
if (item.IsRoot == true) return null;
newItem = item.Next(); // 获取下一个
// 当为null 则是已经到了最后一个,再查找就需要查找父类的下一个
if (newItem == null)
{
newItem = item; // 重新定义,方便循环
bool flg = true;
while (flg)
{
// 如果当前项为root 则退出
if (newItem.IsRoot == true) return null;
// 当前项不是root,则需要查找其父类的下一个
var res = newItem.Parent.Next(); // 获取当前对象父类的下一个(同时也父类不是root)
// 父类的下一个为null,则说明当前项父类已经是最后一个,需要查找更为高一个父类的下一个
if (res == null) // 当这个获取还是null,说明已经没有下一项了
{
// 将父类重新赋值给newItem,方便下次循环时调用
newItem = newItem.Parent;
/// 需要再次验证,尤其是1级最后一项
/// 因为如果是最后一项,向前循环查到1级时,这个时候它还不是root,但是root的最后一项
if (newItem.Level == 1 && newItem.IsLast)
{
return null;
}
// 这里没有使用IsLastItem判断是因为,使用此方法,则每一个级别的最后一个元素都要调用一次,会导致增加循环次数
}
else
{
// 找到了则标记并返回
flg = false;
newItem = res;
}
}
}
}
return newItem;
}
internal TreeMenuItem? FindPreviousItem(TreeMenuItem item)
{
if (item.Level == 1 && item.IsFrist) return null; // 当前项已经是可显示的第一个了
TreeMenuItem? newItem;
if (item.IsFrist == true)
{
return item.Parent;
}
else
{
newItem = item.Previous();
if (newItem?.HasChlid == true)
{
return this.GetLastChildItem(newItem);
}
else
{
return newItem;
}
}
return newItem;
}
/// <summary>
/// 判断传入项是否为最后一个项
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool IsLastItem(TreeMenuItem item)
{
TreeMenuItem newItem = item;
if(newItem.IsLast == false) return false; // 必须是所有同级别中最后一个
while (true)
{
if (newItem.Parent != null)
{
newItem = newItem.Parent;
if (newItem.IsLast == false) return false; // 必须是所有同级别中最后一个
}
else
{
return true;
}
}
}
/// <summary>
/// 获取指定项中所有子项、子子项中最后一个项
/// </summary>
/// <param name="item">指定的项,当为null,获取所有项的最后一个项</param>
/// <returns>当不存在节点时返回null</returns>
public TreeMenuItem? GetLastChildItem(TreeMenuItem? item = null)
{
if (item == null) item = this._root;
if (item.HasChlid == false) return null; // 当不存在子项,直接返回null
TreeMenuItem? record = item.LastChild; // 记录当前正在查找的项
if (record == null) return null; // 因为肯定有子项,因此不会是null
TreeMenuItem? res = record.LastChild; // 记录查找当前项最后一个子项的结果
while (true)
{
if (res == null)
{
return record;
}
else
{
record = res; // 重新赋值
res = res.LastChild; // 再次查找子项
}
}
}
}
}
ShowItem.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
/// <summary>
/// 当前显示项
/// </summary>
public class ShowItem
{
/// <summary>
/// 显示的项
/// </summary>
public TreeMenuItem Item { get; set; }
/// <summary>
/// 整个项的区域
/// </summary>
public Rectangle Rect { get; set; }
/// <summary>
/// 整个项左侧图标区域
/// </summary>
public Rectangle LeftIconRect { get; set; }
/// <summary>
/// 右侧折叠图标区域
/// </summary>
public Rectangle FoldIconRect { get; set; }
/// <summary>
/// 标题文字绘图区域
/// </summary>
public Rectangle StringRect { get; set; }
/// <summary>
/// 整个项的背景区域(一般会必绘图区小一点)
/// </summary>
public Rectangle BackgroundRect { get; set; }
/// <summary>
/// 整个项的绘图区域
/// </summary>
public Rectangle DrawArea { get; set; }
/// <summary>
/// 当前项的绘制样式
/// </summary>
public ItemStyle Style { get; set; }
public ShowItem(TreeMenuItem item, Rectangle rect)
{
Item = item;
Rect = rect;
}
}
}
TreeMenu.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
public partial class TreeMenu : UserControl
{
/// <summary>
/// 项的最小高度
/// </summary>
public const int MIN_ITEM_HEIGHT = 24;
/// <summary>
/// 子项的最小缩进间距
/// </summary>
public const int MIN_SINDENT = 8;
#region ===成员===
/// <summary>
/// 第一个显示的控件
/// <para>X坐标表示父类项,Y表示其子项</para>
/// </summary>
private TreeMenuItem? _fristItem = null;
/// <summary>
/// 控件中的列表项
/// </summary>
private TreeMenuItemCollection _items;
/// <summary>
/// 项高度
/// </summary>
private int _itemHight = 28;
/// <summary>
/// 列表项默认样式
/// </summary>
private ItemStyle _defaultItemStyle;
/// <summary>
/// 列表项样式
/// </summary>
private List<ItemStyle> _itemStyle = new List<ItemStyle>();
/// <summary>
/// 选中项的样式
/// </summary>
private ItemStyle _selectItemStyle;
/// <summary>
/// 光标悬停经过时项的样式
/// </summary>
private ItemStyle _hoverItemStyle ;
/// <summary>
/// 折叠图标
/// </summary>
private FoldingIcon _foldIcon = FoldingIcon.Angle;
/// <summary>
/// 无图标占用空间(当某个项没有图标时,则图标位为空,字符串会左移,和其他的图标对其)
/// </summary>
private bool _noIconOccupyingSpace = true;
/// <summary>
/// 子项缩进宽度
/// </summary>
private int _sindent = 15;
/// <summary>
/// 当前选中的项
/// </summary>
private ShowItem? _selectItem = null;
/// <summary>
/// 当前悬停所在的项
/// </summary>
private ShowItem? _hoverItem = null;
/// <summary>
/// 是否可以显示左侧图标
/// </summary>
private bool _showLeftIcon = true;
/// <summary>
/// 显示的项,及对应的位置
/// </summary>
private List<ShowItem> _showItems = new List<ShowItem>();
/// <summary>
/// 左侧图标颜色(包含图标和选择框),优先级高于项字体颜色,当为null或Empty时颜色与项的字体颜色相同。
/// </summary>
private Color? _leftIconColor = null;
/// <summary>
/// 是否点击折叠图标才能折叠项
/// </summary>
private bool _isClickIconFoldItem = false;
#endregion
#region ===公开属性===
public TreeMenuItemCollection Items { get => _items; set => _items = value; }
[Category("样式")]
[Description("左侧图标颜色(包含图标和选择框),优先级高于项字体颜色,当为null或Empty时颜色与项的字体颜色相同。")]
[Browsable(true)]
/// <summary>
/// 左侧图标颜色(包含图标和选择框),优先级高于项字体颜色,当为null或Empty时颜色与项的字体颜色相同。
/// </summary>
public Color? LeftIconColor
{
get=>this._leftIconColor;
set {
if (_leftIconColor == value) return;
_leftIconColor = value;
this.Refresh(); // 重绘前会自动计算缩进
}
}
/// <summary>
/// 当前选中项
/// </summary>
public TreeMenuItem? SelectItem { get => this._selectItem?.Item; }
[Category("其他")]
[Description("是否点击折叠图标才能折叠项,设置为true则要设置折叠图标,否则将无法通过点击鼠标折叠。当然这样也是一种禁止折叠的设置。")]
[Browsable(true)]
/// <summary>
/// 是否点击折叠图标才能折叠项
/// </summary>
public bool IsClickIconFoldItem { get => this._isClickIconFoldItem; set => this._isClickIconFoldItem = value; }
[Category("其他")]
[Description("是否可以显示左侧图标")]
[Browsable(true)]
/// <summary>
/// 是否可以显示左侧图标
/// </summary>
public bool IsShowLeftIcon { get => this._showLeftIcon; set => this._showLeftIcon = value; }
/// <summary>
/// 当前显示区内的第一个显示项
/// </summary>
public TreeMenuItem? FirstShowItem { get => this._fristItem; }
[Category("样式")]
[Description("无图标占用空间(当某个项没有图标时,则图标位为空,字符串会左移,和其他的图标对其)")]
[Browsable(true)]
/// <summary>
/// 无图标占用空间(当某个项没有图标时,则图标位为空,字符串会左移,和其他的图标对其)
/// </summary>
public bool NoIconOccupyingSpace
{
get => _noIconOccupyingSpace;
set
{
if (_noIconOccupyingSpace == value) return;
_noIconOccupyingSpace = value;
this.Refresh(); // 重绘前会自动计算缩进
}
}
[Category("样式")]
[Description("项高度,建议和项的字体匹配,否则会很丑!")]
[Browsable(true)]
/// <summary>
/// 设置项高度
/// </summary>
public int ItemHight { get => _itemHight;
set {
// 限制最小值
_itemHight = value <= MIN_ITEM_HEIGHT ? MIN_ITEM_HEIGHT : value;
// 大小设置后需要重新计算显示区的
this.RefreshVisualArea(); // 重新计算显示区
this.Refresh();
}
}
[Category("样式")]
[Description("子项的缩进,最小值15")]
[Browsable(true)]
/// <summary>
/// 设置或获取子项的缩进距离
/// </summary>
public int Sindent
{
get => _sindent;
set
{
// 限制最小值
_sindent = value <= MIN_SINDENT ? MIN_SINDENT : value;
this.Refresh(); // 重绘前会自动计算缩进
}
}
[Category("样式")]
[Description("折叠图标,当为None不显示折叠图标,但是如果没有禁用折叠,将可以继续折叠")]
[Browsable(true)]
public FoldingIcon FoldIcon { get => this._foldIcon; set=> this._foldIcon = value; }
[Category("样式")]
[Description("未设置项样式的默认样式")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] // 这两句,可以在设计器中显示类的属性
[TypeConverter(typeof(ExpandableObjectConverter))]
/// <summary>
/// 未设置项样式的默认样式
/// </summary>
public ItemStyle DefaultItemStyle { get => _defaultItemStyle; set => _defaultItemStyle = value; }
[Category("样式")]
[Description("设置项的样式,如果同一个Level设置了多个样式,只会取最后一个设置的")]
[Browsable(true)]
/// <summary>
/// 设置项的样式,如果同一个Level设置了多个样式,只会取最后一个设置的
/// </summary>
public List<ItemStyle> ItemStyle { get => _itemStyle; set => _itemStyle = value; }
[Category("样式")]
[Description("选中项的样式")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] // 这两句,可以在设计器中显示类的属性
[TypeConverter(typeof(ExpandableObjectConverter))]
/// <summary>
/// 选中项的样式
/// </summary>
public ItemStyle SelectItemStyle { get => _selectItemStyle; set => _selectItemStyle = value; }
[Category("样式")]
[Description("光标悬停经过时项的样式")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] // 这两句,可以在设计器中显示类的属性
[TypeConverter(typeof(ExpandableObjectConverter))]
/// <summary>
/// 光标悬停经过时项的样式
/// </summary>
public ItemStyle HoverItemStyle { get => _hoverItemStyle; set => _hoverItemStyle = value; }
#endregion
#region ===声明事件===
[Category("Item事件")]
[Description("鼠标左键点击列表项事件。")]
[Browsable(true)]
/// <summary>
/// 鼠标左键点击列表项事件
/// <para>点击后如果是可以选中的,则默认优先选中,再调用此事件。</para>
/// <para>点击后如果是不可选中的,则默认优先调用折叠等方法,再调用此事件。</para>
/// </summary>
public event ClickItemHandler? LeftMouseClickItem;
public event ClickItemHandler? RightMouseClickItem;
[Category("Item事件")]
[Description("鼠标点击折叠或者展开项的事件(仅鼠标操作才触发)。")]
[Browsable(true)]
/// <summary>
/// 鼠标点击折叠或者展开项的事件(仅鼠标操作才触发)。
/// <para>需要知道是折叠操作还是展开操作,只需要判断项当前的状态,如果是展开状态,则就是进行折叠的操作。反之。</para>
/// </summary>
public event FoldItemHandler? MouseClickFoldItem;
[Category("Item事件")]
[Description("鼠标点击列表项的Check值被改变的事件(仅鼠标操作才触发)。")]
[Browsable(true)]
/// <summary>
/// 列表项的Check值被点击改变(仅鼠标操作才触发)。
/// <para>值改变前事件,因此可以取消其改变事件</para>
/// </summary>
public event ItemCheckedChangeHandler? MouseItemCheckedChange;
#endregion
public TreeMenu()
{
this._items = new TreeMenuItemCollection(this); // 创建项控制器
this.DoubleBuffered = true;
InitializeComponent();
// 初始化一些默认的样式
this._itemStyle.Add(new ItemStyle(1) { ForeColor = Color.Black });
this._itemStyle.Add(new ItemStyle(2) { ForeColor = Color.FromArgb(102, 102, 102) });
this._hoverItemStyle = new ItemStyle(-1) { BackColor = Color.FromArgb(50, Color.Red) };
this._selectItemStyle = new ItemStyle(-1) { BackColor = Color.FromArgb(234,85,0) };
this._defaultItemStyle = new ItemStyle(-1) { ForeColor = Color.FromArgb(102,102,102) };
// 绘制透明背景【但由于绘制后显示的文字不清晰,因此就不使用这个功能了。全局有两处相关代码,分别在控件初始化、控件绘制方法】
// 设置控件样式,支持透明背景
//SetStyle(ControlStyles.SupportsTransparentBackColor |
// ControlStyles.UserPaint |
// ControlStyles.AllPaintingInWmPaint |
// ControlStyles.OptimizedDoubleBuffer, true);
设置背景色为透明
//this.BackColor = Color.Transparent;
其他初始化代码...
}
#region ===绘制项===
/// <summary>
/// 调用方法前,需要提前计算好数据
/// </summary>
protected virtual void DrawItems(Graphics g)
{
if (this._showItems.Count == 0)
{
this.RefreshVisualArea(); // 重新计算显示区
}
if (this._showItems.Count > 0)
{
if (this._showItems[0].Item.Id != this._fristItem?.Id)
{
this.RefreshVisualArea(); // 重新计算显示区
}
}
foreach (var item in this._showItems)
{
this.DrawItem(g, item);
}
}
/// <summary>
/// 画列表项
/// </summary>
/// <param name="g"></param>
/// <param name="rect"></param>
/// <param name="item"></param>
protected virtual void DrawItem(Graphics g, ShowItem showItem)
{
// 【测试】仅仅测试时进行检查使用
//g.DrawRectangles(Pens.Black, rects.Values.ToArray());
// 绘制项的背景色
this.OnDrawItemBackground(g, showItem.BackgroundRect, showItem);
// 绘制左侧图标
this.OnDrawItemLeftIcon(g, showItem.LeftIconRect, showItem);
// 绘制文字图标
this.OnDrawItemString(g, showItem.StringRect, showItem);
// g.DrawRectangle(Pens.Green, showItem.FoldIconRect);
// 绘制右侧折叠图标
this.OnDrawItemFoldIcon(g, showItem.FoldIconRect, showItem);
}
/// <summary>
/// 绘制项背景
/// </summary>
/// <param name="g">画板</param>
/// <param name="rect">绘图区域,可能为空(rect.IsEmpty)为空则不应该画图</param>
/// <param name="item">正在绘制的项</param>
/// <param name="style">绘制这个项的样式</param>
protected virtual void OnDrawItemBackground(Graphics g, Rectangle rect, ShowItem item)
{
// 绘制项的背景色
if (!(item.Style.BackColor.IsEmpty || item.Style.BackColor == Color.Transparent))
{
using (Brush brush = new SolidBrush(item.Style.BackColor))
{
if (item.Style.Radius > 0)
{
var r = DrawPath.RoundedRectangle(rect, item.Style.Radius);
g.FillPath(brush, r);
}
else
{
g.FillRectangle(brush, rect);
}
}
}
}
/// <summary>
/// 绘制项左侧图标
/// </summary>
/// <param name="g">画板</param>
/// <param name="rect">绘图区域,可能为空(rect.IsEmpty)为空则不应该画图</param>
/// <param name="item">正在绘制的项</param>
/// <param name="style">绘制这个项的样式</param>
protected virtual void OnDrawItemLeftIcon(Graphics g, Rectangle rect, ShowItem item)
{
// 不用绘制则退出
if (this._showLeftIcon == false || rect.IsEmpty) return;
// 定义绘制的颜色
Color color = this._leftIconColor==null ? item.Style.ForeColor: (Color)this._leftIconColor;
if (item.Item.Icon != null)
{
Icon icon = IconHelper.GetFontIcon((IconType)item.Item.Icon, color, rect.Height);
g.DrawIcon(icon, rect);
}
}
/// <summary>
/// 绘制项文字
/// </summary>
/// <param name="g">画板</param>
/// <param name="rect">绘图区域,可能为空(rect.IsEmpty)为空则不应该画图</param>
/// <param name="item">正在绘制的项</param>
/// <param name="style">绘制这个项的样式</param>
protected virtual void OnDrawItemString(Graphics g, Rectangle rect, ShowItem item)
{
if (string.IsNullOrEmpty(item.Item.Name) == false)
{
Font font = item.Style.Font;
string text = item.Item.Name;
// 使用MeasureString测量文本宽度
SizeF textSize = g.MeasureString(text, font);
using (Brush brush = new SolidBrush(item.Style.ForeColor))
{
StringFormat format = new StringFormat()
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
FormatFlags = StringFormatFlags.NoWrap,
};
g.DrawString(text, font, brush, rect, format);
}
}
}
/// <summary>
/// 绘制项右侧图标
/// </summary>
/// <param name="g">画板</param>
/// <param name="rect">绘图区域,可能为空(rect.IsEmpty)为空则不应该画图</param>
/// <param name="item">正在绘制的项</param>
/// <param name="style">绘制这个项的样式</param>
protected virtual void OnDrawItemFoldIcon(Graphics g, Rectangle rect, ShowItem item)
{
if (rect.IsEmpty == false)
{
IconType iconTypeRight, iconTypeDown;
switch (this._foldIcon)
{
case FoldingIcon.None:
return;
case FoldingIcon.Caret:
iconTypeRight = IconType.CaretRight;
iconTypeDown = IconType.CaretDown;
break;
case FoldingIcon.ChevronCircle:
iconTypeRight = IconType.ChevronCircleRight;
iconTypeDown = IconType.ChevronCircleDown;
break;
case FoldingIcon.Angle:
iconTypeRight = IconType.AngleRight;
iconTypeDown = IconType.AngleDown;
break;
case FoldingIcon.Chevron:
iconTypeRight = IconType.ChevronRight;
iconTypeDown = IconType.ChevronDown;
break;
default:
return;
}
IconType iconType = item.Item.Expand ? iconTypeDown : iconTypeRight;
// 获取一个默认图标
Icon icon = IconHelper.GetFontIcon(iconType, item.Style.ForeColor, rect.Height);
// 绘制右侧图标
g.DrawIcon(icon, rect);
}
}
#endregion
#region ===重写功能事件===
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// 绘制透明背景【但由于绘制后显示的文字不清晰,因此就不使用这个功能了。全局有两处相关代码,分别在控件初始化、控件绘制方法】
//e.Graphics.FillRectangle(new SolidBrush(this.BackColor), e.ClipRectangle);
绘制前的设置,确保文字渲染清晰
//e.Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
// 每次重新绘图前必须重新计算是否选中等样式的计算
this._showItems.ForEach(x => x.Style = this.GetItemStyle(x.Item));
// 重新绘制
e.Graphics.Clear(this.BackColor);
this.DrawItems(e.Graphics);
}
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
if (this._fristItem == null || this._items.Count == 0) return;
Point mousePos = Cursor.Position; // 获取当前鼠标位置
mousePos = this.PointToClient(mousePos); // 转换为控件相对坐标
if (this.ClientRectangle.Contains(mousePos))
{
if (e.Delta > 0)
{
var item = this._items.FindPreviousItem(this._fristItem);
if (item == null) return; // 当前已经是第一个了
this._fristItem = item;
this._hoverItem = null; // 只要滚动移位,原本悬停的位置就变动了
this.Refresh();
}
else if (e.Delta < 0)
{
int maxCount = (int)((this.Height - 1) / this._itemHight) + 1; // 计算显示项的数量
if (this._showItems.Count < maxCount - 1) return; // 当前选中的项少于了最大项-1,则就不能向下滚动了
var item = this._items.FindNextItem(this._fristItem);
if (item == null) return; // 当前已经是最后一个
this._fristItem = item;
this._hoverItem = null; // 只要滚动移位,原本悬停的位置就变动了
this.Refresh();
}
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
//Point mousePos = Cursor.Position; // 获取当前鼠标位置
//mousePos = this.PointToClient(mousePos); // 转换为控件相对坐标
if (this._hoverItem == null)
{
for (int i = 0; i < this._showItems.Count; i++)
{
if (this._showItems[i].Rect.Contains(e.Location))
{
this._hoverItem = this._showItems[i];
this.Refresh();
}
}
}
else
{
if (this._hoverItem.Rect.Contains(e.Location))
{
return;
}
else
{
for (int i = 0; i < this._showItems.Count; i++)
{
if (this._showItems[i].Rect.Contains(e.Location))
{
this._hoverItem = this._showItems[i];
this.Refresh();
}
}
}
}
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
// 大小设置后需要重新计算显示区的
this.RefreshVisualArea(); // 重新计算显示区
}
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
for (int i = 0; i < this._showItems.Count; i++)
{
if (this._showItems[i].Rect.Contains(e.Location))
{
if (e.Button == MouseButtons.Right)
{
if (this.RightMouseClickItem != null) this.RightMouseClickItem(this, this._showItems[i].Item);
}
else if (e.Button == MouseButtons.Left)
{
bool change_item = false;
// 这里的前提是可以被选中(如分组就不能被选中)
if (this._selectItem?.Item.Id != this._showItems[i].Item.Id)
{
change_item = true; // 更换了选中项
}
this._selectItem = this._showItems[i]; // 设置选中项
// 判断是否为可折叠的节点
bool folding = this.UnfoldOrCollapseItemOperation(this._showItems[i],e.Location);
if (folding) this.RefreshVisualArea(); // 折叠后还需要重新计算绘图区
// 更换选中项后需要重新绘制、折叠或展开后也是要重新绘制
if (change_item == true || folding == true) this.Refresh();
if (this.LeftMouseClickItem != null)
{
this.LeftMouseClickItem(this, this._showItems[i].Item);
}
}
}
}
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
this._hoverItem = null;
this.Refresh();
}
#endregion
/// <summary>
/// 获取指定位置的项
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public TreeMenuItem? GetItemByPoint(Point point)
{
for (int i = 0; i < this._showItems.Count; i++)
{
if (this._showItems[i].Rect.Contains(point))
{
return this._showItems[i].Item;
}
}
return null;
}
#region ===其他私有方法===
/// <summary>
/// 计算每个项的绘图区域
/// </summary>
/// <param name="rect"></param>
/// <param name="item"></param>
/// <param name="style"></param>
/// <returns></returns>
protected Dictionary<string, Rectangle> GetItemDrawRect(TreeMenuItem item,Rectangle rect, ItemStyle style)
{
Dictionary<string, Rectangle> dr = new Dictionary<string, Rectangle>();
//定义绘图区
Rectangle r = rect.Shifting(1, 1, -4, -2);
dr["background"] = r;
dr["background"].Shifting(0, -1, 0, 0); // 做一个偏移,要不然画出的图形有点偏
// 根据字体的高度,重新计算绘图区域(绘图区域不是整个项的全部区域)
if (r.Height > style.Font.Height)
{
int diff = r.Height - style.Font.Height;
r.Y = r.Y + diff / 2;
r.Height = style.Font.Height;
}
dr["drawArea"] = r; // 保存绘图区域
// 计算缩进
int sindent = (int)((item.Level - 1) * this._sindent);
r.X += sindent;
r.Width -= sindent;
// 计算左侧图标
dr["left"] = new Rectangle(r.X, r.Y, r.Height, r.Height);
// 判断当前列表项是否设置了图标
if ((this._noIconOccupyingSpace == true && item.Icon == null) || this._showLeftIcon == false)
{
dr["left"] = Rectangle.Empty;
}
// 计算展开与折叠图标位置
dr["fold_icon"] = new Rectangle(r.Right - r.Height, r.Y, r.Height, r.Height);
if (item.SubItem.Count == 0) dr["fold_icon"] = Rectangle.Empty;
// 正常情况显示
dr["string"] = new Rectangle(r.X + 1 + dr["left"].Width + 1, r.Y, r.Width - dr["fold_icon"].Width - (dr["left"].X + 2), r.Height);
return dr;
}
/// <summary>
/// 刷新显示区(重新计算显示区域)
/// </summary>
protected void RefreshVisualArea()
{
List<TreeMenuItem> items = this.CalculateDisplayItems();
this._showItems.Clear();
for (int i = 0; i < items.Count; i++)
{
// 绘制 一级目录
Rectangle rect = new Rectangle(0, i * this.ItemHight, this.Width - 1, this.ItemHight);
var showItem = new ShowItem(items[i], rect);
showItem.Style = this.GetItemStyle(items[i]); // 设置样式
var dict = this.GetItemDrawRect(items[i], rect, showItem.Style); // 计算绘图区域
// 设置获取的值
showItem.BackgroundRect = dict["background"];
showItem.DrawArea = dict["drawArea"];
showItem.LeftIconRect = dict["left"];
showItem.StringRect = dict["string"];
showItem.FoldIconRect = dict["fold_icon"];
this._showItems.Add(showItem);
}
}
/// <summary>
/// 根据不同的项获取不同的设置样式
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private ItemStyle GetItemStyle(TreeMenuItem item)
{
// 获取当前项的样式
var styles = this._itemStyle.Where(x => x.Level == item.Level).ToList();
ItemStyle style = styles.Count > 0 ? styles.Last() : style = this._defaultItemStyle;
// 判断当前项是否悬停选择的项
if (this._hoverItem?.Item.Id == item.Id) style = this._hoverItemStyle;
// 判断是否是选中的项
if (this._selectItem?.Item.Id == item.Id) style = this._selectItemStyle;
return style;
}
/// <summary>
/// 折叠或展开项操作(事件)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private bool UnfoldOrCollapseItemOperation(ShowItem item,Point point)
{
if (this._isClickIconFoldItem == true) // 判断是否必须点击折叠图标在可以
{
if (item.FoldIconRect.Contains(point) == false) return false; // 没有点击图标,则跳过
}
// 如果是点击任何部分都可以折叠的执行后续
if (item.Item.HasChlid == false) return false; // 验证是否有子项,没有则不用折叠
if (item.Item.SubItem.Where(x => x.Visible == true).Count() == 0) return false; // 当列表项中没有可视项,也会被返回
bool has = true;
if (this.MouseClickFoldItem != null)
{
this.MouseClickFoldItem(this, item.Item,ref has);
}
if (has == false) return false;
item.Item.Expand = !item.Item.Expand; // 进行取反操作
return true;
}
/// <summary>
/// 勾选选项操作(事件)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private bool ChoiceItemOperation(ShowItem item, Point point)
{
if (item.LeftIconRect.Contains(point) == false) return false;
var checkedChange = true;
// 调用事件
if (this.MouseItemCheckedChange != null) this.MouseItemCheckedChange(this, item.Item, ref checkedChange);
if (checkedChange == true)
{
// null→true→false→true→false...顺序设置
if (item.Item.Checked == null) item.Item.Checked = true;
else if (item.Item.Checked == true) item.Item.Checked = false;
else item.Item.Checked = true;
}
return checkedChange;
}
/// <summary>
/// 递归查找第一个索引
/// </summary>
/// <param name="item"></param>
/// <param name="indexs"></param>
/// <param name="index"></param>
/// <returns></returns>
private TreeMenuItem? _FindItem(TreeMenuItem item, List<int> indexs, int index)
{
// 当前查找的索引长度已经超过了查找的深度,这个时候还没有返回,那么真的就没有了
if (indexs.Count - 1 < index)
{
return null;
}
else if (indexs.Count - 1 == index)
{
// 当前查找的深度刚好是定位索引引列表的最后一个
// 定位索引的,查找的索引值是否超出了边界,超过了则就是没有对应的项
if (indexs[index] > item.SubItem.Count - 1) return null; // 结束了查找
// 找到了对应的值
return item.SubItem[indexs[index]];
}
else
{
// 定位索引,还没有找到最后一位
// 定位索引的,查找的索引值是否超出了边界,超过了则就是没有对应的项
if (indexs[index] > item.SubItem.Count - 1) return null;
//没有的则继续查找
return _FindItem(item.SubItem[indexs[index]], indexs, index + 1);
}
}
#endregion
#region ===计算绘图区===
/// <summary>
/// 计算显示的项
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private List<TreeMenuItem> CalculateDisplayItems()
{
List<TreeMenuItem> items = new List<TreeMenuItem>();
if (this._items.Count == 0) return items; // 这里应当返回示例数据
// 只有初始没有设置值,进行一个初始设置
if (this._fristItem == null) this._fristItem = this._items[0];
int maxCount = (int)((this.Height - 1) / this._itemHight) + 1; // 计算显示项的数量
TreeMenuItem? item = this._FindItem(this._items.Root, this._fristItem.Indexs, 0);
if (item == null) throw new Exception("当前第一个显示项的定位索引未找到数据!可以重置索引!");
items.Add(item);
var it = item;
// 第一个添加完毕也是需要对折叠进行一个逻辑判断
if (it.Expand == false)
{
// 这里不能直接使用next寻找下一个项,寻找下一个项的任务必须由 FindNextItem(it)函数完成。
if (it.HasChlid == true)
{
it = it.DeepestLastChild; // 获取子子项的最末尾,方便FindNextItem(it)调用,然后直接找到it的next对象
}
}
int count = 1;
while (true)
{
if (count >= maxCount) return items; // 招够数量了就返回
it = this._items.FindNextItem(it);
if (it != null)
{
// 当前项是隐藏项,那么查询下一个则为当前项的下一个
if (it.Visible == false)
{
// 这里不能直接使用next寻找下一个项,寻找下一个项的任务必须由 FindNextItem(it)函数完成。
if (it.HasChlid == true)
{
it = it.DeepestLastChild; // 获取子子项的最末尾,方便FindNextItem(it)调用,然后直接找到it的next对象
}
continue;
}
// 不管展开与不展开,都是要显示的。
items.Add(it);
count++;
// 根据折叠或展开再判断
if (it.Expand == false)
{
// 这里不能直接使用next寻找下一个项,寻找下一个项的任务必须由 FindNextItem(it)函数完成。
if (it.HasChlid == true)
{
it = it.DeepestLastChild; // 获取子子项的最末尾,方便FindNextItem(it)调用,然后直接找到it的next对象
}
}
}
else
{
return items;
}
}
}
#endregion
}
/// <summary>
/// 点击列表项事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void ClickItemHandler(object sender, TreeMenuItem e);
/// <summary>
/// 折叠或展开列表子项事件(仅鼠标点击才触发,编程设置后刷新显示不触发)
/// </summary>
/// <param name="sender">控件对象</param>
/// <param name="e">被点击的项</param>
/// <param name="agree">是否同意折叠或展开。默认是true</param>
public delegate void FoldItemHandler(object sender, TreeMenuItem e, ref bool agree);
/// <summary>
/// 列表项的Check值被点击改变(仅鼠标点击才触发,编程设置后刷新显示不触发)
/// </summary>
/// <param name="sender">控件对象</param>
/// <param name="e">被点击的项</param>
/// <param name="agree">是否同意改变。默认是true</param>
public delegate void ItemCheckedChangeHandler(object sender, TreeMenuItem e, ref bool agree);
}
TreeMenuItem.cs 文件
namespace XiaoHeiControls.CollapseMenu
{
public class TreeMenuItem
{
/// <summary>
/// Id为默认值,创建后不可修改
/// </summary>
private string _id = Guid.NewGuid().ToString();
internal List<int> _indexs = new List<int>();
private string _name = string.Empty;
private string _value = string.Empty;
private IconType? _icon = null;
private object? _tag = null;
private bool? _checked = false;
private bool _expand = true;
private bool _visible = true;
internal TreeMenuItem? _parent = null;
private List<TreeMenuItem> _sub = new List<TreeMenuItem>();
#region ===公开属性===
/// <summary>
/// 项的Id
/// </summary>
public string Id { get { return _id; } }
/// <summary>
/// 项的索引(移动项需要重置索引的)
/// </summary>
public List<int> Indexs { get { return _indexs; } }
/// <summary>
/// 名称
/// </summary>
public string Name { get => _name; set => _name = value; }
/// <summary>
/// 值
/// </summary>
public string Value { get => _value; set => _value = value; }
/// <summary>
/// 图标
/// </summary>
public IconType? Icon { get => _icon; set => _icon = value; }
/// <summary>
/// 绑定数据
/// </summary>
public object? Tag { get => _tag; set => _tag = value; }
/// <summary>
/// 深度等级
/// </summary>
public int Level { get => this._indexs.Count;}
/// <summary>
/// 是否选中,设置值,只能是bool类型,不能是null
/// </summary>
public bool? Checked { get => _checked;
set {
if (value == null) return;
this.CheckedSubItem((bool)value);
this.CheckedParetItem();
}
}
/// <summary>
/// 是否展开了子项
/// </summary>
public bool Expand { get => _expand; set => _expand = value; }
/// <summary>
/// 是否可视的
/// </summary>
public bool Visible { get => _visible; set => _visible = value; }
/// <summary>
/// 当前项的父类,如果为null,则没有父类
/// </summary>
public TreeMenuItem? Parent { get => _parent; }
/// <summary>
/// 全部子项【虽然这里是List,是方便调用循环,请勿从这里添加子项】
/// </summary>
public List<TreeMenuItem> SubItem { get => _sub; set => _sub = value; }
/// <summary>
/// 是否有子类
/// </summary>
public bool HasChlid { get=>this._sub.Count > 0;}
/// <summary>
/// 当前节点是否是根节点(如果直接新建项,没有使用Add方法添加的节点也是根节点)
/// </summary>
internal bool IsRoot { get => this.Level == 0; }
/// <summary>
/// 当前节点是否是同级别(同一个父类分支)的最后一项
/// <para>使用前可以判断下是否为Root,Root是没有IsFrist或IsLast</para>
/// </summary>
public bool IsLast { get => this.Next() == null; }
/// <summary>
/// 当前节点是否是同级别(同一个父类分支)的第一项。
/// <para>使用前可以判断下是否为Root,Root是没有IsFrist或IsLast</para>
/// </summary>
public bool IsFrist { get => this.Previous() == null; }
#endregion
/// <summary>
/// 创建项清单
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="icon"></param>
public TreeMenuItem(string name, string value = "", IconType? icon = null)
{
_name = name;
_value = value;
_icon = icon;
}
/// <summary>
/// 添加子项
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="icon"></param>
/// <returns></returns>
public TreeMenuItem AddSubItem(string name, string value = "", IconType? icon = null)
{
var sub = new TreeMenuItem(name, value, icon);
return this.AddSubItem(sub);
}
/// <summary>
/// 添加子项
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public TreeMenuItem AddSubItem(TreeMenuItem item)
{
if (item.Level == 9) throw new NotSupportedException("MenuItem添加子项失败!原因:MenuItem最大深度为9级!9级不能添加子项!");
item._parent = this;
item._indexs = this._indexs.ToList(); // 使用ToList方法复制一个
item._indexs.Add(this._sub.Count); // 添加所在位置
_sub.Add(item);
return item;
}
/// <summary>
/// 删除子项
/// </summary>
/// <param name="index"></param>
public void DelSubItem(int index)
{
_sub.RemoveAt(index);
}
/// <summary>
/// 删除子项
/// </summary>
/// <param name="item"></param>
public void DelSubItem(TreeMenuItem item)
{
_sub.Remove(item);
}
/// <summary>
/// 根据当前项的index,递归重置所有子项的索引
/// </summary>
internal void ResetIndexs()
{
for (int i = 0; i < this._sub.Count; i++)
{
this._sub[i]._indexs = this._indexs.ToList(); // 使用ToList方法复制一个
this._sub[i]._indexs.Add(i); // 添加所在位置
// 调用子项方法 递归 设置子子项indexs
this._sub[i].ResetIndexs();
}
}
/// <summary>
/// 查找子项
/// </summary>
/// <param name="text">查找的文本,仅支持Name字段</param>
/// <param name="isAll">是否查找所有的子项。true:递归查找所有子项,false:只查找当前项的子项</param>
/// <param name="isLike">是否以包含的方式查找。true:只要Name字段内容包含text即可,false:必须完全匹配</param>
/// <returns></returns>
public List<TreeMenuItem> FindChilds(string text,bool isAll = true, bool isLike = false)
{
List<TreeMenuItem> items = new List<TreeMenuItem>();
for (int i = 0; i < this._sub.Count; i++)
{
if(isLike == true)
{
if (this._sub[i].Name.IndexOf(text) >= 0) items.Add(this._sub[i]);
}
else
{
if (this._sub[i].Name == text) items.Add(this._sub[i]);
}
if (isAll == true) items.AddRange(this._sub[i].FindChilds(text, isAll, isLike));
}
return items;
}
/// <summary>
/// 查找子项
/// </summary>
/// <param name="id">查找的id,仅支持Name字段</param>
/// <param name="isAll">是否查找所有的子项。true:递归查找所有子项,false:只查找当前项的子项</param>
/// <returns></returns>
public TreeMenuItem? FindChildById(string id, bool isAll = true)
{
for (int i = 0; i < this._sub.Count; i++)
{
if (this._sub[i].Id == id) return this._sub[i];
if (isAll == true)
{
var subRes = this._sub[i].FindChildById(id, isAll);
if (subRes != null) return subRes;
}
}
return null;
}
/// <summary>
/// 获取下一级别(只能获取同一个父类中的同级别子项)
/// </summary>
/// <returns>只有父类为null,或者已经是同级别最后一个,也就不能查找到下一个对象</returns>
/// <exception cref="Exception"></exception>
public TreeMenuItem? Next()
{
if (this._parent == null) throw new Exception("当前项没有父类,无法计算下一个!"); // 顶级父类为null则没有下一级
int index = this._parent._sub.FindIndex(x=>x.Id == this._id);
if (index == -1) throw new Exception("在父类查找子项没有找到,出现了莫名其妙的错误!");
if (index == this._parent._sub.Count -1) return null; // 已经是最后一项了
return this._parent._sub[index + 1];
}
/// <summary>
/// 获取上一个级别(只能获取同一个父类中的同级别子项)
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public TreeMenuItem? Previous()
{
if (this._parent == null) throw new Exception("当前项没有父类,无法计算上一个!");
int index = this._parent._sub.FindIndex(x => x.Id == this._id);
if (index == -1) throw new Exception("在父类查找子项没有找到,出现了莫名其妙的错误!");
if (index == 0) return null; // 已经是第一项了
return this._parent._sub[index - 1];
}
/// <summary>
/// 获取当前项的最后一个子项
/// </summary>
/// <returns></returns>
public TreeMenuItem? LastChild
{
get {
if (this.HasChlid == false) return null;
return this._sub[_sub.Count - 1];
}
}
/// <summary>
/// 获取当前项的第一个子项
/// </summary>
/// <returns></returns>
public TreeMenuItem? FirstChild
{
get {
if (this.HasChlid == false) return null;
return this._sub[0];
}
}
/// <summary>
/// 获取当前项最后最后子子项的那一个(当前项的LastChild,获取后再获取LastChild)
/// </summary>
/// <returns></returns>
public TreeMenuItem? DeepestLastChild
{
get {
var sub = this.LastChild; // 当前项的子项
while (sub?.HasChlid == true)
{
sub = sub.LastChild; // 循环获取最后一个,直至没有了子项
}
return sub;
}
}
/// <summary>
/// 递归设置子类的值
/// </summary>
/// <param name="val"></param>
private void CheckedSubItem(bool val)
{
this._checked = val;
this.SubItem.ForEach(x => x.CheckedSubItem(val));
}
/// <summary>
/// 递归设置父类值,
/// </summary>
private void CheckedParetItem()
{
if (this.IsRoot == true) return; // 到达root即便就需要停止设置
// 当所有子项还有是半选中状态,则父类肯定还是半选中状态
if (this.Parent?.SubItem.Where(x => x.Checked == null).Count() > 0)
{
this.Parent._checked = null;
}
else
{
// 全部都是选中的,则父类也应该是选中的
if (this.Parent?.SubItem.Where(x => x.Checked == true).Count() == 0)
{
this.Parent._checked = false;
}
// 全部没有选中的,则父类也应该是没有选中的
else if (this.Parent?.SubItem.Where(x => x.Checked == false).Count() == 0)
{
this.Parent._checked = true;
}
else
{
// 不是这种情况,那么肯定是既有选中,又有没有选中
if (this.Parent!=null) this.Parent._checked = null;
}
}
this.Parent?.CheckedParetItem();
}
}
}
=Comm文件夹=======
以下代码不是我写的,在网上找到,当时也没找到作者。
IconHelper.cs
namespace XiaoHeiControls.Comm
{
/// <summary>
/// 图标帮助类
/// </summary>
public sealed class IconHelper
{
public static Bitmap GetFontImage(IconType type, Color color, int size)
{
var bmp = new Bitmap(size, size);
var g = Graphics.FromImage(bmp);
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
g.InterpolationMode = InterpolationMode.HighQualityBilinear;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.SmoothingMode = SmoothingMode.HighQuality;
var ch = char.ConvertFromUtf32((int)type);
var font = GetAdjustedFont(g, ch, size, size, 4, true);
var stringSize = g.MeasureString(ch, font, size);
float w = stringSize.Width;
float h = stringSize.Height;
// center icon
float left = (size - w) / 2;
float top = (size - h) / 2;
// Draw string to screen.
using (var brush = new SolidBrush(color))
{
g.DrawString(ch, font, brush, new PointF(left, top));
}
return bmp;
}
public static Icon GetFontIcon(IconType type, Color color, int size)
{
return Icon.FromHandle(GetFontImage(type,color,size).GetHicon());
}
/// <summary>
/// 获取调整后的图标字体
/// </summary>
/// <param name="g">画板</param>
/// <param name="graphicString">图形字符串</param>
/// <param name="containerWidth">宽度</param>
/// <param name="maxFontSize">最大字体尺寸</param>
/// <param name="minFontSize">最小字体尺寸</param>
/// <param name="smallestOnFail">是否最小故障</param>
/// <returns></returns>
private static Font GetAdjustedFont(Graphics g, string graphicString, int containerWidth, int maxFontSize, int minFontSize, bool smallestOnFail)
{
for (double adjustedSize = maxFontSize; adjustedSize >= minFontSize; adjustedSize = adjustedSize - 0.5)
{
Font testFont = GetIconFont((float)adjustedSize);
// Test the string with the new size
SizeF adjustedSizeNew = g.MeasureString(graphicString, testFont);
if (containerWidth > Convert.ToInt32(adjustedSizeNew.Width))
{
// Fits! return it
return testFont;
}
}
// Could not find a font size
// return min or max or maxFontSize?
return GetIconFont(smallestOnFail ? minFontSize : maxFontSize);
}
/// <summary>
/// 获取图标字体
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
private static Font GetIconFont(float size)
{
return new Font(Fonts.Families[0], size, GraphicsUnit.Point);
}
static IconHelper()
{
InitialiseFont();
}
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
private static extern IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, IntPtr pdv, [System.Runtime.InteropServices.In] ref uint pcFonts);
private static readonly PrivateFontCollection Fonts = new PrivateFontCollection();
private static void InitialiseFont()
{
try
{
unsafe
{
fixed (byte* pFontData = Resource1.fontawesome_webfont)
{
uint dummy = 0;
Fonts.AddMemoryFont((IntPtr)pFontData, Resource1.fontawesome_webfont.Length);
AddFontMemResourceEx((IntPtr)pFontData, (uint)Resource1.fontawesome_webfont.Length, IntPtr.Zero, ref dummy);
}
}
}
catch (Exception)
{
// log?
}
}
}
/// <summary>
/// 图标类型
/// <para>具体图标可以查询:https://fontawesome.dashgame.com/#google_vignette</para>
/// </summary>
public enum IconType
{
Adjust = 0xf042,
Adn = 0xf170,
AlignCenter = 0xf037,
AlignJustify = 0xf039,
AlignLeft = 0xf036,
AlignRight = 0xf038,
Ambulance = 0xf0f9,
Anchor = 0xf13d,
Android = 0xf17b,
AngleDoubleDown = 0xf103,
AngleDoubleLeft = 0xf100,
AngleDoubleRight = 0xf101,
AngleDoubleUp = 0xf102,
AngleDown = 0xf107,
AngleLeft = 0xf104,
AngleRight = 0xf105,
AngleUp = 0xf106,
Apple = 0xf179,
Archive = 0xf187,
ArrowCircleDown = 0xf0ab,
ArrowCircleLeft = 0xf0a8,
ArrowCircleODown = 0xf01a,
ArrowCircleOLeft = 0xf190,
ArrowCircleORight = 0xf18e,
ArrowCircleOUp = 0xf01b,
ArrowCircleRight = 0xf0a9,
ArrowCircleUp = 0xf0aa,
ArrowDown = 0xf063,
ArrowLeft = 0xf060,
ArrowRight = 0xf061,
ArrowUp = 0xf062,
Arrows = 0xf047,
ArrowsAlt = 0xf0b2,
ArrowsH = 0xf07e,
ArrowsV = 0xf07d,
Asterisk = 0xf069,
Automobile = 0xf1b9,
Backward = 0xf04a,
Ban = 0xf05e,
Bank = 0xf19c,
BarChartO = 0xf080,
Barcode = 0xf02a,
Bars = 0xf0c9,
Beer = 0xf0fc,
Behance = 0xf1b4,
BehanceSquare = 0xf1b5,
Bell = 0xf0f3,
BellO = 0xf0a2,
Bitbucket = 0xf171,
BitbucketSquare = 0xf172,
Bitcoin = 0xf15a,
Bold = 0xf032,
Bolt = 0xf0e7,
Bomb = 0xf1e2,
Book = 0xf02d,
Bookmark = 0xf02e,
BookmarkO = 0xf097,
Briefcase = 0xf0b1,
Btc = 0xf15a,
Bug = 0xf188,
Building = 0xf1ad,
BuildingO = 0xf0f7,
Bullhorn = 0xf0a1,
Bullseye = 0xf140,
Cab = 0xf1ba,
Calendar = 0xf073,
CalendarO = 0xf133,
Calculator = 0xf1ec,
Camera = 0xf030,
CameraRetro = 0xf083,
Car = 0xf1b9,
CaretDown = 0xf0d7,
CaretLeft = 0xf0d9,
CaretRight = 0xf0da,
CaretSquareODown = 0xf150,
CaretSquareOLeft = 0xf191,
CaretSquareORight = 0xf152,
CaretSquareOUp = 0xf151,
CaretUp = 0xf0d8,
Certificate = 0xf0a3,
Chain = 0xf0c1,
ChainBroken = 0xf127,
Check = 0xf00c,
CheckCircle = 0xf058,
CheckCircleO = 0xf05d,
CheckSquare = 0xf14a,
CheckSquareO = 0xf046,
ChevronCircleDown = 0xf13a,
ChevronCircleLeft = 0xf137,
ChevronCircleRight = 0xf138,
ChevronCircleUp = 0xf139,
ChevronDown = 0xf078,
ChevronLeft = 0xf053,
ChevronRight = 0xf054,
ChevronUp = 0xf077,
Child = 0xf1ae,
Circle = 0xf111,
CircleO = 0xf10c,
CircleONotch = 0xf1ce,
CircleThin = 0xf1db,
Clipboard = 0xf0ea,
ClockO = 0xf017,
Cloud = 0xf0c2,
CloudDownload = 0xf0ed,
CloudUpload = 0xf0ee,
Cny = 0xf157,
Code = 0xf121,
CodeFork = 0xf126,
Codepen = 0xf1cb,
Coffee = 0xf0f4,
Cog = 0xf013,
Cogs = 0xf085,
Columns = 0xf0db,
Comment = 0xf075,
CommentO = 0xf0e5,
Comments = 0xf086,
CommentsO = 0xf0e6,
Compass = 0xf14e,
Compress = 0xf066,
Copy = 0xf0c5,
CreditCard = 0xf09d,
Crop = 0xf125,
Crosshairs = 0xf05b,
Css3 = 0xf13c,
Cube = 0xf1b2,
Cubes = 0xf1b3,
Cut = 0xf0c4,
Cutlery = 0xf0f5,
Dashboard = 0xf0e4,
Database = 0xf1c0,
Dedent = 0xf03b,
Delicious = 0xf1a5,
Desktop = 0xf108,
Deviantart = 0xf1bd,
Digg = 0xf1a6,
Dollar = 0xf155,
DotCircleO = 0xf192,
Download = 0xf019,
Dribbble = 0xf17d,
Dropbox = 0xf16b,
Drupal = 0xf1a9,
Edit = 0xf044,
Eject = 0xf052,
EllipsisH = 0xf141,
EllipsisV = 0xf142,
Empire = 0xf1d1,
Envelope = 0xf0e0,
EnvelopeO = 0xf003,
EnvelopeSquare = 0xf199,
Eraser = 0xf12d,
Eur = 0xf153,
Euro = 0xf153,
Exchange = 0xf0ec,
Exclamation = 0xf12a,
ExclamationCircle = 0xf06a,
ExclamationTriangle = 0xf071,
Expand = 0xf065,
ExternalLink = 0xf08e,
ExternalLinkSquare = 0xf14c,
Eye = 0xf06e,
EyeSlash = 0xf070,
Facebook = 0xf09a,
FacebookSquare = 0xf082,
FastBackward = 0xf049,
FastForward = 0xf050,
Fax = 0xf1ac,
Female = 0xf182,
FighterJet = 0xf0fb,
File = 0xf15b,
FileArchiveO = 0xf1c6,
FileAudioO = 0xf1c7,
FileCodeO = 0xf1c9,
FileExcelO = 0xf1c3,
FileImageO = 0xf1c5,
FileMovieO = 0xf1c8,
FileO = 0xf016,
FilePdfO = 0xf1c1,
FilePhotoO = 0xf1c5,
FilePictureO = 0xf1c5,
FilePowerpointO = 0xf1c4,
FileSoundO = 0xf1c7,
FileText = 0xf15c,
FileTextO = 0xf0f6,
FileVideoO = 0xf1c8,
FileWordO = 0xf1c2,
FileZipO = 0xf1c6,
FilesO = 0xf0c5,
Film = 0xf008,
Filter = 0xf0b0,
Fire = 0xf06d,
FireExtinguisher = 0xf134,
Flag = 0xf024,
FlagCheckered = 0xf11e,
FlagO = 0xf11d,
Flash = 0xf0e7,
Flask = 0xf0c3,
Flickr = 0xf16e,
FloppyO = 0xf0c7,
Folder = 0xf07b,
FolderO = 0xf114,
FolderOpen = 0xf07c,
FolderOpenO = 0xf115,
Font = 0xf031,
Forward = 0xf04e,
Foursquare = 0xf180,
FrownO = 0xf119,
Gamepad = 0xf11b,
Gavel = 0xf0e3,
Gbp = 0xf154,
Ge = 0xf1d1,
Gear = 0xf013,
Gears = 0xf085,
Gift = 0xf06b,
Git = 0xf1d3,
GitSquare = 0xf1d2,
Github = 0xf09b,
GithubAlt = 0xf113,
GithubSquare = 0xf092,
Gittip = 0xf184,
Glass = 0xf000,
Globe = 0xf0ac,
Google = 0xf1a0,
GooglePlus = 0xf0d5,
GooglePlusSquare = 0xf0d4,
GraduationCap = 0xf19d,
Group = 0xf0c0,
HSquare = 0xf0fd,
HackerNews = 0xf1d4,
HandODown = 0xf0a7,
HandOLeft = 0xf0a5,
HandORight = 0xf0a4,
HandOUp = 0xf0a6,
HddO = 0xf0a0,
Header = 0xf1dc,
Headphones = 0xf025,
Heart = 0xf004,
HeartO = 0xf08a,
History = 0xf1da,
Home = 0xf015,
HospitalO = 0xf0f8,
HourglassHalf = 0xf252,
HourglassEnd = 0xf253,
Html5 = 0xf13b,
Image = 0xf03e,
Inbox = 0xf01c,
Indent = 0xf03c,
Info = 0xf129,
InfoCircle = 0xf05a,
Inr = 0xf156,
Instagram = 0xf16d,
Institution = 0xf19c,
Italic = 0xf033,
Joomla = 0xf1aa,
Jpy = 0xf157,
Jsfiddle = 0xf1cc,
Key = 0xf084,
KeyboardO = 0xf11c,
Krw = 0xf159,
Language = 0xf1ab,
Laptop = 0xf109,
Leaf = 0xf06c,
Legal = 0xf0e3,
LemonO = 0xf094,
LevelDown = 0xf149,
LevelUp = 0xf148,
LifeBouy = 0xf1cd,
LifeRing = 0xf1cd,
LifeSaver = 0xf1cd,
LightbulbO = 0xf0eb,
LineChart = 0xf201,
Link = 0xf0c1,
Linkedin = 0xf0e1,
LinkedinSquare = 0xf08c,
Linux = 0xf17c,
List = 0xf03a,
ListAlt = 0xf022,
ListOl = 0xf0cb,
ListUl = 0xf0ca,
LocationArrow = 0xf124,
Lock = 0xf023,
LongArrowDown = 0xf175,
LongArrowLeft = 0xf177,
LongArrowRight = 0xf178,
LongArrowUp = 0xf176,
Magic = 0xf0d0,
Magnet = 0xf076,
MailForward = 0xf064,
MailReply = 0xf112,
MailReplyAll = 0xf122,
Male = 0xf183,
MapMarker = 0xf041,
Maxcdn = 0xf136,
Medkit = 0xf0Fa,
MehO = 0xf11a,
Microphone = 0xf130,
MicrophoneSlash = 0xf131,
Minus = 0xf068,
MinusCircle = 0xf056,
MinusSquare = 0xf146,
MinusSquareO = 0xf147,
Mobile = 0xf10b,
MobilePhone = 0xf10b,
Money = 0xf0d6,
MoonO = 0xf186,
MortarBoard = 0xf19d,
Music = 0xf001,
Navicon = 0xf0c9,
Openid = 0xf19b,
Outdent = 0xf03b,
Pagelines = 0xf18c,
PaperPlane = 0xf1d8,
PaperPlaneO = 0xf1d9,
Paperclip = 0xf0c6,
Paragraph = 0xf1dd,
Paste = 0xf0ea,
Pause = 0xf04c,
PauseCircle = 0xf28b,
PauseCircleO = 0xf28c,
Paw = 0xf1b0,
Pencil = 0xf040,
PencilSquare = 0xf14b,
PencilSquareO = 0xf044,
Phone = 0xf095,
PhoneSquare = 0xf098,
Photo = 0xf03e,
PictureO = 0xf03e,
PiedPiper = 0xf1a7,
PiedPiperAlt = 0xf1a8,
PiedPiperSquare = 0xf1a7,
Pinterest = 0xf0d2,
PinterestSquare = 0xf0d3,
Plane = 0xf072,
Play = 0xf04b,
PlayCircle = 0xf144,
PlayCircleO = 0xf01d,
Plus = 0xf067,
PlusCircle = 0xf055,
PlusSquare = 0xf0fe,
PlusSquareO = 0xf196,
PowerOff = 0xf011,
Print = 0xf02f,
PuzzlePiece = 0xf12e,
QQ = 0xf1d6,
Rrcode = 0xf029,
Ruestion = 0xf128,
RuestionCircle = 0xf059,
RuoteLeft = 0xf10d,
RuoteRight = 0xf10e,
Ra = 0xf1d0,
Random = 0xf074,
Rebel = 0xf1d0,
Recycle = 0xf1b8,
Reddit = 0xf1a1,
RedditSquare = 0xf1a2,
Refresh = 0xf021,
Renren = 0xf18b,
Reorder = 0xf0c9,
Repeat = 0xf01e,
Reply = 0xf112,
ReplyAll = 0xf122,
Retweet = 0xf079,
Rmb = 0xf157,
Road = 0xf018,
Rocket = 0xf135,
RotateLeft = 0xf0e2,
RotateRight = 0xf01e,
Rouble = 0xf158,
Rss = 0xf09e,
RssSquare = 0xf143,
Rub = 0xf158,
Ruble = 0xf158,
Rupee = 0xf156,
Save = 0xf0c7,
Scissors = 0xf0c4,
Search = 0xf002,
SearchMinus = 0xf010,
SearchPlus = 0xf00e,
Send = 0xf1d8,
SendO = 0xf1d9,
Share = 0xf064,
ShareAlt = 0xf1e0,
ShareAltSquare = 0xf1e1,
ShareSquare = 0xf14d,
ShareSquareO = 0xf045,
Shield = 0xf132,
ShoppingCart = 0xf07a,
SignIn = 0xf090,
SignOut = 0xf08b,
Signal = 0xf012,
Sitemap = 0xf0e8,
Skype = 0xf17e,
Slack = 0xf198,
Sliders = 0xf1de,
SmileO = 0xf118,
Sort = 0xf0dc,
SortAlphaAsc = 0xf15d,
SortAlphaDesc = 0xf15e,
SortAmountAsc = 0xf160,
SortAmountDesc = 0xf161,
SortAsc = 0xf0de,
SortDesc = 0xf0dd,
SortDown = 0xf0dd,
SortNumericAsc = 0xf162,
SortNumericDesc = 0xf163,
SortUp = 0xf0de,
Soundcloud = 0xf1be,
SpaceShuttle = 0xf197,
Spinner = 0xf110,
Spoon = 0xf1b1,
Spotify = 0xf1bc,
Square = 0xf0c8,
SquareO = 0xf096,
StackExchange = 0xf18d,
StackOverflow = 0xf16c,
Star = 0xf005,
StarHalf = 0xf089,
StarHalfEmpty = 0xf123,
StarHalfFull = 0xf123,
StarHalfO = 0xf123,
StarO = 0xf006,
Steam = 0xf1b6,
SteamSquare = 0xf1b7,
StepBackward = 0xf048,
StepForward = 0xf051,
Stethoscope = 0xf0f1,
Stop = 0xf04d,
StopCircle = 0xf28d,
StopCircleO = 0xf28e,
Strikethrough = 0xf0cc,
Stumbleupon = 0xf1a4,
StumbleuponCircle = 0xf1a3,
Subscript = 0xf12c,
Suitcase = 0xf0f2,
SunO = 0xf185,
Superscript = 0xf12b,
Support = 0xf1cd,
Table = 0xf0ce,
Tablet = 0xf10a,
Tachometer = 0xf0e4,
Tag = 0xf02b,
Tags = 0xf02c,
Tasks = 0xf0ae,
Taxi = 0xf1ba,
TencentWeibo = 0xf1d5,
Terminal = 0xf120,
TextHeight = 0xf034,
TextWidth = 0xf035,
Th = 0xf00a,
ThLarge = 0xf009,
ThList = 0xf00b,
ThumbTack = 0xf08d,
ThumbsDown = 0xf165,
ThumbsODown = 0xf088,
ThumbsOUp = 0xf087,
ThumbsUp = 0xf164,
Ticket = 0xf145,
Times = 0xf00d,
TimesCircle = 0xf057,
TimesCircleO = 0xf05c,
Tint = 0xf043,
ToggleDown = 0xf150,
ToggleLeft = 0xf191,
ToggleRight = 0xf152,
ToggleUp = 0xf151,
TrashO = 0xf014,
Tree = 0xf1bb,
Trello = 0xf181,
Trophy = 0xf091,
Truck = 0xf0d1,
Try = 0xf195,
Tumblr = 0xf173,
TumblrSquare = 0xf174,
TurkishLira = 0xf195,
Twitter = 0xf099,
TwitterSquare = 0xf081,
Umbrella = 0xf0e9,
Underline = 0xf0cd,
Undo = 0xf0e2,
University = 0xf19c,
Unlink = 0xf127,
Unlock = 0xf09c,
UnlockAlt = 0xf13e,
Unsorted = 0xf0dc,
Upload = 0xf093,
Usd = 0xf155,
User = 0xf007,
UserMd = 0xf0f0,
Users = 0xf0c0,
VideoCamera = 0xf03d,
VimeoSquare = 0xf194,
Vine = 0xf1ca,
Vk = 0xf189,
VolumeDown = 0xf027,
VolumeOff = 0xf026,
VolumeUp = 0xf028,
Warning = 0xf071,
Wechat = 0xf1d7,
Weibo = 0xf18a,
Weixin = 0xf1d7,
Wheelchair = 0xf193,
Windows = 0xf17a,
Won = 0xf159,
Wordpress = 0xf19a,
Wrench = 0xf0ad,
Xing = 0xf168,
XingSquare = 0xf169,
Yahoo = 0xf19e,
Yen = 0xf157,
Youtube = 0xf167,
YoutubePlay = 0xf16a,
YoutubeSquare = 0xf166,
}
}
其他能用到的好像就一个计算圆角矩形的函数了
/// <summary>
/// 生成一个圆角矩形
/// </summary>
/// <param name="rect">矩形</param>
/// <param name="cRadius">弧度</param>
/// <returns></returns>
public static GraphicsPath RoundedRectangle(Rectangle rect, int cRadius)
{
// 指定图形路径, 有一系列 直线/曲线 组成
GraphicsPath myPath = new GraphicsPath();
myPath.StartFigure();
myPath.AddArc(new Rectangle(new Point(rect.X, rect.Y), new Size(2 * cRadius, 2 * cRadius)), 180, 90); // 左上角弧度
myPath.AddLine(new Point(rect.X + cRadius, rect.Y), new Point(rect.Right - cRadius, rect.Y)); // 顶部横线
myPath.AddArc(new Rectangle(new Point(rect.Right - 2 * cRadius, rect.Y), new Size(2 * cRadius, 2 * cRadius)), 270, 90); // 右上角弧度
myPath.AddLine(new Point(rect.Right, rect.Y + cRadius), new Point(rect.Right, rect.Bottom - cRadius)); // 右侧垂直线
myPath.AddArc(new Rectangle(new Point(rect.Right - 2 * cRadius, rect.Bottom - 2 * cRadius), new Size(2 * cRadius, 2 * cRadius)), 0, 90); // 右下角弧度
myPath.AddLine(new Point(rect.Right - cRadius, rect.Bottom), new Point(rect.X + cRadius, rect.Bottom)); // 底部横线
myPath.AddArc(new Rectangle(new Point(rect.X, rect.Bottom - 2 * cRadius), new Size(2 * cRadius, 2 * cRadius)), 90, 90); // 左下角弧度
myPath.AddLine(new Point(rect.X, rect.Bottom - cRadius), new Point(rect.X, rect.Y + cRadius)); // 左侧垂直线
myPath.CloseFigure();
return myPath;
}