孫海民, 姜學東, 計大杰, 于萬國
(河北民族師范學院 數字與計算機科學學院,河北 承德 067000)
隨著大數據產業發展,在可視化數據結果時,人們希望將分析結果如文本、圖片、表格、數字等保存到文件,自動生成數據分析報告。筆者調查90%以上的數據分析管理系統存在這樣需求。文獻調研發現90%的MS Office自動化客戶端項目都使用VBA開發,如《基于Excel VBA的漁具選擇性分析SELECT模型實現》[1]《基于Excel的水泵性能試驗數據處理的VBA開發》[2]《利用VBA技術和EndNote軟件建立查新報告數據庫》[3]。使用C++開發Microsoft Office自動化客戶端研究很少,并且存在設計缺陷。以VC開發環境下生成Word文件為例,有人提出將數據分析結果寫入臨時文件,再通過VBA從臨時文件中讀取數據并寫入Word中[4]。由于使用臨時文件和VBA會造成應用程序工作效率降低,究其原因是人們對自動化(Automation)技術研究不足。C++作為主流軟件開發語言在開發MS Office自動化客戶端方面研究不足。另外,已有文獻關于自動化技術和MS Office組件對象模型研究不夠全面和深入。深入研究自動化技術、MS Office組件對象模型,將數據結果自動保存到文件中的技術具有現實意義。本文將結合理論知識和實踐應用兩個方面,提出一種基于自動化技術自動生成數據報告文件的解決方案。
MS Office組件是基于自動化技術的,開發人員利用這項技術可以在應用程序中調用組件方法和服務,利用現有組件功能達到自己的目的[5]。例如在應用程序中調用Excel對象插入圖表實現數據可視化。自動化技術中提供服務的部分稱服務器(也稱為自動化組件)例如Word、Excel等,調用自動化組件服務的應用程序稱為客戶端。自動化組件由通過COM接口為客戶端提供服務,每個自動化對象都會對外公開COM接口,對客戶端來說組件接口是已知的。一個COM接口包含了功能上相關的一組函數,客戶端獲得COM接口指針后,便可以調用自己所期望的函數以利用其功能。IUnknown是COM的基本接口,所有COM接口都從該接口繼承。它有QueryInterface、AddRef和Release三個方法,第一個方法用來查詢COM對象的其它接口,第二第三個方法用于對象引用計數。生存期控制和接口查詢是IUnknown接口兩個重要功能[6]。
自動化是基于IDispatch接口的COM,IDispatch接口繼承IUnknown接口。自動化技術繼承COM優點,簡化COM底層細節,還提供一組專用于自動化的數據類型等[7]。自動化對象是實現IDispatch接口的COM對象,該接口包含GetIDsOfNames、GetTypeInfo、GetTypeInfoCount和Invoke 4個方法。通過GetTypeInfoCount方法可以判斷對象是否提供類型信息,如果對象提供類型信息,客戶端調用GetTypeInfo方法就可以獲取到類型信息,如CLSID、接口ID、成員函數等。GetIDsOfNames方法的功能是根據名字返回方法或者屬性的DISPID,客戶端以DISPID調用Invoke方法從而獲得對象提供的功能。圖1所示一個自動化對象模型。

