不想再手動複製貼上了?用 VBA 一鍵批次生成 Word 公文,省下 80% 重複工作

內容目錄

前言:你是不是也在做這種事?

每個月月底,辦公桌上都會疊著一大堆合約、通知書、或公文。

每一份都長得差不多,就差在「客戶名字」、「金額」、「日期」這幾個欄位不同。

你打開 Word,複製上個月的版本,手動改名字,改日期,存檔,再開下一份……

**一個下午就這樣沒了。**

這篇文章要教你一個方法:把 Excel 當資料庫,Word 當模板,用 VBA 寫一段代碼,**按一個按鈕,幾秒內自動生成幾十份公文**,還會自動標記哪些已完成、哪些需要跳過。

Excel 轉 Word 自動化範例懶人包

為了讓練完手的讀者更容易有機會上手實用,我提供了合約的懶人包給各位下載,把模版的內容改一改就可以用了。如果5秒沒有點擊下載需要在另外一個頁面再點擊直接開啟文件。

不想再手動複製貼上了?用 VBA 一鍵批次生成 Word 公文,省下 80% 重複工作

打開之後你會看到一個ZIP檔,點擊下載,解壓之後是兩個檔Data和Template,我有試過不用壓縮,分成兩個檔,也沒有比較方便,另外,因為它是巨集關係,我直接把VBA的code寫到裡面去了,Google對它的防範還是比較高一點。(因為有讀者反映看不到rar檔,圖片我就不再換,zip檔就是新的了。)

不想再手動複製貼上了?用 VBA 一鍵批次生成 Word 公文,省下 80% 重複工作

第一次使用點擊啟用編輯,

不想再手動複製貼上了?用 VBA 一鍵批次生成 Word 公文,省下 80% 重複工作

然後會有安全性風險,關掉data,到資料夾選取data右鍵按內容,勾選解除封鎖按套用。

為了方便演示才全部都按出來,其實一開始解壓縮之後就可以先解除封鎖,不過如果在別的地方下載巨集.xlsm,最後要先到開發人員的VISUAL BASIC看一看裡面的代碼,會不會挾帶別的惡意程式,它可以拿到很大的權限,真的要非常非常小心!所以,你很少會看到有網站把整個巨集丟出來下載。

最後,再點擊啟用內容就可以用了,其它使用上的問題可以看使用前注意事項,類似說明書之類的。

自動化範例的簡易說明書

解壓縮之後有兩個檔案,一個叫data的Excel檔,另一個叫Template的word檔,要放在相同路徑,第一次執行Excel後會在相同路徑產生一個資料夾叫GeneratedContracts,裡面會用來裝著我們用data產生的合約,有些電腦在第一次產生資料夾時會產生word檔,有些版本會產生錯誤,把data頁面F欄的completed清除,再次執行就可以了。

要怎樣執行呢?原本我已經制作了按鈕,但有些電腦下載時會把按鈕清除,

你可以在開發人員–>插入表單控制項按鈕(最左上面角落那個),然後點選要執行的巨集–>工作表1.GenerateWordContracts–>確定

VBA代碼的流程邏輯

整個流程分成四個階段:環境設定 → 啟動 Word → 迴圈處理 → 清理環境。

第一節:環境設定 ── 讀懂第一段代碼

vba
Sub GenerateWordContracts()
    Dim wdApp As Object
    Dim wdDoc As Object
    Dim templatePath As String
    Dim outputFolder As String
    Dim dataSheet As Worksheet
    Dim lastRow As Long
    Dim i As Long
    Dim rowData As Variant
    Dim successCount As Long
    Dim skipCount As Long
    
    On Error GoTo ErrorHandler
    
    templatePath = ThisWorkbook.Path & "\Template.docx"
    outputFolder = ThisWorkbook.Path & "\GeneratedContracts\"
    
    If Dir(templatePath) = "" Then
        MsgBox "錯誤:找不到 Template.docx", vbCritical
        Exit Sub
    End If
    
    If Dir(outputFolder, vbDirectory) = "" Then MkDir outputFolder
    
    Set dataSheet = ThisWorkbook.Sheets("Data")
    lastRow = dataSheet.Cells(dataSheet.Rows.Count, "A").End(xlUp).Row
    
    If lastRow < 2 Then
        MsgBox "Data 工作表沒有資料!", vbExclamation
        Exit Sub
    End If
Dim wdApp As Object / Dim wdDoc As Object

這兩個變數代表「Word 應用程式本身」和「單一 Word 文件」。宣告為 `Object` 是因為這段 VBA 是在 Excel 裡執行的,Excel 不認識 Word 的型別,所以用泛型 `Object` 來承接。這種技術叫做「Late Binding(晚期綁定)」,好處是不需要在 Excel 裡預先勾選 Word 的參考程式庫。

