城城小时候 發表於 2019-11-15 09:02:00

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

<h1 id="net手撸绘制typescript类图下篇">.NET手撸绘制TypeScript类图——下篇</h1>
<p>在上篇的文章中,我们介绍了如何使用<code>.NET</code>解析<code>TypeScript</code>,这篇将介绍如何使用代码将类图渲染出来。</p>
<p>注:以防有人错过了,上篇链接如下:https://www.cnblogs.com/sdflysha/p/20191113-ts-uml-with-dotnet-1.html</p>
<h1 id="类型定义渲染">类型定义渲染</h1>
<p>不出意外,我们继续使用<code>FlysEngine</code>。虽然文字排版没做过,但不试试怎么知道好不好做呢?</p>
<p>正常实时渲染时,画一两行文字可能很容易,但绘制大量文字时,就需要引入一些排版操作了。为了实现排板,首先需要将<code>ClassDef</code>类扩充一下——干脆再加个<code>RenderingClassDef</code>类,包含一个<code>ClassDef</code>:</p>
<pre><code class="language-csharp">class RenderingClassDef
{
    public ClassDef Def { get; set; }

    public Vector2 Position { get; set; }

    public Vector2 Size { get; set; }

    public Vector2 Center =&gt; Position + Size / 2;
}
</code></pre>
<p>它包含了一些位置和大小信息,并提供了一个中间值的变量。之所以这样定义,因为这里存在了一些挺麻烦的过程,比如想想以下操作:</p>
<ul>
<li>如果我想绘制放在中间的<code>类名</code>,我就必须知道所有行的宽度</li>
<li>如果我想绘制边框,我也必须知道所有行的高度</li>
</ul>
<p>还好<code>Direct2D</code>/<code>DirectWrite</code>提供了方块的文字宽度、高度计算属性,通过<code>.Metrics</code>即可获取。有了这个,排板过程中,我认为最难处理的是<code>y</code>坐标了,它是一个状态机,需要实时去更新、计算<code>y</code>坐标的位置,绘制过程如下:</p>
<pre><code class="language-csharp">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&lt;TextLayout&gt; lines =
      classDef.Def.Properties.OrderByDescending(x =&gt; x.IsPublic).Select(x =&gt; x.ToString())
      .Concat(new string[] { "" })
      .Concat(classDef.Def.Methods.OrderByDescending(m =&gt; m.IsPublic).Select(x =&gt; x.ToString()))
      .Select(x =&gt; XResource.TextLayouts)
      .ToList();

    TextLayout titleLayout = XResource.TextLayouts;
    float width = Math.Max(titleLayout.Metrics.Width, lines.Max(x =&gt; 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) =&gt;
    {
      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);
}
</code></pre>
<p>请注意变量<code>y</code>的使用,我使用了一个<code>LINQ</code>中的<code>Aggregate</code>,实时的绘制并统计<code>y</code>变量的最新值,让代码简化了不少。</p>
<blockquote>
<p>这里我又取巧了,正常文章排板应该是<code>x</code>和<code>y</code>都需要更新,但这里<strong>每个定义都固定为一行</strong>,因此我不需要关心<code>x</code>的位置。但如果您想搞一些更<strong>骚</strong>的操作,如所有<strong>类型着个色</strong>,这时只需要同时更新<code>x</code>和<code>y</code>即可。</p>
</blockquote>
<p>此时渲染出来效果如下:<br>
<img src="https://img2018.cnblogs.com/blog/233608/201911/233608-20191114235439371-22780035.png" alt="" loading="lazy"></p>
<p>可见<code>类图</code>可能太小,我们可能需要局部放大一点,然后类图之间产生了重叠,我们需要拖拽的方式来移动到正确位置。</p>
<h2 id="放大和缩小">放大和缩小</h2>
<p>由于我们使用了<code>Direct2D</code>,无损的高清放大变得非常容易,首先我们需要定义一个变量,并响应鼠标滚轮事件:</p>
<pre><code class="language-csharp">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);
}
</code></pre>
<p>其中魔术值<code>1.1</code>代表,鼠标每滚动一次,放大<code>1.1</code>倍。</p>
<p>另外<code>mousePos</code>变量由鼠标移动事件的<code>X</code>和<code>Y</code>坐标经<code>worldTransform</code>的逆变换计算而来:</p>
<pre><code class="language-csharp">protected override void OnMouseMove(MouseEventArgs e)
{
    mousePos = XResource.InvertTransformPoint(worldTransform, new Vector2(e.X, e.Y));
}
</code></pre>
<blockquote>
<p>注意:</p>
<p>矩阵逆变换涉及一些高等数学中的线性代数知识,没必要立即掌握。只需知道矩阵变换可以变换点位置,矩阵逆变换可以恢复原点的位置。</p>
<p>在本文中鼠标移动的坐标是窗体提供的,换算成真实坐标,即需要进行“矩阵逆变换”——这在碰撞检测中很常见。</p>
</blockquote>
<p>以防我有需要,我们还再加一个快捷键,按空格即可立即恢复缩放:</p>
<pre><code class="language-csharp">protected override void OnKeyUp(KeyEventArgs e)
{
    if (e.KeyCode == Keys.Space) worldTransform = Matrix3x2.Identity;
}
</code></pre>
<p>然后在<code>OnDraw</code>事件中,将<code>worldTransform</code>应用起来即可:</p>
<pre><code class="language-csharp">protected override void OnDraw(DeviceContext ctx)
{
    ctx.Clear(Color.White);
    ctx.Transform = worldTransform; // 重点
    // 其它代码...
}
</code></pre>
<p>运行效果如下(注意放大缩小时,会以鼠标位置为中心点进行):<br>
<img src="https://img2018.cnblogs.com/blog/233608/201911/233608-20191114235447891-1149416789.gif" alt="" loading="lazy"></p>
<h2 id="碰撞检测和拖拽">碰撞检测和拖拽</h2>
<p>拖拽而已,为什么会和碰撞检测有关呢?</p>
<p>这是因为拖拽时,必须知道鼠标是否处于元素的上方,这就需要碰撞检测了。</p>
<p>首先给<code>RenderingClassDef</code>方法加一个<code>TestPoint()</code>方法,判断是鼠标是否与绘制位置重叠,这里我使用了<code>SharpDX</code>提供的<code>RectangleF.Contains(Vector2)</code>方法,具体算法已经不用关心,调用函数即可:</p>
<pre><code class="language-csharp">class RenderingClassDef
{
    // 其它代码...
    public bool TestPoint(Vector2 point) =&gt; new RectangleF(Position.X, Position.Y, Size.X, Size.Y).Contains(point);
}
</code></pre>
<p>然后在<code>OnDraw</code>方法中,做一个判断,如果类方框与鼠标出现重叠,则画一个宽度<code>2.0</code>的红色的边框,代码如下:</p>
<pre><code class="language-csharp">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);
}
</code></pre>
<p>测试效果如下(注意鼠标位置和红框):<br>
<img src="https://img2018.cnblogs.com/blog/233608/201911/233608-20191114235455641-986091007.gif" alt="" loading="lazy"></p>
<p>碰撞检测做好,就能写代码拖拽了。要实现拖拽,首先需要在<code>RenderingClassDef</code>类中定义两个变量,用于保存其起始位置和鼠标起始位置,用于计算鼠标移动距离:</p>
<pre><code class="language-csharp">class RenderingClassDef
{
    // 其它定义...

    public Vector2? CapturedPosition { get; set; }
   
    public Vector2 OriginPosition { get; set; }
}
</code></pre>
<p>然后在鼠标按下、鼠标移动、鼠标松开时进行判断,如果鼠标按下时处于某个类的方框里面,则记录这两个起始值:</p>
<pre><code class="language-csharp">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;
      }
    }
}
</code></pre>
<p>如果鼠标移动时,且有类的方框处于有值的状态,则计算偏移量,并让该方框随着鼠标移动:</p>
<pre><code class="language-csharp">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;
      }
    }
}
</code></pre>
<p>如果鼠标松开,则清除该记录值:</p>
<pre><code class="language-csharp">protected override void OnMouseUp(MouseEventArgs e)
{
    foreach (var item in this.AllClass.Values)
    {
      item.CapturedPosition = null;
    }
}
</code></pre>
<p>此时,运行效果如下:<br>
<img src="https://img2018.cnblogs.com/blog/233608/201911/233608-20191114235504665-825718493.gif" alt="" loading="lazy"></p>
<h2 id="类型间的关系">类型间的关系</h2>
<p>类型和类型之间是有依赖关系的,这也应该通过图形的方式体现出来。使用<code>DeviceContext.DrawLine()</code>方法即可画出线条,注意先画的会被后画的覆盖,因此这个<code>foreach</code>需要放在<code>OnDraw</code>方法的<code>foreach</code>语句之前:</p>
<pre><code class="language-csharp">foreach (var classDef in AllClass.Values)
{
    List&lt;string&gt; allTypes = classDef.Def.Properties.Select(x =&gt; x.Type).ToList();
    foreach (var kv in AllClass.Where(x =&gt; allTypes.Contains(x.Key)))
    {
      ctx.DrawLine(classDef.Center, kv.Value.Center, XResource.GetColor(Color.Gray), 2.0f);
    }
}
</code></pre>
<p>此时,运行效果如下:<br>
<img src="https://img2018.cnblogs.com/blog/233608/201911/233608-20191114235510461-1909388152.png" alt="" loading="lazy"></p>
<blockquote>
<p>注意:在真正的<code>UML</code>图中,除了依赖关系,继承关系也是需要体现的。而且线条是有箭头、且线条类型也是有讲究的,<code>Direct2D</code>支持自定义线条,这些都能做,权当留给各位自己去挑战尝试了。</p>
</blockquote>
<h2 id="方框顺序">方框顺序</h2>
<p>现在我们不能决定哪个在前,哪个在后,想象中方框可能应该就像窗体一样,客户点击哪个哪个就应该提到最前,这可以通过一个<code>ZIndex</code>变量来表示,首先在<code>RenderingClassDef</code>类中加一个属性:</p>
<pre><code class="language-csharp">public int ZIndex { get; set; } = 0;
</code></pre>
<p>然后在鼠标点击事件中,判断如果击中该类的方框,则将<code>ZIndex</code>赋值为最大值加1:</p>
<pre><code class="language-csharp">protected override void OnClick(EventArgs e)
{
    foreach (var item in this.AllClass.Values)
    {
      if (item.TestPoint(mousePos))
      {
            item.ZIndex = this.AllClass.Values.Max(v =&gt; v.ZIndex) + 1;
      }
    }
}
</code></pre>
<p>然后在<code>OnDraw</code>方法的第二个<code>foreach</code>循环,改成按<code>ZIndex</code>从小到大排序渲染即可:</p>
<pre><code class="language-csharp">// 其它代码...
foreach (var classDef in AllClass.Values.OrderBy(x =&gt; x.ZIndex))
// 其它代码...
</code></pre>
<p>运行效果如下(注意我的鼠标点击和前后顺序):<br>
<img src="https://img2018.cnblogs.com/blog/233608/201911/233608-20191114235533063-2117715088.gif" alt="" loading="lazy"></p>
<h1 id="总结">总结</h1>
<p>其实这是一个真实的需求,我们公司写代码时要求设计文档,通常我们都使用<code>ProcessOn</code>等工具来绘制,但前端开发者通过需要面对好几屏幕的类、方法和属性,然后弄将其名称、参数和类型一一拷贝到该工具中,这是一个需要极大耐心的工作。</p>
<p>“哪里有需求,哪里就有办法”,这个小工具也许能给我们的客户少许帮助,我正准备“说干就干”时——有人提醒我,我们的开发流程要先出文档,再写代码。所以……理论上不应该存在这种工具😂</p>
<p>但后来有一天,某同事突然点醒了我,“为什么不能有呢?这就叫<code>Code First</code>设计!”——是啊,<code>Entity Framework</code>也提供了<code>Code First</code>设计,很合理嘛,所以最后,就有了本篇文章😁。</p>
<p>本文所用到的<strong>完整代码</strong>,可以在我的<code>Github</code>仓库中下载:<br>
https://github.com/sdcb/blog-data/tree/master/2019/20191113-ts-uml-with-dotnet</p>
<p>喜欢的朋友 请关注我的微信公众号:【DotNet骚操作】</p>
<p><img src="https://img2018.cnblogs.com/blog/233608/201908/233608-20190825165420518-990227633.jpg" alt="DotNet骚操作" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/sdcb/p/20191114-ts-uml-with-dotnet-2.html
頁: [1]
查看完整版本: .NET手撸绘制TypeScript类图——下篇