圖1 自動化對象示例圖
客戶端在調用自動化組件功能時,必須獲取自動化組件對象屬性和方法的相關信息[8]。自動化技術使用類型庫(tlb文件類型)來保存這些信息。除此之外,OCX、OlB、DLL和EXE等文件也可以保存類型信息。使用OLE Object Viewer可查看組件類型庫信息。
客戶端調用自動化組件對象有早綁定和晚綁定兩種方式。早綁定是在編譯期間就確定了調用組件對象相關信息,如方法名、參數等,通過導入類型庫來實現,是靜態綁定。晚綁定是應用程序在運行時根據對象屬性或方法名調用GetIDsOfNames方法獲取DISPID,再調用接口的Invoke方法,是動態綁定。
自動化組件能否將自身狀態變化通知給客戶端?例如當切換EXCEL工作表時,客戶端能夠感知到EXCEL組件對象的狀態變化,以便做出相應處理,如保存當前工作表數據防止丟失。在自動化技術中通過事件通知(Events)來實現。
事件通知傳出接口,它包含一組函數,每個函數對應一個事件。事件通知的接口實現是由客戶端的捕獲器來完成的。自動化技術中通過IConnectionPointContainer和IConnectionPoint接口以連接點的方式來實現事件通知和處理。第1個接口用于對連接點的管理,該接口FindConnectionPoint方法根據事件通知接口ID返回第2個接口指針,將事件通知與第2個接口關聯起來。第2個接口的Advise方法將客戶端的事件捕獲器關聯起來,這樣就形成“事件通知-連接點-捕獲器”的關聯。當組件對象狀態改變時,事件捕獲器收到事件并進行處理。圖2所示客戶端處理自動化組件事件通知的過程。
如果從頭開始開發C++自動化對象不僅效率低而且非常繁瑣[9][10]。VC++可以創建支持自動化特性的工程,該工程會維護一個idl文件,該文件記錄了工程中所有自動化對象、接口及其屬性方法等信息。利用MFC添加類型向導時,開發者可以指定“自動化”選項,從而創建自動化接口及從CCmdTarget類派生的自動化類。MFC封裝了所有自動化對象所必須的一些代碼,簡化了開發自動化對象過程。

圖2 自動化客戶端處理組件事件通知時序圖
MFC提供了COleDispatchDriver類實現對自動化對象IDispatch接口的處理。當用Class Wizard導入組件類型庫時,Wizard自動創建組件接口的COleDispatchDriver包裝類,組件接口的屬性和方法被轉換為該類的成員函數,其操作過程如下:
(1)在Class View視圖右擊,在級聯菜點擊“添加”,鼠標指向“類”。
(2)在“添加類”對話框左邊窗格中,選擇“Visual C++|MFC”選項,在右邊窗格中選擇“TypeLib中的MFC類”,點擊“打開”按鈕。
(3)在“從類型庫添加類向導”對話框選擇從“注冊表”來源添加可用的類型庫,在“可用的類型庫”列表框中選擇類型庫,“接口”窗格就會顯示該類型庫的接口,雙擊接口則會自動創建接口對應的類名稱及生成對應的頭文件。圖3使用類型庫向導為應用程序添加Excel Object Library引用。

圖3 VC++中導入類型庫圖
自動化技術中使用VARIANT數據類型傳遞數據,MFC提供了COleVariant類實現對VARIANT數據結構的封裝。
MS Office提供了一個可編程對象集合,開發者可以通過可編程對象來調用Office組件功能[11]。這將極大加速應用程序開發,我們以Word為例講解MS Office組件對象模型。
MS Office組件對象以樹狀層級結構排列,Word的任何元素,如文檔、表格、書簽等都是對象。每個對象都有一個父對象(Application除外),包含多個子對象。圖4所示,Word形成以Application對象為根節點的樹狀層級結構[12]。

