.NET手撸绘制TypeScript类图——下篇

在上篇的文章中,我们介绍了如何使用.NET解析TypeScript,这篇将介绍如何使用代码将类图渲染出来。

注:以防有人错过了,上篇链接如下:https://www.cnblogs.com/sdflysha/p/20191113-ts-uml-with-dotnet-1.html

类型定义渲染

不出意外,我们继续使用FlysEngine。虽然文字排版没做过,但不试试怎么知道好不好做呢?

正常实时渲染时,画一两行文字可能很容易,但绘制大量文字时,就需要引入一些排版操作了。为了实现排板,首先需要将ClassDef类扩充一下——干脆再加个RenderingClassDef类,包含一个ClassDef

class RenderingClassDef
{
public ClassDef Def { get; set; } public Vector2 Position { get; set; } public Vector2 Size { get; set; } public Vector2 Center => Position + Size / 2;
}

它包含了一些位置和大小信息,并提供了一个中间值的变量。之所以这样定义,因为这里存在了一些挺麻烦的过程,比如想想以下操作:

  • 如果我想绘制放在中间的类名,我就必须知道所有行的宽度
  • 如果我想绘制边框,我也必须知道所有行的高度

还好Direct2D/DirectWrite提供了方块的文字宽度、高度计算属性,通过.Metrics即可获取。有了这个,排板过程中,我认为最难处理的是y坐标了,它是一个状态机,需要实时去更新、计算y坐标的位置,绘制过程如下:

foreach (var classDef in AllClass.Values)
{
ctx.FillRectangle(new RectangleF(classDef.Position.X, classDef.Position.Y, classDef.Size.X, classDef.Size.Y), XResource.GetColor(Color.AliceBlue)); var position = classDef.Position;
List<TextLayout> lines =
classDef.Def.Properties.OrderByDescending(x => x.IsPublic).Select(x => x.ToString())
.Concat(new string[] { "" })
.Concat(classDef.Def.Methods.OrderByDescending(m => m.IsPublic).Select(x => x.ToString()))
.Select(x => XResource.TextLayouts[x, FontSize])
.ToList(); TextLayout titleLayout = XResource.TextLayouts[classDef.Def.Name, FontSize + 3];
float width = Math.Max(titleLayout.Metrics.Width, lines.Max(x => x.Metrics.Width)) + MarginLR * 2;
ctx.DrawTextLayout(new Vector2(position.X + (width - titleLayout.DetermineMinWidth()) / 2 + MarginLR, position.Y), titleLayout, XResource.GetColor(Color.Black));
ctx.DrawLine(new Vector2(position.X, position.Y + titleLayout.Metrics.Height), new Vector2(position.X + width, position.Y + titleLayout.Metrics.Height), XResource.GetColor(TextColor), 2.0f); float y = lines.Aggregate(position.Y + titleLayout.Metrics.Height, (y, pt) =>
{
if (pt.Metrics.Width == 0)
{
ctx.DrawLine(new Vector2(position.X, y), new Vector2(position.X + width, y), XResource.GetColor(TextColor), 2.0f);
return y;
}
else
{
ctx.DrawTextLayout(new Vector2(position.X + MarginLR, y), pt, XResource.GetColor(TextColor));
return y + pt.Metrics.Height;
}
});
float height = y - position.Y; ctx.DrawRectangle(new RectangleF(position.X, position.Y, width, height), XResource.GetColor(TextColor), 2.0f);
classDef.Size = new Vector2(width, height);
}

请注意变量y的使用,我使用了一个LINQ中的Aggregate,实时的绘制并统计y变量的最新值,让代码简化了不少。

这里我又取巧了,正常文章排板应该是xy都需要更新,但这里每个定义都固定为一行,因此我不需要关心x的位置。但如果您想搞一些更的操作,如所有类型着个色,这时只需要同时更新xy即可。

此时渲染出来效果如下:

可见类图可能太小,我们可能需要局部放大一点,然后类图之间产生了重叠,我们需要拖拽的方式来移动到正确位置。

放大和缩小

