一、前言
最近接到一个开发任务,要对基因表达结果进行分类,其中算法分类心酸就不一一提了,最终成功将分类给做出来了,接着想展示形式,询问相关应用同事,也查了相关资料,确定了显示形式。
二、关于基因表达分类图
在征求多个同事意见和建议后,采取类似的一个变种图,将连线连到基因和样本上,整体大概形式为左侧为分类A 顶部分类B。
当然这是最终效果了
三、准备开发
作为一个开发,总会不觉得去想怎么实现,对于实际业务这个图可以认为是无限扩展的,如果没有分类曲线图,还是很容易实现的,但是多了曲线,实现思路有两个,修改TreeView样式,实现逐级递减,第二个自己实现布局控件并结合列表控件。
优点:
ControlTemplate
调整节点样式(如字体、颜色、缩进),满足基本UI需求。HierarchicalDataTemplate
,适合层级数据的展示和操作。缺点:
优点:
MeasureOverride
)和排列(ArrangeOverride
)逻辑,优化渲染效率。缺点:
相比较两种方案,我其实更喜欢第二种,一方面可以对自己理解布局排列有加深巩固,另一方面也可以对绘制可以更加深刻,最重要一点比较灵活,后边可以逐渐实现虚拟化等方式。
四、绘制自定义布局控件
开始之前有个小插曲,最开始没打算写布局控件,只打算自定义控件将两边树形图画出来即可,折腾出来到实际的使用上发现一个问题,无法能够高效的跟主内容对齐互动,所以这版方案也放弃了,但是也不算没有收获,验证了树状图绘制逻辑和方法。为后来布局控件绘制打下基础。
0、准备
首先我们要进行绘制,实际上是一个树状的结构,我们需要根据层级来进行依次绘制,定义每个节点的DataContext 的实体类
结构如下:
/// <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来确定线段起始或者终点。
定义线段类
/// <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将标签排到最右侧,空出左侧绿色部分供我们进行画图
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方法
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主要目的是将线画出来,并且根据方向来实现分别生成要画的线序列。
画竖线:
/// <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;
} }
画横线:
/// <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的一根线作为父级,代码如下:
/// <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的变换和计算
/// <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:https://github.com/tangmanger/ClusteringPlot
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。