On Error GoTo ErrorHandler

這是整段程式的「安全網」。萬一在執行過程中發生錯誤(例如 Word 當機、磁碟空間不足),程式不會直接崩潰,而是跳到最底部的 `ErrorHandler` 區塊,乾淨地關閉 Word、顯示錯誤訊息。

templatePath = ThisWorkbook.Path & "\Template.docx"

ThisWorkbook.Path 會自動取得「這個 Excel 檔案所在的資料夾路徑」。這樣設計的好處是:不管你把整個資料夾搬到哪台電腦,路徑永遠是正確的,不需要寫死像 C:\Users\Charlie\Desktop\...這種會失效的絕對路徑。

If Dir(templatePath) = "" Then

`Dir()` 函式就像去摸一個路徑「有沒有東西」。如果回傳空字串 `""`,代表那個檔案不存在,程式就會跳出並提示錯誤,而不是繼續執行、最後出現一個讓人看不懂的錯誤。

**`If Dir(outputFolder, vbDirectory) = "" Then MkDir outputFolder`**

這行做了兩件事:先檢查輸出資料夾存不存在,如果不存在就自動建立。`vbDirectory` 是在告訴 `Dir()` 「我要找的是資料夾,不是檔案」。

**`lastRow = dataSheet.Cells(dataSheet.Rows.Count, "A").End(xlUp).Row`**

這是 VBA 界最著名的「找最後一行」技巧。它的邏輯是:先從 A 欄的最底部(第 1048576 行)往上找,找到第一個有資料的儲存格,那一行的行號就是 `lastRow`。比直接用 `dataSheet.UsedRange.Rows.Count` 更可靠,因為後者容易把空白的「曾用過」範圍也算進去。

## 第二節:啟動 Word,開始批次處理

    Set wdApp = CreateObject("Word.Application")
    wdApp.Visible = False
    
    successCount = 0
    skipCount = 0
    
    For i = 2 To lastRow
        If UCase(dataSheet.Cells(i, 6).Value) = "COMPLETED" Then
            skipCount = skipCount + 1
            GoTo NextRow
        End If
        
        rowData = dataSheet.Range("A" & i & ":E" & i).Value
        
        Set wdDoc = wdApp.Documents.Add(templatePath)
        
        Call ReplacePlaceholder(wdDoc, "{{ClientName}}", rowData(1, 1))
        Call ReplacePlaceholder(wdDoc, "{{ContractDate}}", Format(rowData(1, 2), "yyyy年mm月dd日"))
        Call ReplacePlaceholder(wdDoc, "{{Amount}}", Format(rowData(1, 3), "#,##0"))
        Call ReplacePlaceholder(wdDoc, "{{CompanyName}}", rowData(1, 4))
        Call ReplacePlaceholder(wdDoc, "{{Terms}}", rowData(1, 5))
        
        Dim fileName As String
        fileName = outputFolder & "合約_" & rowData(1, 1) & "_" & Format(Now, "hhmmss") & ".docx"
        
        wdDoc.SaveAs2 fileName, 16
        wdDoc.Close False
        
        With dataSheet.Cells(i, 6)
            .Value = "Completed"
            .Font.Color = RGB(0, 128, 0)
            .Font.Bold = True
        End With
        
        successCount = successCount + 1
        
NextRow:
    Next i