由于我们使用了Direct2D,无损的高清放大变得非常容易,首先我们需要定义一个变量,并响应鼠标滚轮事件:

Vector2 mousePos;
Matrix3x2 worldTransform = Matrix3x2.Identity; protected override void OnMouseWheel(MouseEventArgs e)
{
float scale = MathF.Pow(1.1f, e.Delta / 120.0f);
worldTransform *= Matrix3x2.Scaling(scale, scale, mousePos);
}

其中魔术值1.1代表,鼠标每滚动一次,放大1.1倍。

另外mousePos变量由鼠标移动事件的XY坐标经worldTransform的逆变换计算而来:

protected override void OnMouseMove(MouseEventArgs e)
{
mousePos = XResource.InvertTransformPoint(worldTransform, new Vector2(e.X, e.Y));
}

注意:

矩阵逆变换涉及一些高等数学中的线性代数知识,没必要立即掌握。只需知道矩阵变换可以变换点位置,矩阵逆变换可以恢复原点的位置。

在本文中鼠标移动的坐标是窗体提供的,换算成真实坐标,即需要进行“矩阵逆变换”——这在碰撞检测中很常见。

以防我有需要,我们还再加一个快捷键,按空格即可立即恢复缩放:

protected override void OnKeyUp(KeyEventArgs e)
{
if (e.KeyCode == Keys.Space) worldTransform = Matrix3x2.Identity;
}

然后在OnDraw事件中,将worldTransform应用起来即可:

protected override void OnDraw(DeviceContext ctx)
{
ctx.Clear(Color.White);
ctx.Transform = worldTransform; // 重点
// 其它代码...
}

运行效果如下(注意放大缩小时,会以鼠标位置为中心点进行):

碰撞检测和拖拽

拖拽而已,为什么会和碰撞检测有关呢?

这是因为拖拽时,必须知道鼠标是否处于元素的上方,这就需要碰撞检测了。

首先给RenderingClassDef方法加一个TestPoint()方法,判断是鼠标是否与绘制位置重叠,这里我使用了SharpDX提供的RectangleF.Contains(Vector2)方法,具体算法已经不用关心,调用函数即可:

class RenderingClassDef
{
// 其它代码...
public bool TestPoint(Vector2 point) => new RectangleF(Position.X, Position.Y, Size.X, Size.Y).Contains(point);
}

然后在OnDraw方法中,做一个判断,如果类方框与鼠标出现重叠,则画一个宽度2.0的红色的边框,代码如下:

if (classDef.TestPoint(mousePos))
{
ctx.DrawRectangle(new RectangleF(classDef.Position.X, classDef.Position.Y, classDef.Size.X, classDef.Size.Y), XResource.GetColor(Color.Red), 2.0f);
}

测试效果如下(注意鼠标位置和红框):

碰撞检测做好,就能写代码拖拽了。要实现拖拽,首先需要在RenderingClassDef类中定义两个变量,用于保存其起始位置和鼠标起始位置,用于计算鼠标移动距离:

class RenderingClassDef
{
// 其它定义... public Vector2? CapturedPosition { get; set; } public Vector2 OriginPosition { get; set; }
}

然后在鼠标按下、鼠标移动、鼠标松开时进行判断,如果鼠标按下时处于某个类的方框里面,则记录这两个起始值:

protected override void OnMouseDown(MouseEventArgs e)
{
foreach (var item in this.AllClass.Values)
{
item.CapturedPosition = null;
} foreach (var item in this.AllClass.Values)
{
if (item.TestPoint(mousePos))
{
item.CapturedPosition = mousePos;
item.OriginPosition = item.Position;
return;
}
}
}

如果鼠标移动时,且有类的方框处于有值的状态,则计算偏移量,并让该方框随着鼠标移动:

protected override void OnMouseMove(MouseEventArgs e)
{
mousePos = XResource.InvertTransformPoint(worldTransform, new Vector2(e.X, e.Y));
foreach (var item in this.AllClass.Values)
{
if (item.CapturedPosition != null)
{
item.Position = item.OriginPosition + mousePos - item.CapturedPosition.Value;
return;
}
}
}