圖4 Word對象模型圖
Application對象包含Documents集合對象,通過其Item屬性就可以得到單個Document對象。通過Document對象又可以得到Range、Sections、Sentences和Paragraphs等對象。每個對象都有屬性和方法,屬性是對自動化對象的某種狀態的描述,Document對象有段落、背景、保存等屬性。方法是指自動化對象提供的服務,如Document對象Undo和Redo方法執行撤銷和恢復功能。集合對象是一組同類對象的容器,通過枚舉的方法可以得到該集合中的對象,圖4背景為白色的對象都是集合對象。如Dialogs集合對象。通過Item方可以得到集合中的每個對象。
Word版本不同其對象模型有所差異,隨著Word版本的不斷提高,不斷有新的對象加入到模型中[13]。例如,在Word2003中新增了Break(s)等對象。版本變化也會帶來對象的屬性和方法的會更新,如Word 2010版中Application對象放棄了MountVolume等方法和屬性。在開發中使用OLE編程標識符(ProgID)創建自動化對象[14],Word使用“Application”作為編程標識符,PPT使用“PowerPoint.Application”作為編程標識符。
3.2.1Application對象
Application表示Word應用程序,是其它所有對象的根。在這些成員對象中可以通過get_Application方法直接得到Application對象[15]。當用戶啟動了Word應用程序時,也就創建了Application對象,創建Application對象代碼如下:
CApplication app;
app.CreateDispatch("Word.Application")));
下面代碼演示Word退出時的函數調用過程,在代碼中調用ReleaseDispatch方法釋放資源。
CComVariant save(false),origFmt,doc;
app.Quit(&save,&origFmt,&doc);
app.ReleaseDispatch();
可以使用Application對象的屬性和方法來設置和獲取Word環境信息。例如下面代碼將Word窗口可視化并設置為最大化。
app.putVisible(TRUE);
app.putWindowState(1);
3.2.2Document對象
新建Word文檔時就創建了一個Document對象,該對象被添加到Documents集合中。Document對象Avtive方法用于設置對象為活動狀態。調用對象Open和Add方法打開和創建的文檔都具有活動文檔屬性。下面代碼示意關閉文檔。
CComVariant varSave(-2),varDoc(1),varRoute(FALSE);
doc.Close(&varSave,&varDoc,&varRoute);
Documents是Docment的集合對象,調用Add方法創建一個新的Document對象并加入該集合;Open方法打開一個Word文檔并加入該集合;Item返回一個文檔對象;Close方法關閉指定文檔;Save方法保存所有的文檔。
3.2.3Selection對象
操作Word文檔主要是通過Selection對象來實現。Selection對象表示當前選擇的區域,例如設置文本顏色時被選中的文本就是一個Selection對象。Selection對象始終存在于文檔中,如果用戶沒有選擇文本則它表示插入點,表1所示該對象的主要屬性。
應用程序有且只能有一個活動的Selection對象。下面代碼示意為所選中的每個段落添加矩形邊框,
CPphs pahs = selection.get_Paragraphs();
CBorders borders = pahs.get_Borders();
borders.put_Enable(TRUE);
3.2.4Range對象
Range對象通過開始和結束字符的位置來來引用

