.NET手撸绘制TypeScript类图——下篇
.NET手擼繪制TypeScript類圖——下篇
在上篇的文章中,我們介紹了如何使用?.NET解析?TypeScript,這篇將介紹如何使用代碼將類圖渲染出來。
類型定義渲染
不出意外,我們繼續使用?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等工具來繪制,但前端開發者通過需要面對好幾屏幕的類、方法和屬性,然后弄將其名稱、參數和類型一一拷貝到該工具中,這是一個需要極大耐心的工作。
“哪里有需求,哪里就有辦法”,這個小工具也許能給我們的客戶少許幫助,我正準備“說干就干”時——有人提醒我,我們的開發流程要先出文檔,再寫代碼。所以……理論上不應該存在這種工具😂
但后來有一天,某同事突然點醒了我,“為什么不能有呢?這就叫?CodeFirst設計!”——是啊,?EntityFramework也提供了?CodeFirst設計,很合理嘛,所以最后,就有了本篇文章😁。
微信公眾號無法評論,有什么想法各位可以轉至我的博客園留言/評論/點贊:https://www.cnblogs.com/sdflysha/p/20191114-ts-uml-with-dotnet-2.html
本文所用到的完整代碼,可以在我的?Github倉庫中下載:https://github.com/sdcb/blog-data/tree/master/2019/20191113-ts-uml-with-dotnet
總結
以上是生活随笔為你收集整理的.NET手撸绘制TypeScript类图——下篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 行云万里,转型未来 | 行云创新受邀参加
- 下一篇: 中国.net 开发者峰会 2019 数字