如果鼠标松开,则清除该记录值:

protected override void OnMouseUp(MouseEventArgs e)
{
foreach (var item in this.AllClass.Values)
{
item.CapturedPosition = null;
}
}

此时,运行效果如下:

类型间的关系

类型和类型之间是有依赖关系的,这也应该通过图形的方式体现出来。使用DeviceContext.DrawLine()方法即可画出线条,注意先画的会被后画的覆盖,因此这个foreach需要放在OnDraw方法的foreach语句之前:

foreach (var classDef in AllClass.Values)
{
List<string> allTypes = classDef.Def.Properties.Select(x => x.Type).ToList();
foreach (var kv in AllClass.Where(x => allTypes.Contains(x.Key)))
{
ctx.DrawLine(classDef.Center, kv.Value.Center, XResource.GetColor(Color.Gray), 2.0f);
}
}

此时,运行效果如下:

注意:在真正的UML图中,除了依赖关系,继承关系也是需要体现的。而且线条是有箭头、且线条类型也是有讲究的,Direct2D支持自定义线条,这些都能做,权当留给各位自己去挑战尝试了。

方框顺序

现在我们不能决定哪个在前,哪个在后,想象中方框可能应该就像窗体一样,客户点击哪个哪个就应该提到最前,这可以通过一个ZIndex变量来表示,首先在RenderingClassDef类中加一个属性:

public int ZIndex { get; set; } = 0;

然后在鼠标点击事件中,判断如果击中该类的方框,则将ZIndex赋值为最大值加1:

protected override void OnClick(EventArgs e)
{
foreach (var item in this.AllClass.Values)
{
if (item.TestPoint(mousePos))
{
item.ZIndex = this.AllClass.Values.Max(v => v.ZIndex) + 1;
}
}
}

然后在OnDraw方法的第二个foreach循环,改成按ZIndex从小到大排序渲染即可:

// 其它代码...
foreach (var classDef in AllClass.Values.OrderBy(x => x.ZIndex))
// 其它代码...

运行效果如下(注意我的鼠标点击和前后顺序):

总结

其实这是一个真实的需求,我们公司写代码时要求设计文档,通常我们都使用ProcessOn等工具来绘制,但前端开发者通过需要面对好几屏幕的类、方法和属性,然后弄将其名称、参数和类型一一拷贝到该工具中,这是一个需要极大耐心的工作。

“哪里有需求,哪里就有办法”,这个小工具也许能给我们的客户少许帮助,我正准备“说干就干”时——有人提醒我,我们的开发流程要先出文档,再写代码。所以……理论上不应该存在这种工具