表1 Selection對象常用屬性
文檔中某一連續區域。Word組件模型中多個對象具有Range屬性。下面代碼演示Range對象引用文檔中第四段落,設置段落格式為右對齊且選中該段落。
CPphs pahs = doc.get_Paragraphs();
CPphs pah = pahs.Item(4);
CRange range = pah.get_Range();
CPahFormat pahFormat = pah.get_Format();
pahFormat.put_Alignment(2);
range.Select();
3.2.53個屬性對象
Information、Type和Flag對象通常用于獲取和設置對象的屬性信息。Information對象返回Selection或range對象的有關信息。如頁碼、節、表格列號等。下面代碼示意如何獲得當前光標所處的行號。
CComVariant varLineNum=selection.get_Information(10);
Type對象用于返回Selection、Document、Window等對象的屬性。Type的屬性依對象不同而不相同。如Selection對象具有wdSelectionIP等屬性值。開發人員通過操作對象屬性而修改對象特征,下面代碼將一個圖片從嵌入型版式修改為浮于文字上方。
if(wdSelectionInlineShape==selection.get_Type()){
CnlineShapes inlineShapes=selection.get_InlineShapes();
CnlineShape inlineShape = inlineShapes.Item(1);
inlineShape.ConvertToShape();}
Flag屬性僅用于Selection對象,該屬性可讀寫,屬性值包括wdSelStartActive等。當向一個Word文檔輸入文本時,若希望設置編輯為插入狀態,代碼示意如下:
if (wdSelOvertype&selection.get_Flags()){
selection.put_Flags(wdSelReplace);}
我們以VC環境下開發Word(2003版)組件客戶端為例,詳細講解自動化組件的客戶端開發過程。開發情境如下:在一個油田井下測試數據分析平臺中需要將數據分析結果保存到Word中,自動生成數據分析報告。數據分析報告包括封面、目錄、正文部分(包括章節、正文、表格、圖片、正文等)、頁眉等。數據分析報告可以分為固定項目和插入項目兩個部分。固定項目是指每次創建文件時不需要更改的部分,如封面格式、目錄、頁眉、不變的文本、正文格式、表格屬性、圖片屬性、正文章節層級結構、插入圖片的位置和表格的位置等。插入部分是指平臺產生的數據、文本、圖片、表格中的數據等,要利用Word組件對象的功能將這部分內容插入到指定位置。因此需要創建一個文檔模版,設置封皮樣式、頁眉頁腳、目錄結構、插入字體格式、表格屬性和圖片屬性等。生成Word報告時,創建Word組件對象客戶端,打開文檔模版并向其中添加數據,完成后另存文檔。
(1)封皮。設置報告題目“……報告”黑體一號字加粗,“單位:……”宋體一號字,“報告人:……”宋體一號字,“完成日期:……”宋體一號字,插入分頁符。
(2)頁眉頁腳。添加頁眉“……”,在頁腳添加頁碼。
(3)目錄。目錄便于用戶快速定位該報告的內容。在報告模版中預留目錄空間,當完成報告后動態添加目錄。在目錄位置輸入“目 錄”設置宋體一號字,插入分頁符。
(4)目錄結構設置。根據報告的需要設置目錄的格式,形成遞進的層級結構。
(5)錄入固定不變的內容。將每次生成Word報告都相同的內容包括文字、圖片和表格等保存在模版中,并預留插入文本、圖片的位置。在生成Word報告時,在預留的位置中添加數據處理結果包括文本、數字和圖片等。
(6)設置表格和圖片。在模版中添加表格并設置表格屬性。這種方法不需要在VC中動態生成表格,開發人員只需要向表格內添加數據。如果表格行數不確定(大于2行),則只需要在模版中添加標題行和一個空行就可以了。在程序中根據具體情況動態添加表格行。在模版中預留插入圖片的位置并設置格式,報告中插入的圖片基本都是嵌入型版式且水平居中對齊,由程序添加圖名稱和表名稱。
(7)設置模版密碼。為增加模版安全性,為該模版設置密碼保護,防止被意外修改。另外,開發人員根據具體情況決定是否進行頁面設置等。
為提高開發速度減少重復代碼,在開發中對Word接口的包裝類進行封裝。由于在Word文檔中幾乎所有的操作都通過Selection對象來完成,所以我們將在Word中的所有操作都封裝在一個CMySelection類中,該類主要實現以下功能:①插入符操作:移動插入符至文檔開始結束位置、行首、行尾、向上下左右移動和換行。②字符串操作:查找、刪除、插入和替換。③表格操作:創建表格、添加數據、刪除表格和添加表標題。④圖片操作:插入、刪除圖片和添加圖標題;復制和粘貼。⑤創建目錄。圖5所示生成Word報告模塊的靜態類圖,COperateMSWord類中組合了CMySelection、CAppliction和CDocuments類。在COperateMSWord類中按照生成Word報告文檔內容的先后順序依次聲明方法,在這些方法中調用CMySelection類的有關方法實現對文本、數字、圖片、表格、目錄等內容的添加。

