拋磚引玉 之 VISIO VBA 繪制鏈表

“學(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程序,運(yùn)行后幾秒就完成了剩余圖片的大小調(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繪制也不算太麻煩,但是畢竟還得一個一個的繪制出來,于是就想能不能編制程序,通過讀取文件的形式將想繪制的圖形繪制出來。雖然自己編程可以實(shí)現(xiàn),但是自己編的程序,繪制出來的圖形可能將會比較粗糙,要想美觀,需要下一定的功夫才行,本來就是為了方便(懶)嘛。并且繪制的圖形希望可以進(jìn)行二次編輯和調(diào)整,如果這樣的功能全都由自己實(shí)現(xiàn)的話,光各種Style的設(shè)置就麻煩死了,畢竟咱不專業(yè)啊。于是就想,能不能用VBA直接在VISIO中繪制,這樣繪制出的圖形可以只是一個基本結(jié)構(gòu),美觀的話可以繼續(xù)進(jìn)行手工調(diào)整,這樣多省事兒,于是就有了本文所涉內(nèi)容。

圖 1就是通過VBA在VISIO繪制的,完全符合自己的要求,想上色上色,想修改修改,完全支持二次手工加工。下文就拿它的繪制作為“磚”,拋來引“玉”。

圖1 鏈表.jpg

不過,對于沒有VB基礎(chǔ)的人來說,想完成這樣的繪制,也還是有點(diǎn)麻煩的——需要熟悉熟悉VB的文法。當(dāng)然這都不是關(guān)鍵點(diǎn),關(guān)鍵點(diǎn)是MS給出的VISIO相關(guān)的VBA文檔,感覺簡直就是不想讓人看懂,很敷衍的樣子(也可能是自己膚淺了)。另外,網(wǎng)上的相關(guān)資料不能說少,但是很散。因此,個人自我感覺這塊磚,還是得拋的。

2. 基本概念

下面先介紹一下針對Visio進(jìn)行VBA編程中所需要了解的一些基本對象和概念。說實(shí)話,弄清楚下面這些對象究竟是啥含義還真是挺費(fèi)事兒。特別地,微軟提供的在線文檔,如果在不了解這些概念前看簡直就是天書,會感覺寫的很抽象,但是,在了解這些概念后就好很多,起碼知道在說啥,并且會發(fā)現(xiàn)理解起來也比較容易了。當(dāng)然,本來幫助文檔應(yīng)該是給不熟悉的人看的,但結(jié)果是讓不熟悉的人看著一頭霧水,那么本身本質(zhì)上還是文檔的撰寫者寫的不合格。別跟我抬杠,很多文檔的編輯者都有這樣的問題,我也不一定例外。那么來看下面的基礎(chǔ)概念:

  1. Page:頁面對象,承載各種繪制元素的頁面,包括前景和背景頁面。ActivePage,激活頁面,正在繪制圖新的當(dāng)前頁面。
  2. Stencil:模具,Master的一個集合,可以自己創(chuàng)建模具,包含自己所需的各種Master。
  3. Master:原件,是模具中的一個對象。
  4. Shape:圖形,在繪畫頁面中繪制的對象,一般通過從Stencil中拖拽一個Master對象,并放置到繪畫頁面上實(shí)現(xiàn)創(chuàng)建。
  5. 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實(shí)現(xiàn)繪制的那些Shape不會進(jìn)行修改。
圖2 VISIO對象說明.jpg
  1. Section:部分,1個Shape的Section包括多個Row和Cell,對應(yīng)存儲該Shape再某方面的特征屬性,如文字部分的特征屬性集合,線條屬性集合等。Shape的每個Section的具體內(nèi)容可以在點(diǎn)擊Visio“開發(fā)工具”菜單下的“ShapeSheet”按鈕后彈出的窗口中查看。每個Shape均有多個Section,每個Section中均包括若干的Row,每個Row中有若干Cell。
  2. Row:行,Section的組成部分,每個Section可以有一到多個Row,每一行就是一個Row。
  3. Cell:格子,Shape,Style,或者Row對象的屬性(之一)。如,Shape的每個Section中的每個Row中包括多個Cell,也就是說其中的一個Cell表示該Shape的在某Section下的某Row中的某個屬性。
圖3 Document Stencil和Shape Sheet顯示按鈕.jpg
圖4 Shape Sheet中的Row和Cell.jpg

具體各個概念可以見圖 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è)計(jì)

本文的樣例是一個鏈表(List),包括頭結(jié)點(diǎn)(HD),和鏈表中的各結(jié)點(diǎn)(Ai,i∈[1,n]),見圖 1所示。為了簡化問題,本文的鏈表就是由相同的結(jié)點(diǎn)(Node)構(gòu)成,不單設(shè)頭結(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu),和鏈表結(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu),以及表示鏈表的List結(jié)構(gòu)(沒學(xué)好數(shù)據(jù)結(jié)構(gòu)的請回爐重鑄)。因此,首先應(yīng)該定義鏈表結(jié)點(diǎn)的類型。另外,本人VB不靈,屬于現(xiàn)學(xué)現(xiàn)賣,所以不要在這方面來較真兒,且以后不在聲明。

Type ELEMENT_NODE_Shapes
    InforShape As Visio.Shape
    PointerShape As Visio.Shape
End Type

ELEMENT_NODE_Shapes是定義的鏈表結(jié)點(diǎn)對象類型,其中包括數(shù)據(jù)域InforShape和指針域PointerShape,它們都是Visio.Shape類型對象,也就是上文中的Shape類型對象。這樣就能夠之后在其基礎(chǔ)上繪制出兩個連續(xù)的shape(矩形)來表示鏈表結(jié)點(diǎn),見圖 5,第1給矩形為數(shù)據(jù)域,第2個矩形為指針域。

圖5 繪制出的鏈表結(jié)點(diǎn).jpg

首先,是DrawList過程。DrawList為主控程序,用于繪制鏈表,其中CreateNode用于在指定位置創(chuàng)建鏈表結(jié)點(diǎn)的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對象的繪制位置,即圖形外框的中心點(diǎn)(三角形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é)點(diǎn)用于頭結(jié)點(diǎn)的處理
    Dim HeadNode As ELEMENT_NODE_Shapes
    //' 通過CreateNode在指定的位置創(chuàng)建頭結(jié)點(diǎn),并指定頭結(jié)點(diǎn)的數(shù)據(jù)域顯示的字符串
    HeadNode = CreateNode(PosX, PosY, "HD")
    //' 定義結(jié)點(diǎn)數(shù)組
    Dim Node(100) As ELEMENT_NODE_Shapes
    //' PrevNode用于之后的鏈表連續(xù)處理
    Dim PrevNode As ELEMENT_NODE_Shapes
    PrevNode = HeadNode
    //'連續(xù)創(chuàng)建10個結(jié)點(diǎn),并且用ConnectTwoNode實(shí)現(xiàn)結(jié)點(diǎn)Shape的連線(帶箭頭的線)
    For Index = 1 To 10
        PosX = PosX + Delta //'橫坐標(biāo)每次遞增Delta
        //' 鏈表終結(jié)點(diǎn)Shape數(shù)據(jù)域顯示的文字是A和index湊成的字符串,如A5
        Node(Index - 1) = CreateNode((PosX), PosY, "A" & Index)
        //' 每次將PrevNode和當(dāng)前新創(chuàng)建的結(jié)點(diǎn)進(jìn)行連線
        Call ConnectTwoNode(PrevNode, Node(Index - 1))
        PrevNode = Node(Index - 1) //' PrevNode綁定到新創(chuàng)建的結(jié)點(diǎn)上,以便后繼處理
        //' 圖形繪制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)容有:

  1. ActivePage.Drop,該方法將指定的Stencil中的Master對象繪制到頁面中。
  2. 通過Application.Documents.Item指定想要的Stencil,如本文使用的“BASIC_M.vssx”(“基本形狀”)。至于具體如何知道那個Stencil叫啥名字,本人也沒有太好的辦法,目前采用就是通過錄制宏的形式,拖動一個想要的Stencil中的Master,來查看錄制好的宏代碼中的Stencil和Master的名稱。特別是本人使用的是中文版的Visio??纯匆院笥袥]有啥好方法,您如果知道請不吝賜教!
  3. 設(shè)置一個繪制好的Shape的文字的字體大小需要訪問該Shape對應(yīng)的Characters的Section中的Row的存儲字體配置的Cell。下面TempNode.InforShape,就是結(jié)點(diǎn)的數(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其實(shí)也是可以的,具體可以參考MS文檔:https://docs.microsoft.com/en-us/office/vba/api/visio.characters.charprops,建議有條件的話,還是看英文的文檔。另外,Cell有不同的訪問方式,如:可以通過Shape對象的CellsSRC方法訪問。
圖6 Character Section和Size Cell.jpg
  1. 可以通過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提供的文檔真是要命,根本不知道他們在說啥,費(fèi)事兒就費(fèi)事兒在這里了。
  2. 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
  3. 獲得了Cell,通過設(shè)置Formula,類似于Excel中的每個方格cell,ShapeSheet里面每個Cell都可以使用公式來完成屬性的設(shè)置,這點(diǎn)不得不佩服MS的強(qiáng)大,能夠左到不同工具的統(tǒng)一的處理(好像也就應(yīng)該這么做才更合理)。
  4. 組合繪制好的數(shù)據(jù)域和指針域的Shape。組合是先建立選擇Selection對象,然后選擇各個Shape,然后調(diào)用Selection對象的Group方法將已經(jīng)選定的各個圖形進(jìn)行組合。需要說明一下,選擇第一個Shape時(shí),需要保證沒有選擇其他的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é)點(diǎn)結(jié)構(gòu)的對象,而不是Group之后的對象,因?yàn)橹笾羔樣虻腟hape需要進(jìn)行連線到后一個結(jié)點(diǎn)的數(shù)據(jù)域?qū)ο笊?    CreateNode = TempNode
End Function

再次,ConnectTwoNode使用一個連接線(帶箭頭)連接兩個Node的對應(yīng)Shape,前一個Node的指針域Shape的X2連接點(diǎn)作為起點(diǎn),后一個Node的數(shù)據(jù)域Shape的X4連接點(diǎn)的作為終點(diǎn)。其中,需要說明的有:

  1. 連接線,本文里面選擇的是動態(tài)連接線,就是開始菜單下的“連接線”(一般在“指針工具”下方),因?yàn)槠淇梢愿鶕?jù)連接的兩個Shape之間的相對位置會“動態(tài)”調(diào)整線條布局。也可以根據(jù)需求選擇調(diào)用Shape的一些圖形繪制方法去創(chuàng)建,如:DrawArcByThreePoints,DrawLine等。
  2. 動態(tài)連接線默認(rèn)沒有箭頭,需要進(jìn)行設(shè)置,通過CellsSRC方法可以獲的連接線對應(yīng)的屬性的Cell,通過設(shè)置屬性值改變該屬性。
  3. 連接Shape,先獲得連接線的起點(diǎn)Cell和終點(diǎn)Cell,將使用起點(diǎn)Cell和終點(diǎn)Cell的的GlutTo方法,分別連接到不同Shape的連接點(diǎn)(Connection Point)上。
  4. 不同類型的Shape,會有不同數(shù)量的Connection Point,如:三角形有四個,分別是三個頂點(diǎn)和中間的,名稱分別是Connection.X1~ Connection.X4(名稱缺省不顯示,見紅框左側(cè),但可以選擇一個后點(diǎn)擊其他灰色框的觀察其缺省名稱),具體的情況可以通過觀察該Shape的SheetShape進(jìn)行確定,見圖 7所示。
圖7 三角形的連接點(diǎn).jpg
//' 使用動態(tài)連線,連接2個鏈表結(jié)點(diǎn)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ǎn),然后調(diào)用連接線兩端Cell的GlueTo操作進(jìn)行連接,由于從前向后,因此分別時(shí)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

最后,運(yùn)行該VBA程序,就可以在Visio的當(dāng)前頁面中繪制出如圖 1 所示的鏈表。

本文主要做拋磚引玉之用,通過提供一個完整的樣例來展示如何通過VBA編程實(shí)現(xiàn)在Visio中進(jìn)行圖形的繪制。通過VBA可以方便的操作各種圖形,有興趣的讀者可以實(shí)現(xiàn)相關(guān)的函數(shù)庫,方便自己組合使用。

如果本文對你有幫助,一定要點(diǎn)贊!記得,別光看不點(diǎn)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容