```

 逐行解析

**`Set wdApp = CreateObject("Word.Application")`**

這行代碼用 Windows 的 COM 技術,在背景靜默啟動一個 Word 應用程式實例。`CreateObject("Word.Application")` 是固定語法,引號裡的字串是 Word 在 Windows 登錄檔裡的「程式 ID(ProgID)」。

**`wdApp.Visible = False`**

讓 Word 在背景執行,使用者看不到視窗在跳動。如果你想看到 Word 的處理過程(例如debug時),可以改成 `True`。

**`If UCase(dataSheet.Cells(i, 6).Value) = "COMPLETED" Then`**

這是「智慧跳過」機制的關鍵。`UCase()` 把 F 欄的值轉成大寫再比較,所以不管之前存的是 `Completed`、`completed` 還是 `COMPLETED`,都能正確識別。搭配後面寫回 Excel 的 `"Completed"`,形成了一個可以多次執行的「冪等(idempotent)」操作 ── 重複跑程式不會重複生成已完成的文件。

**`rowData = dataSheet.Range("A" & i & ":E" & i).Value`**

一次把 A 到 E 欄的值讀入一個二維陣列 `rowData`。這樣比一格一格讀取快很多,尤其是資料列數多的時候。存取方式是 `rowData(1, 1)` 代表第一列第一欄(A欄),`rowData(1, 2)` 是第二欄(B欄),以此類推。

**`Set wdDoc = wdApp.Documents.Add(templatePath)`**

注意是 `.Add(templatePath)` 而不是 `.Open(templatePath)`。`.Open` 會直接打開並修改原始模板;`.Add` 則是「以這個模板為基礎,開一份新文件」,原始模板永遠不會被改動。

**`Format(rowData(1, 2), "yyyy年mm月dd日")`**

Excel 儲存日期的方式是一個序列數字(例如 46132 代表某個日期),`Format()` 函式把它轉成可讀的中文日期格式。同樣地,`Format(rowData(1, 3), "#,##0")` 把數字格式化成千分位(例如 85000 → 85,000)。

**`wdDoc.SaveAs2 fileName, 16`**

`SaveAs2` 是 Word 2010 之後的方法,比舊的 `SaveAs` 更穩定地支援 `.docx` 格式。第二個參數 `16` 是一個常數,代表 `wdFormatXMLDocument`,也就是現代的 `.docx` 格式。

**`With dataSheet.Cells(i, 6) ... End With`**

成功生成後,程式回頭在 Excel 的 F 欄標記 `"Completed"`,字體設為深綠色加粗。下次執行程式時,這一列就會被跳過。這個設計讓程式可以「中斷後繼續跑」,不怕跑到一半電腦當機。

---

第三節:代碼的核心 ── ReplacePlaceholder 替換函式詳解

Private Sub ReplacePlaceholder(wdDoc As Object, findText As String, replaceValue As Variant)
    Dim txt As String
    txt = IIf(IsNull(replaceValue) Or IsEmpty(replaceValue), "", CStr(replaceValue))
    
    With wdDoc.Content.Find
        .ClearFormatting
        .Replacement.ClearFormatting
        .Text = findText
        .Replacement.Text = txt
        .Forward = True
        .Wrap = 1
        .Format = False
        .MatchCase = False
        .MatchWholeWord = False
        .MatchByte = True
        .MatchWildcards = False
        .MatchSoundsLike = False
        .MatchAllWordForms = False
        .Execute Replace:=2
    End With
End Sub
```

這個函式是整個系統的核心引擎。每次呼叫它,就是在 Word 文件裡執行一次「全部取代」操作,把像 `{{ClientName}}` 這樣的佔位符,換成真實的資料。

### 參數設計

函式接收三個參數:
- `wdDoc`:要操作的 Word 文件物件
- `findText`:要搜尋的佔位符,例如 `"{{ClientName}}"`
- `replaceValue`:要替換進去的實際值,型態是 `Variant` 以應對各種資料類型(文字、數字、日期)

### 防禦性的 Null 處理

```vba
txt = IIf(IsNull(replaceValue) Or IsEmpty(replaceValue), "", CStr(replaceValue))
```

`IIf()` 是 VBA 的三元運算子。這行說:「如果 `replaceValue` 是 Null 或空值,就用空字串替換;否則把它轉成字串」。這很重要,因為 Excel 儲存格可能是空的,如果直接用 `CStr()` 轉換一個空值,程式會報錯。

### Find 物件的各個屬性解析

**`.ClearFormatting` / `.Replacement.ClearFormatting`**

清除搜尋條件和替換條件上可能殘留的格式設定,確保每次搜尋都是乾淨的狀態。

**`.Forward = True`**

從文件開頭往結尾方向搜尋。

**`.Wrap = 1`(wdFindContinue)**

搜尋到文件結尾時,自動繞回開頭繼續找。這個設定確保不論游標在哪個位置,都能找到所有的佔位符。

**`.Format = False`**

告訴 Word「我只要找純文字,不要考慮格式」。如果設成 `True`,就必須連字型、大小都一樣才算符合。

**`.MatchCase = False`**

不區分大小寫。`{{ClientName}}` 和 `{{clientname}}` 都算符合。

**`.MatchByte = True`**

這個屬性在處理中文時非常重要。它區分全形和半形字元,避免把全形的 `{{` 誤判為半形的 `{{`。在公文系統裡,這個設定能避免一些很難debug的詭異替換問題。

**`.Execute Replace:=2`(wdReplaceAll)**

`Execute` 是真正觸發搜尋替換的方法。`Replace:=2` 代表「全部替換」,等同於 Word 裡按「全部取代」按鈕。如果改成 `Replace:=1`(wdReplaceOne),就只替換第一個找到的結果。

---

## 第四節:收尾 ── 清理環境與完成報告

