.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. 《深入理解Java虚拟机》-----第12章 Java内存模型与线程

    概述 多任务处理在现代计算机操作系统中几乎已是一项必备的功能了.在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统速 ...

  2. Maya零基础新手入门教程第一部分:界面

    第1步:菜单 如果您曾经使用过一个软件,那么您将习惯菜单!在Maya中,菜单包含用于在场景中工作的工具和操作.与大多数程序一样,主菜单位于Maya窗口的顶部,然后还有面板和选项窗口的单独菜单.您还可以 ...

  3. Vue框架构造

    Vue 程序结构框架 Vue.js是典型的MVVM框架,什么是MVVM框架,介绍之前我们先介绍下什么是MVC框架 MVC 即 Model-View-Controller 的缩写,就是 模型-视图-控制 ...

  4. JavaScript回调函数和递归函数

    一.回调函数--通过函数的指针来调用函数 把一个函数的指针作为另一个函数的参数,当调用这个参数的时候,这个函数就叫做回调函数 在链式运动上会用到回调函数,之后运动会见到 A.通过指针来调用函数 B.通 ...

  5. POI读入Excel用String读取数值类型失真问题(精度丢失)

    问题:POI读取Excel数值单元格时,读取的小数数值与真实值不一致 话不多说,直接上代码! public static String getRealStringValue(Cell cell) { ...

  6. 网络攻防实验任务三_(2)X-Scan通用漏洞扫描实验

    首先在宿主机中打开xscan_gui.exe,结果系统直接将它删掉了. 大概是因为开了防火墙的缘故. 于是我在win7虚拟机中运行这个程序. 并且关闭防火墙,在win7中可以运行 我再试了一下win1 ...

  7. 整理了适合新手的20个Python练手小程序

    100个Python练手小程序,学习python的很好的资料,覆盖了python中的每一部分,可以边学习边练习,更容易掌握python. 本文附带基础视频教程:私信回复[基础]就可以获取的 [程序1] ...

  8. Knative 实战:如何在 Knative 中配置自定义域名及路由规则

    作者 | 元毅 阿里云智能事业群高级开发工程师 当前 Knative 中默认支持是基于域名的转发,可以通过域名模板配置后缀,但目前对于用户来说并不能指定全域名设置.另外一个问题就是基于 Path 和 ...

  9. 斯坦福机器学习课程 Exercise 习题四

    Exercise 4: Logistic Regression and Newton’s Method 回顾一下线性回归 hθ(x)=θTx Logistic Regression hθ(x)=11+ ...

  10. Java基础(38)AbstractMap类

    AbstractMap类的子类有HashMap(其子类是LinkedHashMap).TreeMap.EnumMap.WeakHashMap和IdentityHashMap. 1.HashMap (1 ...