.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
變數的最新值,讓程式碼簡化了不少。
這裡我又取巧了,正常文章排板應該是
x
和y
都需要更新,但這裡每個定義都固定為一行,因此我不需要關心x
的位置。但如果您想搞一些更騷的操作,如所有型別著個色,這時只需要同時更新x
和y
即可。
此時渲染出來效果如下:
可見類圖
可能太小,我們可能需要區域性放大一點,然後類圖之間產生了重疊,我們需要拖拽的方式來移動到正確位置。
放大和縮小
由於我們使用了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
變數由滑鼠移動事件的X
和Y
座標經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
等工具來繪製,但前端開發者通過需要面對好幾螢幕的類、方法和屬性,然後弄將其名稱、引數和型別一一拷貝到該工具中,這是一個需要極大耐心的工作。
“哪裡有需求,哪裡就有辦法”,這個小工具也許能給我們的客戶少許幫助,我正準備“說幹就幹”時——有人提醒我,我們的開發流程要先出文件,再寫程式碼。所以……理論上不應該存在這種工具