```vba
    wdApp.Quit
    MsgBox "處理完成!" & vbCrLf & _
           "---------------------------" & vbCrLf & _
           "   新生成合約:" & successCount & " 份" & vbCrLf & _
           "   已跳過(已完成):" & skipCount & " 份" & vbCrLf & _
           "   位置:" & outputFolder, vbInformation
    Exit Sub

ErrorHandler:
    MsgBox "發生錯誤:" & Err.Description, vbCritical
    If Not wdDoc Is Nothing Then wdDoc.Close False
    If Not wdApp Is Nothing Then wdApp.Quit
End Sub
```

### 逐行解析

**`wdApp.Quit`**

關閉在背景執行的 Word。如果忘記這行,每次執行程式都會多開一個隱藏的 Word 實例在記憶體裡,跑幾次之後記憶體就撐不住了。

**`MsgBox ... vbCrLf & _`**

`vbCrLf` 是換行符號。`& _` 是 VBA 的連接字元,讓一行太長的代碼可以分多行書寫,增加可讀性。最終使用者會看到一個整齊的結果報告,清楚知道「生成了幾份、跳過了幾份、存在哪裡」。

**`Exit Sub`**

在正常執行完畢後,主動跳出程式,避免繼續執行到下面的 `ErrorHandler` 區塊。

**`ErrorHandler`**

```vba
If Not wdDoc Is Nothing Then wdDoc.Close False
If Not wdApp Is Nothing Then wdApp.Quit
```

發生錯誤時,先確認物件是否存在(`Not ... Is Nothing`),再關閉文件和 Word。這樣的雙重保險確保即使在錯誤狀態下,Word 也不會殘留在背景佔用資源。

---



第五節:如何擴充這套系統?

### 新增更多欄位

假設你要新增一個 `{{ManagerName}}` 標籤:

1. 在 Excel 的 Data 工作表 F 欄加入 `ManagerName` 的資料(注意原本 F 欄是狀態欄,可能要往後移)
2. 在 Word 模板 `Template.docx` 裡加入 `{{ManagerName}}`
3. 在代碼裡修改讀取範圍 `"A" & i & ":E" & i` → `"A" & i & ":F" & i`
4. 新增一行:`Call ReplacePlaceholder(wdDoc, "{{ManagerName}}", rowData(1, 6))`

### 用 PDF 格式輸出

把這行:
```vba
wdDoc.SaveAs2 fileName, 16
```
改成:
```vba
Dim pdfName As String
pdfName = outputFolder & "合約_" & rowData(1, 1) & "_" & Format(Now, "hhmmss") & ".pdf"
wdDoc.SaveAs2 pdfName, 17  ' 17 = wdFormatPDF
```
---

常見問題FAQ )

**Q:執行時出現「找不到 Template.docx」?**

確認 `Template.docx` 和 `Data.xlsm` 放在同一個資料夾裡,且 Excel 檔案是已儲存的狀態(不能是「未儲存的新文件」,否則 `ThisWorkbook.Path` 會回傳空字串)。

**Q:生成的文件裡 `{{ClientName}}` 沒有被替換?**

最常見的原因是 Word 模板裡的 `{{ClientName}}` 被 Word 的「自動校正」功能拆開了(例如 `{{Client` 和 `Name}}` 變成兩個獨立的文字段落)。解決方式:在 Word 裡手動刪除佔位符,重新打一次,或是關閉 Word 的「自動校正」功能。

**Q:可以在 Mac 上執行嗎?**

不行。這套代碼使用了 Windows 專屬的 COM 自動化(`CreateObject`)。Mac 版的 Office 不支援 COM,需要改用 AppleScript 或其他方式實現。

結語

這套 VBA 自動化系統的設計有幾個值得學習的亮點:

– **防禦性程式設計**:每一個可能出錯的地方都有保護措施(檔案不存在、資料為空、執行錯誤)
– **冪等性**:重複執行不會重複產生結果,透過 F 欄狀態欄實現
– **模組化**:`ReplacePlaceholder` 獨立成一個函式,清晰且可重用
– **Late Binding**:不依賴特定版本的 Word 參考程式庫,相容性更好

把這個思維用在你自己的公文需求上,根據實際的 Word 模板和欄位調整,你也可以打造一套屬於自己部門的「公文自動化流水線」。



*如果這篇文章對你有幫助,歡迎分享給同樣在做重複公文工作的朋友!*
如果仍有無法執行的問題,歡迎與我聯絡。

如果你想補充更多vba對word的基礎操作,可幾參考《自學Excel VBA系列-如何用VBA控制WORD?

或者你還需要學習vba對outlook的操作,可以參考《自學Excel VBA系列-如何用VBA控制OutLook?》這篇。

如果你學習VBA的終極目的是要減輕辦公室工作的壓力,請參考《Excel VBA 自動化教學完整指南:從入門到辦公室自動化應用》,又如果你想分享你的自動化需求,尋求意見,可以透過主頁的「與我聯絡」聯絡我,我很樂意為你解答。

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments