admin管理员组文章数量:1029922
记一次自定义基因分类图实现(一)
一、前言
最近接到一个开发任务,要对基因表达结果进行分类,其中算法分类心酸就不一一提了,最终成功将分类给做出来了,接着想展示形式,询问相关应用同事,也查了相关资料,确定了显示形式。
二、关于基因表达分类图
在征求多个同事意见和建议后,采取类似的一个变种图,将连线连到基因和样本上,整体大概形式为左侧为分类A 顶部分类B。
当然这是最终效果了
三、准备开发
作为一个开发,总会不觉得去想怎么实现,对于实际业务这个图可以认为是无限扩展的,如果没有分类曲线图,还是很容易实现的,但是多了曲线,实现思路有两个,修改TreeView样式,实现逐级递减,第二个自己实现布局控件并结合列表控件。
1. TreeView 方案
优点:
- 内置功能完善:TreeView 提供默认的树状结构、折叠/展开、节点选择等交互功能,减少基础开发工作量。
- 样式可定制:可通过修改
ControlTemplate
调整节点样式(如字体、颜色、缩进),满足基本UI需求。 - 数据绑定支持:天然支持
HierarchicalDataTemplate
,适合层级数据的展示和操作。
缺点:
- 布局灵活性差:
- 默认横向排列,若需竖向紧凑排列(如一级节点和末级节点同列),需大幅修改模板甚至旋转控件,代码复杂。
- 树状缩进逻辑固定,难以实现非标准层级样式(如平铺式布局)。
- 性能问题:节点数量多时(如超1000条),默认渲染可能导致卡顿,需额外优化。
2. 自定义布局控件方案(继承 Panel 重写)
优点:
- 布局完全自由:
- 可灵活定义横向/纵向排版,无需依赖 TreeView 的固有结构。
- 支持复杂连线绘制(如折线、贝塞尔曲线),适应不同视觉需求。
- 性能可控:通过自定义测量(
MeasureOverride
)和排列(ArrangeOverride
)逻辑,优化渲染效率。 - 无冗余功能:仅实现必要特性,避免 TreeView 的额外开销。
缺点:
- 开发成本高:需手动实现布局计算、连线逻辑、交互事件(如点击折叠/展开)。
- 自适应挑战:需额外处理动态数据变化(如节点增删)、DPI 缩放、滚动支持等细节。
- 维护复杂:后续需求变更(如动画效果)可能需重构布局逻辑。
相比较两种方案,我其实更喜欢第二种,一方面可以对自己理解布局排列有加深巩固,另一方面也可以对绘制可以更加深刻,最重要一点比较灵活,后边可以逐渐实现虚拟化等方式。
四、绘制自定义布局控件
开始之前有个小插曲,最开始没打算写布局控件,只打算自定义控件将两边树形图画出来即可,折腾出来到实际的使用上发现一个问题,无法能够高效的跟主内容对齐互动,所以这版方案也放弃了,但是也不算没有收获,验证了树状图绘制逻辑和方法。为后来布局控件绘制打下基础。
0、准备
首先我们要进行绘制,实际上是一个树状的结构,我们需要根据层级来进行依次绘制,定义每个节点的DataContext 的实体类
结构如下:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 基础分类 /// </summary> public class BaseClusterModel { /// <summary> /// 级别 /// </summary> public int Level { get; set; } /// <summary> /// 每个元素宽度 /// </summary> internal double Width { get; set; } /// <summary> /// 每个元素高度 /// </summary> internal double Height { get; set; } /// <summary> /// 父级唯一标识 /// </summary> public string ParentUid { get; set; } /// <summary> /// 标识 /// </summary> public string Uid { get; set; }
}
通过父级标识一级级找自己上级,通过width或者height来确定线段起始或者终点。
定义线段类
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 线段实体 /// </summary> public class DrawLineModel { /// <summary> /// 起始点 /// </summary> public Point StartPoint { get; set; } /// <summary> /// 终点 /// </summary> public Point EndPoint { get; set; } /// <summary> /// 级别 /// </summary> public int Level { get; set; } /// <summary> /// 标识 /// </summary> public string Uid { get; set; } /// <summary> /// 父级标识 /// </summary> public string ParentUid { get; set; } }
拿垂直布局来说,这里垂直指的终端元素垂直
1、了解布局控件
自定义ClusterPanel继承自Panel,有两个非常重要的方法,一个是测量MeasureOverride一个是排列ArrangeOverride,我们自定义布局控件绕不开这两个方法,我们通过ArrangeOverride将标签排到最右侧,空出左侧绿色部分供我们进行画图
代码语言:javascript代码运行次数:0运行复制protected override Size MeasureOverride(Size availableSize){ Size size = new Size(); if (Orientation == Orientation.Horizontal) { foreach (UIElement child in InternalChildren) { child.Measure(availableSize); size.Width = Math.Max(size.Width, child.DesiredSize.Width); size.Width += child.DesiredSize.Width; } } else { foreach (UIElement child in InternalChildren) { child.Measure(availableSize); size.Width = Math.Max(size.Width, child.DesiredSize.Width); size.Height += child.DesiredSize.Height; } } return size;}protected override Size ArrangeOverride(Size finalSize){ BaseClusters = new List<BaseClusterModel>(); if (Orientation == Orientation.Horizontal) { double x = 0; foreach (FrameworkElement child in InternalChildren) { child.Arrange(new Rect(x, ActualHeight - child.DesiredSize.Height, child.DesiredSize.Width, child.DesiredSize.Height)); x += child.DesiredSize.Width; BaseClusterModel baseClusterModel = child.DataContext as BaseClusterModel; if (baseClusterModel != null) { BaseClusters.Add(new BaseClusterModel() { Width = child.DesiredSize.Width, Height = child.DesiredSize.Height, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, Level = baseClusterModel.Level }); } } if (InternalChildren.Count > 0) LineMaxSpace = this.ActualHeight - InternalChildren[0].DesiredSize.Height; } else { double y = 0; foreach (FrameworkElement child in InternalChildren) { child.Arrange(new Rect(ActualWidth - child.DesiredSize.Width, y, child.DesiredSize.Width, child.DesiredSize.Height)); y += child.DesiredSize.Height; BaseClusterModel baseClusterModel = child.DataContext as BaseClusterModel; if (baseClusterModel != null) { BaseClusters.Add(new BaseClusterModel() { Width = child.DesiredSize.Width, Height = child.DesiredSize.Height, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, Level = baseClusterModel.Level }); } } if (InternalChildren.Count > 0) LineMaxSpace = this.ActualWidth - InternalChildren[0].DesiredSize.Width; } return finalSize;}
通过测量元素大小,以及排列 可以算出LineMaxSpace 空间供我们使用
2、重写OnRender方法
代码语言:javascript代码运行次数:0运行复制 protected override void OnRender(DrawingContext drawingContext) { drawingContext.DrawRectangle(Background, null, new Rect(0, 0, ActualWidth, ActualHeight)); //计算每个格的宽度 PreLineLength = LineMaxSpace / (MaxLevel); List<DrawLineModel> drawLines = new List<DrawLineModel>(); int yIndex = 0; double y = 0; int currentLevel = 0; var verStartPoint = new Point(0, 0); var verEndPoint = new Point(0, 0); //绘制横线 if (Orientation == Orientation.Vertical) DrawVer(drawLines, ref yIndex, ref y, ref currentLevel, ref verStartPoint, ref verEndPoint); else DrawHor(drawLines, ref yIndex, ref y, ref currentLevel, ref verStartPoint, ref verEndPoint);
DrawLine(drawingContext, drawLines, true);
base.OnRender(drawingContext); }
重新OnRender主要目的是将线画出来,并且根据方向来实现分别生成要画的线序列。
画竖线:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 生成横向排列Lines /// </summary> /// <param name="drawLines">最终Line合集</param> /// <param name="yIndex">来控制线的位置</param> /// <param name="x">线的位置</param> /// <param name="currentLevel">当前等级</param> /// <param name="verStartPoint">起始</param> /// <param name="verEndPoint">终止</param> private void DrawHor(List<DrawLineModel> drawLines, ref int yIndex, ref double x, ref int currentLevel, ref Point verStartPoint, ref Point verEndPoint) { foreach (BaseClusterModel baseClusterModel in BaseClusters) {
if (yIndex == 0) { x = baseClusterModel.Width / 2; } else { x += baseClusterModel.Width; } yIndex++;
var startPoint = new Point(x, PreLineLength * (baseClusterModel.Level - 1)); var endPoint = new Point(x, LineMaxSpace); drawLines.Add(new DrawLineModel() { Level = baseClusterModel.Level, StartPoint = startPoint, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, EndPoint = endPoint });
verEndPoint = new Point(x, startPoint.Y); currentLevel = baseClusterModel.Level;
} }
画横线:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 生成竖向排列Lines /// </summary> /// <param name="drawLines">最终Line合集</param> /// <param name="yIndex">来控制线的位置</param> /// <param name="y">线的位置</param> /// <param name="currentLevel">当前等级</param> /// <param name="verStartPoint">起始</param> /// <param name="verEndPoint">终止</param> private void DrawVer(List<DrawLineModel> drawLines, ref int yIndex, ref double y, ref int currentLevel, ref Point verStartPoint, ref Point verEndPoint) { foreach (BaseClusterModel baseClusterModel in BaseClusters) {
if (yIndex == 0) { y = baseClusterModel.Height / 2; } else { y += baseClusterModel.Height; } yIndex++;
var startPoint = new Point(PreLineLength * (baseClusterModel.Level - 1), y); var endPoint = new Point(LineMaxSpace, y); drawLines.Add(new DrawLineModel() { Level = baseClusterModel.Level, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, StartPoint = startPoint, EndPoint = endPoint }); if (currentLevel != baseClusterModel.Level) { verStartPoint = new Point(startPoint.X, y); }
verEndPoint = new Point(startPoint.X, y); currentLevel = baseClusterModel.Level; } }
根据元素宽或高来定位线段终点,并根据当前节点等级来确定第一根线的位置。注意:Level从1开始
五、绘制线条
将得到的Lines,进行绘制,大致思路根据等级一级级绘制,第一次绘制出最远端的线,并将同级线段终点连接起来,并取中间作为终点,进入下一次绘制。根据ParentUid来找父级,如果没有父级,则找最近的Level-1的一根线作为父级,代码如下:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 根据等级绘制线段 /// </summary> /// <param name="drawingContext">绘制上下文</param> /// <param name="drawLines">要绘制的线段</param> /// <param name="level">等级</param> void DrawByLevel(DrawingContext drawingContext, List<DrawLineModel> drawLines, int level) { var allLevelData = drawLines.FindAll(x => x.Level == level); if (allLevelData != null && allLevelData.Count > 0) { var uidDic = allLevelData.GroupBy(x => x.ParentUid).ToDictionary(c => c.Key, m => m.ToList()); foreach (var item in uidDic) { var parentLine = drawLines.Find(p => p.Uid == item.Key); if (parentLine == null) { parentLine = drawLines.Find(p => p.Level == level - 1); } if (item.Value.Count == 1) { if (parentLine != null) { var startPoint = new Point(item.Value[0].StartPoint.X, item.Value[0].StartPoint.Y); var endPoint = new Point(item.Value[0].StartPoint.X, parentLine.EndPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), startPoint, endPoint); } } else { var minData = item.Value.Min(x => x.StartPoint.Y); var maxData = item.Value.Max(x => x.StartPoint.Y); var maxX = item.Value.Max(x => x.StartPoint.X); drawingContext.DrawLine(new Pen(Brushes.Black, 1), new Point(maxX, minData), new Point(maxX, maxData)); if (parentLine != null) { var horEndPoint = GetPoint(maxX, minData, maxData); var horStartPoint = new Point(parentLine.StartPoint.X, horEndPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), horStartPoint, horEndPoint); var newDrawLine = new DrawLineModel() { ParentUid = parentLine.ParentUid, Level = parentLine.Level, Uid = Guid.NewGuid().ToString(), StartPoint = horStartPoint, EndPoint = horEndPoint }; drawLines.Add(newDrawLine); } } } level = level - 1; DrawByLevel(drawingContext, drawLines, level); } }
横向排序时,绘制方法打通小异,就是X,Y的变换和计算
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 横向排版绘制 /// </summary> /// <param name="drawingContext">绘制上下文</param> /// <param name="drawLines">要绘制的线段</param> /// <param name="level">等级</param> private void DrawHorLevel(DrawingContext drawingContext, List<DrawLineModel> drawLines, int level) { var allLevelData = drawLines.FindAll(x => x.Level == level);
if (allLevelData != null && allLevelData.Count > 0) { var uidDic = allLevelData.GroupBy(x => x.ParentUid).ToDictionary(c => c.Key, m => m.ToList()); foreach (var item in uidDic) { var parentLine = drawLines.Find(p => p.Uid == item.Key); if (parentLine == null) { parentLine = drawLines.Find(p => p.Level == level - 1); } if (item.Value.Count == 1) { if (parentLine != null) { var startPoint = new Point(item.Value[0].StartPoint.X, item.Value[0].StartPoint.Y); var endPoint = new Point(item.Value[0].StartPoint.X, parentLine.EndPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), startPoint, endPoint); } } else { var minData = item.Value.Min(x => x.StartPoint.X); var maxData = item.Value.Max(x => x.StartPoint.X); var maxY = item.Value.Max(x => x.StartPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), new Point(minData, maxY), new Point(maxData, maxY)); if (parentLine != null) { var verEndPoint = GetPointY(maxY, minData, maxData); var verStartPoint = new Point(verEndPoint.X, parentLine.StartPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), verStartPoint, verEndPoint); var newDrawLine = new DrawLineModel() { ParentUid = parentLine.ParentUid, Level = parentLine.Level, Uid = Guid.NewGuid().ToString(), StartPoint = verStartPoint, EndPoint = verEndPoint }; drawLines.Add(newDrawLine); } } } level = level - 1; DrawHorLevel(drawingContext, drawLines, level); } }
现在我们就实现了一个等级的绘制,
下期我们将中间部分绘制以及大小绑定~ 源码奉上!
GitHub:
记一次自定义基因分类图实现(一)
一、前言
最近接到一个开发任务,要对基因表达结果进行分类,其中算法分类心酸就不一一提了,最终成功将分类给做出来了,接着想展示形式,询问相关应用同事,也查了相关资料,确定了显示形式。
二、关于基因表达分类图
在征求多个同事意见和建议后,采取类似的一个变种图,将连线连到基因和样本上,整体大概形式为左侧为分类A 顶部分类B。
当然这是最终效果了
三、准备开发
作为一个开发,总会不觉得去想怎么实现,对于实际业务这个图可以认为是无限扩展的,如果没有分类曲线图,还是很容易实现的,但是多了曲线,实现思路有两个,修改TreeView样式,实现逐级递减,第二个自己实现布局控件并结合列表控件。
1. TreeView 方案
优点:
- 内置功能完善:TreeView 提供默认的树状结构、折叠/展开、节点选择等交互功能,减少基础开发工作量。
- 样式可定制:可通过修改
ControlTemplate
调整节点样式(如字体、颜色、缩进),满足基本UI需求。 - 数据绑定支持:天然支持
HierarchicalDataTemplate
,适合层级数据的展示和操作。
缺点:
- 布局灵活性差:
- 默认横向排列,若需竖向紧凑排列(如一级节点和末级节点同列),需大幅修改模板甚至旋转控件,代码复杂。
- 树状缩进逻辑固定,难以实现非标准层级样式(如平铺式布局)。
- 性能问题:节点数量多时(如超1000条),默认渲染可能导致卡顿,需额外优化。
2. 自定义布局控件方案(继承 Panel 重写)
优点:
- 布局完全自由:
- 可灵活定义横向/纵向排版,无需依赖 TreeView 的固有结构。
- 支持复杂连线绘制(如折线、贝塞尔曲线),适应不同视觉需求。
- 性能可控:通过自定义测量(
MeasureOverride
)和排列(ArrangeOverride
)逻辑,优化渲染效率。 - 无冗余功能:仅实现必要特性,避免 TreeView 的额外开销。
缺点:
- 开发成本高:需手动实现布局计算、连线逻辑、交互事件(如点击折叠/展开)。
- 自适应挑战:需额外处理动态数据变化(如节点增删)、DPI 缩放、滚动支持等细节。
- 维护复杂:后续需求变更(如动画效果)可能需重构布局逻辑。
相比较两种方案,我其实更喜欢第二种,一方面可以对自己理解布局排列有加深巩固,另一方面也可以对绘制可以更加深刻,最重要一点比较灵活,后边可以逐渐实现虚拟化等方式。
四、绘制自定义布局控件
开始之前有个小插曲,最开始没打算写布局控件,只打算自定义控件将两边树形图画出来即可,折腾出来到实际的使用上发现一个问题,无法能够高效的跟主内容对齐互动,所以这版方案也放弃了,但是也不算没有收获,验证了树状图绘制逻辑和方法。为后来布局控件绘制打下基础。
0、准备
首先我们要进行绘制,实际上是一个树状的结构,我们需要根据层级来进行依次绘制,定义每个节点的DataContext 的实体类
结构如下:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 基础分类 /// </summary> public class BaseClusterModel { /// <summary> /// 级别 /// </summary> public int Level { get; set; } /// <summary> /// 每个元素宽度 /// </summary> internal double Width { get; set; } /// <summary> /// 每个元素高度 /// </summary> internal double Height { get; set; } /// <summary> /// 父级唯一标识 /// </summary> public string ParentUid { get; set; } /// <summary> /// 标识 /// </summary> public string Uid { get; set; }
}
通过父级标识一级级找自己上级,通过width或者height来确定线段起始或者终点。
定义线段类
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 线段实体 /// </summary> public class DrawLineModel { /// <summary> /// 起始点 /// </summary> public Point StartPoint { get; set; } /// <summary> /// 终点 /// </summary> public Point EndPoint { get; set; } /// <summary> /// 级别 /// </summary> public int Level { get; set; } /// <summary> /// 标识 /// </summary> public string Uid { get; set; } /// <summary> /// 父级标识 /// </summary> public string ParentUid { get; set; } }
拿垂直布局来说,这里垂直指的终端元素垂直
1、了解布局控件
自定义ClusterPanel继承自Panel,有两个非常重要的方法,一个是测量MeasureOverride一个是排列ArrangeOverride,我们自定义布局控件绕不开这两个方法,我们通过ArrangeOverride将标签排到最右侧,空出左侧绿色部分供我们进行画图
代码语言:javascript代码运行次数:0运行复制protected override Size MeasureOverride(Size availableSize){ Size size = new Size(); if (Orientation == Orientation.Horizontal) { foreach (UIElement child in InternalChildren) { child.Measure(availableSize); size.Width = Math.Max(size.Width, child.DesiredSize.Width); size.Width += child.DesiredSize.Width; } } else { foreach (UIElement child in InternalChildren) { child.Measure(availableSize); size.Width = Math.Max(size.Width, child.DesiredSize.Width); size.Height += child.DesiredSize.Height; } } return size;}protected override Size ArrangeOverride(Size finalSize){ BaseClusters = new List<BaseClusterModel>(); if (Orientation == Orientation.Horizontal) { double x = 0; foreach (FrameworkElement child in InternalChildren) { child.Arrange(new Rect(x, ActualHeight - child.DesiredSize.Height, child.DesiredSize.Width, child.DesiredSize.Height)); x += child.DesiredSize.Width; BaseClusterModel baseClusterModel = child.DataContext as BaseClusterModel; if (baseClusterModel != null) { BaseClusters.Add(new BaseClusterModel() { Width = child.DesiredSize.Width, Height = child.DesiredSize.Height, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, Level = baseClusterModel.Level }); } } if (InternalChildren.Count > 0) LineMaxSpace = this.ActualHeight - InternalChildren[0].DesiredSize.Height; } else { double y = 0; foreach (FrameworkElement child in InternalChildren) { child.Arrange(new Rect(ActualWidth - child.DesiredSize.Width, y, child.DesiredSize.Width, child.DesiredSize.Height)); y += child.DesiredSize.Height; BaseClusterModel baseClusterModel = child.DataContext as BaseClusterModel; if (baseClusterModel != null) { BaseClusters.Add(new BaseClusterModel() { Width = child.DesiredSize.Width, Height = child.DesiredSize.Height, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, Level = baseClusterModel.Level }); } } if (InternalChildren.Count > 0) LineMaxSpace = this.ActualWidth - InternalChildren[0].DesiredSize.Width; } return finalSize;}
通过测量元素大小,以及排列 可以算出LineMaxSpace 空间供我们使用
2、重写OnRender方法
代码语言:javascript代码运行次数:0运行复制 protected override void OnRender(DrawingContext drawingContext) { drawingContext.DrawRectangle(Background, null, new Rect(0, 0, ActualWidth, ActualHeight)); //计算每个格的宽度 PreLineLength = LineMaxSpace / (MaxLevel); List<DrawLineModel> drawLines = new List<DrawLineModel>(); int yIndex = 0; double y = 0; int currentLevel = 0; var verStartPoint = new Point(0, 0); var verEndPoint = new Point(0, 0); //绘制横线 if (Orientation == Orientation.Vertical) DrawVer(drawLines, ref yIndex, ref y, ref currentLevel, ref verStartPoint, ref verEndPoint); else DrawHor(drawLines, ref yIndex, ref y, ref currentLevel, ref verStartPoint, ref verEndPoint);
DrawLine(drawingContext, drawLines, true);
base.OnRender(drawingContext); }
重新OnRender主要目的是将线画出来,并且根据方向来实现分别生成要画的线序列。
画竖线:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 生成横向排列Lines /// </summary> /// <param name="drawLines">最终Line合集</param> /// <param name="yIndex">来控制线的位置</param> /// <param name="x">线的位置</param> /// <param name="currentLevel">当前等级</param> /// <param name="verStartPoint">起始</param> /// <param name="verEndPoint">终止</param> private void DrawHor(List<DrawLineModel> drawLines, ref int yIndex, ref double x, ref int currentLevel, ref Point verStartPoint, ref Point verEndPoint) { foreach (BaseClusterModel baseClusterModel in BaseClusters) {
if (yIndex == 0) { x = baseClusterModel.Width / 2; } else { x += baseClusterModel.Width; } yIndex++;
var startPoint = new Point(x, PreLineLength * (baseClusterModel.Level - 1)); var endPoint = new Point(x, LineMaxSpace); drawLines.Add(new DrawLineModel() { Level = baseClusterModel.Level, StartPoint = startPoint, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, EndPoint = endPoint });
verEndPoint = new Point(x, startPoint.Y); currentLevel = baseClusterModel.Level;
} }
画横线:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 生成竖向排列Lines /// </summary> /// <param name="drawLines">最终Line合集</param> /// <param name="yIndex">来控制线的位置</param> /// <param name="y">线的位置</param> /// <param name="currentLevel">当前等级</param> /// <param name="verStartPoint">起始</param> /// <param name="verEndPoint">终止</param> private void DrawVer(List<DrawLineModel> drawLines, ref int yIndex, ref double y, ref int currentLevel, ref Point verStartPoint, ref Point verEndPoint) { foreach (BaseClusterModel baseClusterModel in BaseClusters) {
if (yIndex == 0) { y = baseClusterModel.Height / 2; } else { y += baseClusterModel.Height; } yIndex++;
var startPoint = new Point(PreLineLength * (baseClusterModel.Level - 1), y); var endPoint = new Point(LineMaxSpace, y); drawLines.Add(new DrawLineModel() { Level = baseClusterModel.Level, Uid = baseClusterModel.Uid, ParentUid = baseClusterModel.ParentUid, StartPoint = startPoint, EndPoint = endPoint }); if (currentLevel != baseClusterModel.Level) { verStartPoint = new Point(startPoint.X, y); }
verEndPoint = new Point(startPoint.X, y); currentLevel = baseClusterModel.Level; } }
根据元素宽或高来定位线段终点,并根据当前节点等级来确定第一根线的位置。注意:Level从1开始
五、绘制线条
将得到的Lines,进行绘制,大致思路根据等级一级级绘制,第一次绘制出最远端的线,并将同级线段终点连接起来,并取中间作为终点,进入下一次绘制。根据ParentUid来找父级,如果没有父级,则找最近的Level-1的一根线作为父级,代码如下:
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 根据等级绘制线段 /// </summary> /// <param name="drawingContext">绘制上下文</param> /// <param name="drawLines">要绘制的线段</param> /// <param name="level">等级</param> void DrawByLevel(DrawingContext drawingContext, List<DrawLineModel> drawLines, int level) { var allLevelData = drawLines.FindAll(x => x.Level == level); if (allLevelData != null && allLevelData.Count > 0) { var uidDic = allLevelData.GroupBy(x => x.ParentUid).ToDictionary(c => c.Key, m => m.ToList()); foreach (var item in uidDic) { var parentLine = drawLines.Find(p => p.Uid == item.Key); if (parentLine == null) { parentLine = drawLines.Find(p => p.Level == level - 1); } if (item.Value.Count == 1) { if (parentLine != null) { var startPoint = new Point(item.Value[0].StartPoint.X, item.Value[0].StartPoint.Y); var endPoint = new Point(item.Value[0].StartPoint.X, parentLine.EndPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), startPoint, endPoint); } } else { var minData = item.Value.Min(x => x.StartPoint.Y); var maxData = item.Value.Max(x => x.StartPoint.Y); var maxX = item.Value.Max(x => x.StartPoint.X); drawingContext.DrawLine(new Pen(Brushes.Black, 1), new Point(maxX, minData), new Point(maxX, maxData)); if (parentLine != null) { var horEndPoint = GetPoint(maxX, minData, maxData); var horStartPoint = new Point(parentLine.StartPoint.X, horEndPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), horStartPoint, horEndPoint); var newDrawLine = new DrawLineModel() { ParentUid = parentLine.ParentUid, Level = parentLine.Level, Uid = Guid.NewGuid().ToString(), StartPoint = horStartPoint, EndPoint = horEndPoint }; drawLines.Add(newDrawLine); } } } level = level - 1; DrawByLevel(drawingContext, drawLines, level); } }
横向排序时,绘制方法打通小异,就是X,Y的变换和计算
代码语言:javascript代码运行次数:0运行复制 /// <summary> /// 横向排版绘制 /// </summary> /// <param name="drawingContext">绘制上下文</param> /// <param name="drawLines">要绘制的线段</param> /// <param name="level">等级</param> private void DrawHorLevel(DrawingContext drawingContext, List<DrawLineModel> drawLines, int level) { var allLevelData = drawLines.FindAll(x => x.Level == level);
if (allLevelData != null && allLevelData.Count > 0) { var uidDic = allLevelData.GroupBy(x => x.ParentUid).ToDictionary(c => c.Key, m => m.ToList()); foreach (var item in uidDic) { var parentLine = drawLines.Find(p => p.Uid == item.Key); if (parentLine == null) { parentLine = drawLines.Find(p => p.Level == level - 1); } if (item.Value.Count == 1) { if (parentLine != null) { var startPoint = new Point(item.Value[0].StartPoint.X, item.Value[0].StartPoint.Y); var endPoint = new Point(item.Value[0].StartPoint.X, parentLine.EndPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), startPoint, endPoint); } } else { var minData = item.Value.Min(x => x.StartPoint.X); var maxData = item.Value.Max(x => x.StartPoint.X); var maxY = item.Value.Max(x => x.StartPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), new Point(minData, maxY), new Point(maxData, maxY)); if (parentLine != null) { var verEndPoint = GetPointY(maxY, minData, maxData); var verStartPoint = new Point(verEndPoint.X, parentLine.StartPoint.Y); drawingContext.DrawLine(new Pen(Brushes.Black, 1), verStartPoint, verEndPoint); var newDrawLine = new DrawLineModel() { ParentUid = parentLine.ParentUid, Level = parentLine.Level, Uid = Guid.NewGuid().ToString(), StartPoint = verStartPoint, EndPoint = verEndPoint }; drawLines.Add(newDrawLine); } } } level = level - 1; DrawHorLevel(drawingContext, drawLines, level); } }
现在我们就实现了一个等级的绘制,
下期我们将中间部分绘制以及大小绑定~ 源码奉上!
GitHub:
本文标签: 记一次自定义基因分类图实现(一)
版权声明:本文标题:记一次自定义基因分类图实现(一) 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/jiaocheng/1747619249a2194222.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论