.NET手撸绘制TypeScript类图——下篇的更多相关文章

  1. .NET手撸绘制TypeScript类图——上篇

    .NET手撸绘制TypeScript类图--上篇 近年来随着交互界面的精细化,TypeScript越来越流行,前端的设计也越来复杂,而类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方式. ...

  2. 使用工厂方法模式设计能够实现包含加法(+)、减法(-)、乘法(*)、除法(/)四种运算的计算机程序,要求输入两个数和运算符,得到运算结果。要求使用相关的工具绘制UML类图并严格按照类图的设计编写程序实

    2.使用工厂方法模式设计能够实现包含加法(+).减法(-).乘法(*).除法(/)四种运算的计算机程序,要求输入两个数和运算符,得到运算结果.要求使用相关的工具绘制UML类图并严格按照类图的设计编写程 ...

  3. 1、使用简单工厂模式设计能够实现包含加法(+)、减法(-)、乘法(*)、除法(/)四种运算的计算机程序,要求输入两个数和运算符,得到运算结果。要求使用相关的工具绘制UML类图并严格按照类图的设计编写程

    1.使用简单工厂模式设计能够实现包含加法(+).减法(-).乘法(*).除法(/)四种运算的计算机程序,要求输入两个数和运算符,得到运算结果.要求使用相关的工具绘制UML类图并严格按照类图的设计编写程 ...

  4. CentO7-使用plantuml绘制UML类图

    准备工作 到PlantUml官网(http://plantuml.com/download)下载plantuml.jar.官网上还有一个在线的demof服务.plantuml的官网真的很挫! 到官网下 ...

  5. Android Studio中绘制simpleUML类图详细说明及使用

    一.Android Studio中安装simpleUML 1.下载simpleUML jar包 地址为:http://plugins.jetbrains.com/  搜索 simpleUMLCE 2. ...

  6. 转:深入浅出UML类图(具体到代码层次)

    深入浅出UML类图 作者:刘伟 ,发布于:2012-11-23,来源:CSDN   在UML 2.0的13种图形中,类图是使用频率最高的UML图之一.Martin Fowler在其著作<UML ...

  7. 深入浅出UML类图(一)

    在UML 2.0的13种图形中,类图是使用频率最高的UML图之一.Martin Fowler在其著作<UML Distilled: A Brief Guide to the Standard O ...

  8. 深入浅出UML类图

    原作者:http://www.uml.org.cn/oobject/201211231.asp 在UML 2.0的13种图形中,类图是使用频率最高的UML图之一.Martin Fowler在其著作&l ...

  9. UML的类图关系分为: 关联、聚合/组合、依赖、泛化(继承)

    UML的类图关系分为: 关联.聚合/组合.依赖.泛化(继承).而其中关联又分为双向关联.单向关联.自身关联:下面就让我们一起来看看这些关系究竟是什么,以及它们的区别在哪里. 1.关联 双向关联:C1- ...

随机推荐

  1. nginx的负载均衡实战

    前言 nginx是一个高性能的HTTP和反向代理的服务器.它有三个最基本的功能,一是当做web服务器.二是作为反向代理服务器.三是提供负载均衡(在反向代理基础上),由于它占有内存小,并发能力强,所以在 ...

  2. Java对象是怎么创建的(通过对象的创建,了解JVM内存结构)

    在代码层面,我们通过new关键字创建一个对象: Object obj=new Object(); 而虚拟机中,创建一个对象,则经过了许多环节,JVM的内存结构可以通过另一篇文章了解:一个“Hello ...

  3. Docker容器启动失败 Failed to start Docker Application Container Engine的解决办法

    当编辑完daemon.json时,准备systemctl start docker.service启动docker时报以下错误: 网上查找的诸多方法都不行,后面看到一篇类似的文章:http://www ...

  4. Ubuntu安装exfat工具

    sudo apt-get undate sudo apt-get install exfat-utils

  5. JS中作用域和作用域链

    1.执行环境(execution context) 执行环境定义了变量和函数有权访问的其他数据,决定了他们各自的行为.每个执行环境都有与之对应的变量对象(variable object),保存着该环境 ...

  6. Prometheus 源码解读(一)

    Prometheus 源码解读(一) Prometheus 是云原生监控领域的事实标准,越来越来的开源项目开始支持 Prometheus 监控数据格式.从本篇开始,我将和大家一起阅读分析 Promet ...

  7. 【RabbitMQ 实战指南】一 过期时间TTL

    RabbitMQ 可以对消息和队列设置过期时间(TTL) 1.设置消息的TTL 目前有两种方式可以设置消息的TTL 第一种方式是通过队列属性设置,队列中所有消息都有相同的过期时间 第二种方式是对消息本 ...

  8. Spring Cloud Alibaba (nacos 注册中心搭建)

    [nacos下载地址](https://github.com/alibaba/nacos/releases) ### 什么是 Nacos? - nacos主要起到俩个作用一个是注册中心,另外一个是配置 ...

  9. 七、springBoot 简单优雅是实现文件上传和下载

    前言 好久没有更新spring Boot 这个项目了.最近看了一下docker 的知识,后期打算将spring boot 和docker 结合起来.刚好最近有一个上传文件的工作呢,刚好就想起这个脚手架 ...

  10. 再不努力提高效率,小姐姐都被人追走了:K8S一键部署了解一下?

    随着互联网时代的不断发展,开发者可能会面临这样的困境:为了解决问题.提升开发效率而竭力研发出来的"创新",似乎削弱了他们在公司的重要程度,甚至取代了他们原先的地位.比如,在云原生时 ...