圖5 操作Word對象類圖
(1)打開和保存文檔。打開文檔代碼如下,該模版設置密碼保護,所以在Open函數的參數中包含了模版的密碼。為防止在生成報告過程中,用戶更改插入符的位置,設置文檔窗口為不可視狀態。
if (FALSE == word.CreateDispatch(_T("Word.Application"))){
return;
}
word.put_Visible(FALSE);
m_docsWord = word.get_Documents();
CString strReportTem = GetExecutePath();
strReportTem += _T("Report-template.doc");
CComVariant varFile(strReportTem),varT(TRUE),varF(FALSE),varNull(_T("")),varFmt(0),varDire(0),varPwd(_T("shm"));
m_docsWord.Open(&varFile,&varF,&varF,&varF,&varPwd,&varNull,&varF,&varPwd,&varNull,&varFmt,&varF,&varT,&varT,&varDire,&varT,&varNull);
當報告生成后調用Document對象SaveAs方法將報告保存,同時釋放Word對象資源,代碼與前面相似不再給出。
(2)插入文字。CMySelection類實現了InsertStringAfter和InsertStringBefore兩個插入字符串的方法。根據不同情況調用相應的方法在當前選定的字符串之前或者之后插入字符串。這兩個函數都有兩個參數,一個參數是用于確定插入字符串位置的待查找字符串,另一個參數是待插入的字符串。在這兩個方法中,首先執行查找操作,將光標定位到插入字符的位置,然后調用Selection對象的InsertAfter或者InsertBefore方法插入字符串。CMySelection類的FindString函數中封裝了Word的查找和替換操作,在Word對象模型中Find對象實現查找和替換功能。在FindString函數中首先調用Selection對象的get_Find方法得到Find對象,然后清除格式信息,最后調用Find對象的Execute方法執行查找操作。
CFind findWord = m_selWord.get_Find();
findWord.ClearFormatting();
CComVariant varFindText(strFind),varF(FALSE),varT(TRUE),varWrap(1),varNull;
findWord.Execute(&varFindText,&varT,&varT,&varF,&varF,&varF,&varT,&varWrap,&varF,&varF,&varNull,&varF, &varF, &varF, &varF);
用于確定插入字符串位置的字符串,在報告模版中必須具有唯一性。替代這種字符串查找的另外一種方式是使用書簽。由于書簽內容在編輯模版時不能清晰顯示出來,不利于模版文檔的維護,故在開發中未采用這種方法。Find對象實現查找和替換功能,通過Find對象可以得到Replacement對象,該對象是執行替換操作時的替換條件,為該對象賦值后(替換的字符串),該調用Find對象Excute方法執行替換操作。
(3)添加圖片。CMySelection類InsertPicture方法實現向文檔中插入圖片功能。在模版中預留了插入圖片的位置,在圖片位置的正下方是該圖片的名稱。該函數首先調用FindString方法找到圖片名稱,然后調用MoveUp方法將插入符向上移動到插入圖片的位置,然后調用AddPicture方法插入圖片,最后為圖片添加題注。報告文檔中的圖片都是嵌入型的,在Word對象模型中InlineShape對象表示嵌入型圖片。在AddPicture方法中通過Selection對象得到InlineShapes集合對象,然后調用該對象AddPicture方法將圖片添加到文檔中。
CnlineShapes inlineShapes=m_selWord.get_InlineShapes();
CComVariant varLinkToFile(false),varSave (true);
CRange range = m_selWord.get_Range();
inlineShapes.AddPicture(szFilePath,&varLinkToFile, &varSave, &CComVariant(range));
使用題注的方法為圖片和表格命名是很有實用價值的,在CMySelection類中也封裝了插入題注的功能。在該類中首先添加圖標簽,然后調用Selection對象的InsertCaption方法為插入的圖添加題注。
CCaptionLabels captionLabels = appWord.get_CaptionLabels();
captionLabels.Add(_T("圖"));
CComVariant varLab(T("圖")),varTle(_T("")),varTleAuTxt(""),varPos(wdCaptionPositionBelow),varEx(false);
selection.InsertCaption(&varLab,&varTle,&varTleAuTxt,&varPos,&varEx);
(4)添加表格數據。在報告模版中已經插入了表格和表名稱(在表格的正上方),所以首先調用FindString函數找到表名稱并為該表加入題注,然后將光標下移到需要插入數據的位置,插入數據。這時需要判斷當前的插入符是否在表格內,必要時還需要判斷插入符所在的行和列。通過Selection對象的Information屬性實現此項功能。
CComVariant varBInTle(true);
i(varBInTl==election.get_Information(12)){
CComVariant varCol=selection.get_Information(16);
CComVariant varRow=selection.get_Information(13);}
如果插入到表格中數據數量不確定,則該表格行數就不確定。這就要求程序根據數據長度動態添加表格行。在程序中使用MFC的CStringList類保存插入的數據,該類是一個由字符串構成的鏈表。當插入符移動到表格右下角的單元格時,按下Tab鍵會在最后一行下面增加一空行。下面代碼為表格增加一空行。
CComVariant
varUnit(wdCell),varCut(1),varN;
selection.MoveRight(&varUnit,&varCut,&varN);
使用這種方法插入表格行的優點是,當插入空行后,插入符會自動移動到該行的第一個單元格,這樣就可以繼續插入數據。
(5)創建目錄。創建目錄的方法比較簡單,Word中TablesOfContents對象的Add方法實現該功能。該函數參數比較多包括Appliction、Document、Range等對象。創建目錄時選擇使用點填充目錄項目與頁碼之間空白,并設置目錄內容按照級別遞增索引,代碼如下所示。
CApplication app = word.get_Application();
CDoc actDoc=app.get_ActiveDocument();
CTablesOfContents tabsOfCtets = actDoc.get_TablesOfContents();
CRange range = m_selWord.get_Range();
CComVariant varF(false),varT(true),varUp(1),varLow(3),varNull;
CTableOfContents table= tabsOfCtets.Add(range,&varT,&varUp,&varLow,&varF,&varNul,&varT,&varT,&varNul,&varT,&varT,&varT);
table.put_TabLeader(1);
tabsOfCtets.put_Format(0);
由于Word報告中存在格式相同的內容,例如表格1、表格2……,這些表格的格式都一樣,標題行也一樣,只是填充的內容不同。通過調用selection對象Copy和Paste/PasteAndFormat方法執行復制和粘貼操作。這樣解決了在報告中格式相同而內容不同的問題。由于創建的Word報告內容比較多時間長,為了告知用戶當前創建報告的進度,我們使用一個進度條顯示當前生成報告的狀態。
本文以Microsoft Office組件為開發對象,提出一種基于自動化技術生成數據報告文件的解決方案,詳細講解了自動化技術知識,MFC對自動化技術的支持,Microsoft Office組件對象模型,最后以一個項目詳細說明了VC環境下開發自動化組件客戶端的過程,希望本文能夠給人們提供一個很好的幫助。
[1] 金宇鋒,張 健.基于Excel VBA的漁具選擇性分析SELECT模型實現[J].實驗室研究與探索,2014(3):154-158.
[2] 湯 躍,趙 坤,許文博.基于Excel的水泵性能試驗數據處理的VBA開發[J].排灌機械工程學報,2011(2):123-126.
[3] 王 磊,張仁瓊.利用VBA技術和EndNote軟件建立查新報告數據庫[J].現代情報,2015(8):131-136,140.
[4] 朱 敏,沈同圣,王學偉.VC++與VBA結合實現復雜報表[J]. 計算機應用與軟件,2005(2):42-43+101.
[5] 葉 明,張 諍.基于C#.NET的Word報告生成功能開發[J].計算機工程與應用,2008(9):104-106.
[6] 潘愛民.COM原理與應用[M].北京:清華大學出版社,1999:334.
[7] 朱 敏,沈同圣.VC++與VBA結合實現復雜報表[J].計算機應用與軟件,2005(2):42.
[8] 湯克明,陳 崚.Word自動閱卷系統的設計與實現[J]. 計算機工程與應用,2008(35):69-72.
[9] 孔令彥,董蓬勃,姜青香.使用Visual Basic操縱Microsoft Word對象生成報表文檔[J].計算機工程與應用,2003(36):115-117.
[10] 趙宏亮,楊鶴標.面向領域的語義搜索引擎的應用研究[J].計算機工程與設計,2012,(05):1801-1805.
[11] 冉 沛,楊吉云,譚金勇.一種新的Word電子文檔完整性保護方案[J].計算機工程與應用,2013(13):76-79+148.
[12] MSDN.Office development[EB/OL].https://msdn.microsoft.com/en-us/library/fp161347.aspx, 2016-12-1.
[13] 朱 敏,方登建,王 哲. Word模板數據自校驗設計與信息提取技術[J]. 實驗室研究與探索,2012(3):75-78.
[14] 楊德明,郭 盛. 基于Word文檔的數據隱藏方法[J].計算機應用與軟件,2015(5):314-318.
[15] 高麗萍,郭棟彬,鄭博文. Co-Word中圖文混排的文檔一致性研究[J].計算機應用研究,2017(11):1-10.