“學(xué)習(xí)永遠(yuǎn)是個痛苦的過程懊昨,需要極大的勇氣才能持之以恒”
1. 動機(jī)
似乎是十年之前窄潭,在得到一本圖書的掃描版pdf后,由于非常喜歡該圖書酵颁,所以進(jìn)行了“重新編輯”(只是留給自己看嫉你,從未散播過給他人)。該圖書主要是“圖文配合”躏惋,且是以圖為主的一本書幽污。因此,在編輯過程中簿姨,需要將pdf中的圖片復(fù)制并粘貼到word中距误,并且需要根據(jù)我自己的排版調(diào)整大小簸搞。由于該圖書的圖片特別多,近2000多張圖片的樣子准潭,在調(diào)整了100張圖片后果斷放棄手工調(diào)整圖片大小的方式趁俊,太受罪了。于是想起使用宏來處理刑然,完全可以選擇圖片后錄制宏來幫我完成寺擂,但是這樣也得一張張?zhí)幚恚€是麻煩闰集。因此沽讹,第一次“鼓起勇氣”拿起VBA這個神器來。雖然之前沒有學(xué)過任何VB的內(nèi)容(就是懶武鲁,不愿意學(xué))爽雄,但在搜索+嘗試的基礎(chǔ)上20分鐘的樣子搞定了一個VBA程序,運行后幾秒就完成了剩余圖片的大小調(diào)整沐鼠,完美完成任務(wù)挚瘟。這樣,第一次體會到VBA的好處饲梭。雖然是有好處乘盖,但咱也不做編輯,so憔涉,之后再也沒有用過VBA订框。雖然MS的OFFICE功能超強(qiáng),但畢竟“學(xué)習(xí)永遠(yuǎn)是個痛苦的過程”兜叨,還是能不學(xué)就不學(xué)吧(懶是一種態(tài)度)穿扳。這里吐槽一下,有專門的“人才”跑學(xué)校來講LATEX国旷,說其功能如何強(qiáng)大矛物,且曰Word完成不了的LATEX可以。我只想說跪但,那是您真·不會使Word履羞。這么好的所見即所得(WYSIWYG)工具為啥不好好用,干嘛給自己找別扭屡久?推薦侯捷老師的書《Word排版藝術(shù)》忆首,針對Word2003的,但是里面的內(nèi)容即使2019也足夠你使用了被环,并且還有VBA這一神器糙及。
寫了那么多,下面進(jìn)入正題蛤售。
由于各種原因丁鹉,經(jīng)常要將某種的數(shù)據(jù)結(jié)構(gòu)展示為圖型妒潭,特別是圖啊,樹啊什么的揣钦,每次雖然拿VISIO繪制也不算太麻煩雳灾,但是畢竟還得一個一個的繪制出來,于是就想能不能編制程序冯凹,通過讀取文件的形式將想繪制的圖形繪制出來谎亩。雖然自己編程可以實現(xiàn),但是自己編的程序宇姚,繪制出來的圖形可能將會比較粗糙匈庭,要想美觀,需要下一定的功夫才行浑劳,本來就是為了方便(懶)嘛阱持。并且繪制的圖形希望可以進(jìn)行二次編輯和調(diào)整,如果這樣的功能全都由自己實現(xiàn)的話魔熏,光各種Style的設(shè)置就麻煩死了衷咽,畢竟咱不專業(yè)啊。于是就想蒜绽,能不能用VBA直接在VISIO中繪制镶骗,這樣繪制出的圖形可以只是一個基本結(jié)構(gòu),美觀的話可以繼續(xù)進(jìn)行手工調(diào)整躲雅,這樣多省事兒鼎姊,于是就有了本文所涉內(nèi)容。
圖 1就是通過VBA在VISIO繪制的相赁,完全符合自己的要求相寇,想上色上色,想修改修改噪生,完全支持二次手工加工裆赵。下文就拿它的繪制作為“磚”东囚,拋來引“玉”跺嗽。
不過,對于沒有VB基礎(chǔ)的人來說页藻,想完成這樣的繪制桨嫁,也還是有點麻煩的——需要熟悉熟悉VB的文法。當(dāng)然這都不是關(guān)鍵點份帐,關(guān)鍵點是MS給出的VISIO相關(guān)的VBA文檔璃吧,感覺簡直就是不想讓人看懂,很敷衍的樣子(也可能是自己膚淺了)废境。另外畜挨,網(wǎng)上的相關(guān)資料不能說少筒繁,但是很散。因此巴元,個人自我感覺這塊磚毡咏,還是得拋的。
2. 基本概念
下面先介紹一下針對Visio進(jìn)行VBA編程中所需要了解的一些基本對象和概念逮刨。說實話呕缭,弄清楚下面這些對象究竟是啥含義還真是挺費事兒。特別地修己,微軟提供的在線文檔恢总,如果在不了解這些概念前看簡直就是天書,會感覺寫的很抽象睬愤,但是片仿,在了解這些概念后就好很多,起碼知道在說啥尤辱,并且會發(fā)現(xiàn)理解起來也比較容易了滋戳。當(dāng)然,本來幫助文檔應(yīng)該是給不熟悉的人看的啥刻,但結(jié)果是讓不熟悉的人看著一頭霧水奸鸯,那么本身本質(zhì)上還是文檔的撰寫者寫的不合格。別跟我抬杠可帽,很多文檔的編輯者都有這樣的問題娄涩,我也不一定例外。那么來看下面的基礎(chǔ)概念:
- Page:頁面對象映跟,承載各種繪制元素的頁面蓄拣,包括前景和背景頁面。ActivePage努隙,激活頁面球恤,正在繪制圖新的當(dāng)前頁面。
- Stencil:模具荸镊,Master的一個集合咽斧,可以自己創(chuàng)建模具,包含自己所需的各種Master躬存。
- Master:原件张惹,是模具中的一個對象。
- Shape:圖形岭洲,在繪畫頁面中繪制的對象宛逗,一般通過從Stencil中拖拽一個Master對象,并放置到繪畫頁面上實現(xiàn)創(chuàng)建盾剩。
- Document Stencil:文檔模具雷激,一個特殊的Stencil替蔬,它里面存放的是從Stencil中拖到繪畫頁面中的Shape所用的那些Master的副本。請注意這里的Master不是直接引用屎暇,是copy进栽,但如果你創(chuàng)建的多個Shape是來自同一個Master,那么Document Stencil中則不重新創(chuàng)建新的同樣的Master恭垦,除非你之前修改了那個被copy的Master快毛。這樣做有什么好處呢?好處是番挺,如果你想修改繪制頁面中的現(xiàn)有的由相同的Master創(chuàng)建的所有Shape的style唠帝,那么你只要修改Document Stencil中的這個Master就能夠?qū)崿F(xiàn)全部修改。但你如果修改的是Stencil中的Master玄柏,則當(dāng)前繪畫頁面中之前是由拖拽這個Master實現(xiàn)繪制的那些Shape不會進(jìn)行修改襟衰。
- Section:部分,1個Shape的Section包括多個Row和Cell粪摘,對應(yīng)存儲該Shape再某方面的特征屬性瀑晒,如文字部分的特征屬性集合,線條屬性集合等徘意。Shape的每個Section的具體內(nèi)容可以在點擊Visio“開發(fā)工具”菜單下的“ShapeSheet”按鈕后彈出的窗口中查看苔悦。每個Shape均有多個Section,每個Section中均包括若干的Row椎咧,每個Row中有若干Cell玖详。
- Row:行,Section的組成部分勤讽,每個Section可以有一到多個Row蟋座,每一行就是一個Row。
- Cell:格子脚牍,Shape向臀,Style,或者Row對象的屬性(之一)诸狭。如券膀,Shape的每個Section中的每個Row中包括多個Cell,也就是說其中的一個Cell表示該Shape的在某Section下的某Row中的某個屬性作谚。
具體各個概念可以見圖 2三娩、圖 3和圖 4中的標(biāo)注庵芭。還有其他的相關(guān)概念妹懒,如Selection等,這里不介紹双吆,在后面隨著使用會具體說明眨唬。特別地会前,圖4中的ShapeSheet對應(yīng)的Shape是ActivePage中被選中的“三角形”Shape,相應(yīng)的Row和Cell也是這個“三角形”的匾竿。通過這次研究Visio VBA才發(fā)現(xiàn)人家軟件的強(qiáng)大瓦宜,真要是自己開發(fā)滿足之前所述需求的軟件,那會累死的岭妖。
3. 繪制設(shè)計
本文的樣例是一個鏈表(List)临庇,包括頭結(jié)點(HD),和鏈表中的各結(jié)點(Ai昵慌,i∈[1,n])假夺,見圖 1所示。為了簡化問題斋攀,本文的鏈表就是由相同的結(jié)點(Node)構(gòu)成已卷,不單設(shè)頭結(jié)點的數(shù)據(jù)結(jié)構(gòu),和鏈表結(jié)點的數(shù)據(jù)結(jié)構(gòu)淳蔼,以及表示鏈表的List結(jié)構(gòu)(沒學(xué)好數(shù)據(jù)結(jié)構(gòu)的請回爐重鑄)侧蘸。因此,首先應(yīng)該定義鏈表結(jié)點的類型鹉梨。另外讳癌,本人VB不靈,屬于現(xiàn)學(xué)現(xiàn)賣存皂,所以不要在這方面來較真兒析桥,且以后不在聲明。
Type ELEMENT_NODE_Shapes
InforShape As Visio.Shape
PointerShape As Visio.Shape
End Type
ELEMENT_NODE_Shapes是定義的鏈表結(jié)點對象類型艰垂,其中包括數(shù)據(jù)域InforShape和指針域PointerShape泡仗,它們都是Visio.Shape類型對象,也就是上文中的Shape類型對象猜憎。這樣就能夠之后在其基礎(chǔ)上繪制出兩個連續(xù)的shape(矩形)來表示鏈表結(jié)點娩怎,見圖 5,第1給矩形為數(shù)據(jù)域胰柑,第2個矩形為指針域截亦。
首先,是DrawList過程柬讨。DrawList為主控程序崩瓤,用于繪制鏈表,其中CreateNode用于在指定位置創(chuàng)建鏈表結(jié)點的Shape踩官,ConnectTwoNode用“連線”Shape連接創(chuàng)建的兩個鏈表Shape却桶。簡要說明VB中Sub和Function的區(qū)別:Sub就是表示過程,無返回值;Function是有返回值的颖系,具體請參考VB相關(guān)資料嗅剖。另外,需要注意的是Visio的頁面的坐標(biāo)和平常熟悉的Windows坐標(biāo)不太一樣嘁扼,Windows的坐標(biāo)是左上角為<0, 0>信粮,但是Visio中式左下角為<0, 0>。本文的Visio文檔是使用美制單位(英寸)趁啸,所以1個單位長度就是1英寸强缘。PosX和PosY分別對應(yīng)一個Shape對象的繪制位置,即圖形外框的中心點(三角形Shape的外框也是矩形的)不傅。
注意:在簡書的代碼編輯系統(tǒng)中欺旧,不認(rèn)VB的注釋,所以前面加了//蛤签,如果你copy 的話辞友,請恢復(fù)。
Sub DrawList()
Dim Delta As Double
//' 增量震肮,1.25個單位称龙,本文是英寸
Delta = 1.25
Dim PosX As Double
Dim PosY As Double
PosX = 0.5 //' 圖形繪制X坐標(biāo)
PosY = 10 //' 圖形繪制Y坐標(biāo)
//' 頭結(jié)點用于頭結(jié)點的處理
Dim HeadNode As ELEMENT_NODE_Shapes
//' 通過CreateNode在指定的位置創(chuàng)建頭結(jié)點,并指定頭結(jié)點的數(shù)據(jù)域顯示的字符串
HeadNode = CreateNode(PosX, PosY, "HD")
//' 定義結(jié)點數(shù)組
Dim Node(100) As ELEMENT_NODE_Shapes
//' PrevNode用于之后的鏈表連續(xù)處理
Dim PrevNode As ELEMENT_NODE_Shapes
PrevNode = HeadNode
//'連續(xù)創(chuàng)建10個結(jié)點戳晌,并且用ConnectTwoNode實現(xiàn)結(jié)點Shape的連線(帶箭頭的線)
For Index = 1 To 10
PosX = PosX + Delta //'橫坐標(biāo)每次遞增Delta
//' 鏈表終結(jié)點Shape數(shù)據(jù)域顯示的文字是A和index湊成的字符串鲫尊,如A5
Node(Index - 1) = CreateNode((PosX), PosY, "A" & Index)
//' 每次將PrevNode和當(dāng)前新創(chuàng)建的結(jié)點進(jìn)行連線
Call ConnectTwoNode(PrevNode, Node(Index - 1))
PrevNode = Node(Index - 1) //' PrevNode綁定到新創(chuàng)建的結(jié)點上,以便后繼處理
//' 圖形繪制Y坐標(biāo)的控制沦偎,達(dá)到一定程度就“換行”
If Index Mod 5 = 0 Then
PosX = 0.5
PosY = PosY - 1
End If
Next
End Sub
其次疫向,是CreateNode方法,用于在start_x和start_y位置繪制創(chuàng)建的Node并返回創(chuàng)建的Node豪嚎,該Node的數(shù)據(jù)域顯示的是由info傳入的String搔驼。其中需要說明的內(nèi)容有:
- ActivePage.Drop,該方法將指定的Stencil中的Master對象繪制到頁面中侈询。
- 通過Application.Documents.Item指定想要的Stencil舌涨,如本文使用的“BASIC_M.vssx”(“基本形狀”)。至于具體如何知道那個Stencil叫啥名字扔字,本人也沒有太好的辦法囊嘉,目前采用就是通過錄制宏的形式,拖動一個想要的Stencil中的Master革为,來查看錄制好的宏代碼中的Stencil和Master的名稱扭粱。特別是本人使用的是中文版的Visio≌痖荩看看以后有沒有啥好方法琢蛤,您如果知道請不吝賜教!
- 設(shè)置一個繪制好的Shape的文字的字體大小需要訪問該Shape對應(yīng)的Characters的Section中的Row的存儲字體配置的Cell。下面TempNode.InforShape虐块,就是結(jié)點的數(shù)據(jù)域?qū)?yīng)的Shape俩滥,其通過.Characters獲得對應(yīng)的字符Section屬性對象,并通過.CharProps屬性嘉蕾,指定visCharacterSize參數(shù)獲得字符Size的屬性贺奠,并設(shè)置為14pt,見圖 6错忱,紅框中是該Shape的Character Section儡率,藍(lán)框中的是對應(yīng)Size的Cell。其中visCharacterSize是一個枚舉量以清,其值為7儿普,你寫7其實也是可以的,具體可以參考MS文檔:https://docs.microsoft.com/en-us/office/vba/api/visio.characters.charprops掷倔,建議有條件的話眉孩,還是看英文的文檔。另外勒葱,Cell有不同的訪問方式浪汪,如:可以通過Shape對象的CellsSRC方法訪問。
- 可以通過Shape.CellsSRC屬性來訪問一個Shape的某Section下的某Row中的某Cell凛虽,要么怎么名字中有SRC死遭,具體可以參考MS文檔:https://docs.microsoft.com/en-us/office/vba/api/visio.shape.cellssrc。每個Shape都有一個ShapeSheet凯旋,這個就相當(dāng)于一個三維表呀潭,那么通過第1個參確定要訪問的Section,第2個參數(shù)指定確定的Section中要訪問的Row至非,第3個參數(shù)指定該Row中的Cell钠署。在沒有了解SheetShape的作用之前,看MS提供的文檔真是要命荒椭,根本不知道他們在說啥踏幻,費事兒就費事兒在這里了。
- Section戳杀、Row该面、Cell的枚舉量的定義,請參考MS的這3篇文檔信卡,有了它你就能夠拿到Shape中的任何你想要的Cell:
a) https://docs.microsoft.com/en-us/office/vba/api/visio.vissectionindices
b) https://docs.microsoft.com/en-us/office/vba/api/visio.visrowindices
c) https://docs.microsoft.com/en-us/office/vba/api/visio.viscellindices - 獲得了Cell隔缀,通過設(shè)置Formula,類似于Excel中的每個方格cell傍菇,ShapeSheet里面每個Cell都可以使用公式來完成屬性的設(shè)置猾瘸,這點不得不佩服MS的強(qiáng)大,能夠左到不同工具的統(tǒng)一的處理(好像也就應(yīng)該這么做才更合理)。
- 組合繪制好的數(shù)據(jù)域和指針域的Shape牵触。組合是先建立選擇Selection對象淮悼,然后選擇各個Shape,然后調(diào)用Selection對象的Group方法將已經(jīng)選定的各個圖形進(jìn)行組合揽思。需要說明一下袜腥,選擇第一個Shape時,需要保證沒有選擇其他的Shape钉汗,因此羹令,Select方法的的選擇動作參數(shù)(第2個參數(shù))要綁定visDeselectAll 和 visSelect,表示先全不選然后選擇該Shape损痰。
Function CreateNode(start_x As Double, start_y As Double, info As String) As ELEMENT_NODE_Shapes
Dim TempNode As ELEMENT_NODE_Shapes
Set TempNode.InforShape =
//' 在當(dāng)前頁面Drop一個矩形到指定位置福侈,該矩形由BASIC_M.vssx這個Stencil里面的矩形Master得到
ActivePage.Drop(Application.Documents.Item("BASIC_M.vssx").Masters.ItemU("Rectangle"), start_x, start_y)
TempNode.InforShape.Text = info //' 設(shè)置該矩形的文字信息為參數(shù)info的內(nèi)容
//' 設(shè)置該Shape的字符size的屬性
TempNode.InforShape.Characters.CharProps(visCharacterSize) = 14
//' 繪制指針域的Shape,橫移0.5個單位長度
Set TempNode.PointerShape = ActivePage.Drop(Application.Documents.Item("BASIC_M.vssx").Masters.ItemU("Rectangle"), start_x + 0.5, start_y)
//' 聲明一個Cell變量卢未,用于之后訪問不同的Cell
Dim TempCell As Visio.Cell
//' 設(shè)置繪制矩形(數(shù)據(jù)域和指針域)的透明度為0.8(80%的透明度)
Set TempCell = TempNode.InforShape.CellsSRC(visSectionObject, visRowFill, visFillForegndTrans)
TempCell.Formula = "0.8"
Set TempCell = TempNode.PointerShape.CellsSRC(visSectionObject, visRowFill, visFillForegndTrans)
TempCell.Formula = "0.8"
//' 設(shè)置繪制矩形(數(shù)據(jù)域和指針域)的寬和長肪凛,均為0.5英寸
Set TempCell = TempNode.InforShape.CellsSRC(visSectionObject, visRowXFormOut, visXFormWidth)
TempCell.Formula = "0.5"
Set TempCell = TempNode.InforShape.CellsSRC(visSectionObject, visRowXFormOut, visXFormHeight)
TempCell.Formula = "0.5"
Set TempCell = TempNode.PointerShape.CellsSRC(visSectionObject, visRowXFormOut, visXFormWidth)
TempCell.Formula = "0.5"
Set TempCell = TempNode.PointerShape.CellsSRC(visSectionObject, visRowXFormOut, visXFormHeight)
TempCell.Formula = "0.5"
//' 下面要進(jìn)行組合,所以聲明一個選擇對象的變量
Dim Selection As Visio.Selection
//' 設(shè)置為激活窗口的選擇
Set Selection = ActiveWindow.Selection
//' 選擇數(shù)據(jù)域的矩形Shape辽社,并且先全部去掉選擇伟墙,然后再選擇,防止選取了其他的Shape
Selection.Select TempNode.InforShape, visDeselectAll + visSelect
//' 然后再選擇指針域的矩形Shape
Selection.Select TempNode.PointerShape, visSelect
//' 申明一個Shape變量爹袁,之后綁定組合后的對象
Dim GroupShape As Visio.Shape
//' 將之前選擇的Shape進(jìn)行組合远荠,組合的結(jié)果綁定到GroupShape上
Set GroupShape = Selection.Group
//' 返回的是創(chuàng)建的TempNode,也就是鏈表結(jié)點結(jié)構(gòu)的對象失息,而不是Group之后的對象譬淳,因為之后指針域的Shape需要進(jìn)行連線到后一個結(jié)點的數(shù)據(jù)域?qū)ο笊? CreateNode = TempNode
End Function
再次,ConnectTwoNode使用一個連接線(帶箭頭)連接兩個Node的對應(yīng)Shape盹兢,前一個Node的指針域Shape的X2連接點作為起點邻梆,后一個Node的數(shù)據(jù)域Shape的X4連接點的作為終點。其中绎秒,需要說明的有:
- 連接線浦妄,本文里面選擇的是動態(tài)連接線,就是開始菜單下的“連接線”(一般在“指針工具”下方)见芹,因為其可以根據(jù)連接的兩個Shape之間的相對位置會“動態(tài)”調(diào)整線條布局剂娄。也可以根據(jù)需求選擇調(diào)用Shape的一些圖形繪制方法去創(chuàng)建,如:DrawArcByThreePoints玄呛,DrawLine等阅懦。
- 動態(tài)連接線默認(rèn)沒有箭頭,需要進(jìn)行設(shè)置徘铝,通過CellsSRC方法可以獲的連接線對應(yīng)的屬性的Cell耳胎,通過設(shè)置屬性值改變該屬性惯吕。
- 連接Shape,先獲得連接線的起點Cell和終點Cell怕午,將使用起點Cell和終點Cell的的GlutTo方法废登,分別連接到不同Shape的連接點(Connection Point)上。
- 不同類型的Shape郁惜,會有不同數(shù)量的Connection Point堡距,如:三角形有四個,分別是三個頂點和中間的扳炬,名稱分別是Connection.X1~ Connection.X4(名稱缺省不顯示吏颖,見紅框左側(cè)搔体,但可以選擇一個后點擊其他灰色框的觀察其缺省名稱)恨樟,具體的情況可以通過觀察該Shape的SheetShape進(jìn)行確定,見圖 7所示疚俱。
//' 使用動態(tài)連線劝术,連接2個鏈表結(jié)點Node中的指針域和數(shù)據(jù)域的Shape
Sub ConnectTwoNode(PrevNode As ELEMENT_NODE_Shapes, NextNode As ELEMENT_NODE_Shapes)
//' 聲明并獲取“動態(tài)連線”的Shape
Dim Line As Visio.Shape
Set Line = ActivePage.Drop(ActiveDocument.Masters.ItemU("Dynamic connector"), 1, 1)
//' 由于缺省的連線不帶箭頭,因此需要在連線的末位位置設(shè)置箭頭呆奕,同樣通過CellsSRC獲取對應(yīng)的Cell
Dim ArrowOfLine As Visio.Cell
//' 設(shè)置箭頭的類型
Set ArrowOfLine = Line.CellsSRC(visSectionObject, visRowLine, visLineEndArrow)
ArrowOfLine.Formula = "5"
//' 設(shè)置箭頭的大小
Set ArrowOfLine = Line.CellsSRC(visSectionObject, visRowLine, visLineEndArrowSize)
ArrowOfLine.Formula = "2"
Dim LineBeginX As Visio.Cell
Dim LineEndX As Visio.Cell
//' 獲得連線的兩端的Cell
Set LineBeginX = Line.Cells("BeginX")
Set LineEndX = Line.Cells("EndX")
//' 通過連接線的名字獲取前面Node的指針域和后面Node的數(shù)據(jù)域?qū)?yīng)的連接點养晋,然后調(diào)用連接線兩端Cell的GlueTo操作進(jìn)行連接,由于從前向后梁钾,因此分別時X2和X4
Dim CellGlueToRect01 As Visio.Cell
Set CellGlueToRect01 = PrevNode.PointerShape.Cells("Connections.X2")
Dim CellGlueToRect02 As Visio.Cell
Set CellGlueToRect02 = NextNode.InforShape.Cells("Connections.X4")
LineBeginX.GlueTo CellGlueToRect01
LineEndX.GlueTo CellGlueToRect02
End Sub
最后绳泉,運行該VBA程序,就可以在Visio的當(dāng)前頁面中繪制出如圖 1 所示的鏈表姆泻。
本文主要做拋磚引玉之用零酪,通過提供一個完整的樣例來展示如何通過VBA編程實現(xiàn)在Visio中進(jìn)行圖形的繪制。通過VBA可以方便的操作各種圖形拇勃,有興趣的讀者可以實現(xiàn)相關(guān)的函數(shù)庫四苇,方便自己組合使用。