1. 程式人生 > >.NET手擼繪製TypeScript類圖——下篇

.NET手擼繪製TypeScript類圖——下篇

.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等工具來繪製,但前端開發者通過需要面對好幾螢幕的類、方法和屬性,然後弄將其名稱、引數和型別一一拷貝到該工具中,這是一個需要極大耐心的工作。

“哪裡有需求,哪裡就有辦法”,這個小工具也許能給我們的客戶少許幫助,我正準備“說幹就幹”時——有人提醒我,我們的開發流程要先出文件,再寫程式碼。所以……理論上不應該存在這種工具