很多做办公自动化的朋友,最后都会走到 Word 文档生成这一步:合同、报价单、通知书、台账、证明、报告,都希望脚本能批量生成。
用 Word COM 可以做,但它有几个老问题:需要安装 Word,后台批量跑不够安静,机器环境一变就容易出小毛病。于是我整理了 DocxTplAHK,一个给 AHK v1 用的轻量 docx 模板渲染和后台编辑库。
它不启动 Word,而是把 .docx 当成 OOXML zip 包来处理:解压、修改 XML、重新打包。这个思路更适合固定模板的批量生成。
它适合什么场景
如果你有一份 Word/WPS 做好的模板,里面固定位置要填客户名、日期、金额、明细表、图片、页眉页脚,那么它就很适合。
我更推荐把它用在固定模板文档上,比如报价单、合同、通知、验收单、批量证明。复杂排版仍然建议先在 Word/WPS 里做好模板,脚本只负责填数据。
最小模板渲染
#Requires AutoHotkey v1.1
#Include <DocxTplAHK>
items := []
items.Push({"name": "基础服务", "price": "199.00"})
items.Push({"name": "文档生成", "price": "299.00"})
context := {"customer": "ahk66 用户", "paid": true, "items": items}
Docx_RenderTemplate("template.docx", "output.docx", context)
模板里可以放类似 {{customer}}、{% if paid %}、{% for item in items %} 这样的占位符。对初学者来说,先会变量替换和循环明细表,就能覆盖不少办公场景。
会话模式更适合批量编辑
如果同一份文档要连续做很多操作,不建议每一步都解压打包一次。会话模式会先打开一次,连续编辑,最后统一保存。
doc := Docx_Open("template.docx")
Docx_SessionRenderTemplate(doc, context)
Docx_SessionAppendParagraph(doc, "备注:后台生成,不启动 Word。")
Docx_SessionInsertTableAtPlaceholder(doc, "items_table", rows)
Docx_Close(doc, "output.docx")
资源包里的 demo 就是按这个思路写的,会生成多份文档,用来演示模板渲染、表格、图片、页眉页脚、分节、书签、内容控件、批注、脚注、目录等能力。
我认为最实用的能力
第一是模板变量、循环和条件。这个直接对应合同、报价单、通知书这类文档。
第二是表格和图片。很多办公文档不是纯文字,明细表和截图经常要插进去。
第三是书签和内容控件定位。模板复杂后,单纯文本替换不够稳,提前在 Word 里放书签或内容控件,会更适合长期维护。
依赖和边界
它不要求安装 Word,但推荐安装 7-Zip,或者把 7z.exe / 7za.exe 放到库目录、脚本目录、bin 或 tools 子目录。找不到 7-Zip 时,会回退到系统方式处理 zip,但批量速度通常慢一些。
它不是完整 Word 引擎,不负责复杂分页、完整样式系统、字段自动刷新。目录、页码这类域结果,通常需要 Word/WPS 打开后更新字段。
如果你的需求是“后台批量生成固定格式 docx”,这个库会比直接操控 Word COM 安静很多;如果你的需求是深度排版和实时所见即所得,那还是 Word COM 或人工模板更合适。
demo效果 .docx 截图展示

demo 里有什么
- 这个示例演示 DocxTplAHK 的常用后台 Word 流程:
1. 使用 {{变量}}、{% if %}、{% for %} 渲染模板
2. 在已有 docx 后追加段落、表格、图片、标题、编号列表和项目符号
3. 设置页眉、页脚、页码,并插入分节符
4. 按块占位符插入表格/图片,编辑指定表格单元格
5. 替换跨 run 文本时尽量保留原有 run 样式
6. 按书签、内容控件定位替换内容
7. 替换已有图片关系资源,不依赖 Word COM
8. 编辑表格底纹、边框、对齐、合并单元格、重复表头
9. 按占位符插入超链接、脚注、尾注、批注、目录、修订痕迹
DocxTplAHK.ahk【文章底部有完整示例】
demo代码片段展示:
#NoEnv
SetBatchLines -1
#SingleInstance Force
#Include <DocxTplAHK>
template := A_ScriptDir . "\sample_template.docx"
output := A_ScriptDir . "\主流程_模板渲染_追加内容_页眉页脚_分节_替换图片.docx"
samplePng := A_ScriptDir . "\sample_image.png"
samplePng2 := A_ScriptDir . "\sample_image_replace.png"
placeholderBase := A_ScriptDir . "\sample_placeholder_base.docx"
tableEditedDoc := A_ScriptDir . "\占位符_表格图片_单元格编辑.docx"
styleTemplate := A_ScriptDir . "\sample_style_template.docx"
styleDoc := A_ScriptDir . "\跨Run样式保真替换.docx"
bookmarkTemplate := A_ScriptDir . "\sample_bookmark_template.docx"
bookmarkDoc := A_ScriptDir . "\书签定位替换.docx"
sdtTemplate := A_ScriptDir . "\sample_sdt_template.docx"
sdtDoc := A_ScriptDir . "\内容控件定位替换.docx"
tableAdvancedDoc := A_ScriptDir . "\表格高级编辑_底纹_边框_对齐_合并_重复表头.docx"
locationBase := A_ScriptDir . "\sample_location_base.docx"
locationOutputDoc := A_ScriptDir . "\定位编辑_超链接_脚注_尾注_批注_目录_修订.docx"
CleanupDemoFiles()
; 实际项目里可以把 template 换成你自己用 Word/WPS 做好的模板。
templateText := "客户:{{customer}}`n状态:{% if paid %}已付款{% else %}待付款{% endif %}`n明细:{% for item in items %}{{item.name}}={{item.price}} 元;{% endfor %}"
if (!Docx_CreateSimpleTemplate(template, templateText))
DemoFail("模板创建失败")
items := []
items.Push({"name": "基础服务", "price": "199.00"})
items.Push({"name": "文档生成", "price": "299.00"})
context := {"customer": "新用户", "paid": true, "items": items}
; 会话模式:只打开/解压一次,在同一个 doc 对象上连续编辑,最后保存/打包一次。
doc := Docx_Open(template)
if (!IsObject(doc))
DemoFail("打开主流程模板失败")
if (!Docx_SessionRenderTemplate(doc, context))
DemoFail("模板渲染失败")
if (!Docx_SessionAppendParagraph(doc, "备注:此文档由 DocxTplAHK 后台生成,没有启动 Word。"))
DemoFail("追加段落失败")
rows := []
rows.Push(["项目", "金额"])
rows.Push(["基础服务", "199.00"])
rows.Push(["文档生成", "299.00"])
if (!Docx_SessionAppendTable(doc, rows))
DemoFail("追加表格失败")
if (!Docx_CreateSamplePng(samplePng))
DemoFail("创建示例图片失败")
if (!Docx_CreateSamplePng(samplePng2))
DemoFail("创建替换图片失败")
if (!Docx_SessionAppendImage(doc, samplePng, 48, 48, "示例图片"))
DemoFail("追加图片失败")
if (!Docx_SessionAppendHeading(doc, "一、执行摘要", 1))
DemoFail("追加标题失败")
numberItems := []
numberItems.Push("收集模板数据")
numberItems.Push("渲染正文")
numberItems.Push("生成最终 docx")
if (!Docx_SessionAppendList(doc, numberItems, "number", 0))
DemoFail("追加编号列表失败")
bulletItems := []
bulletItems.Push("无需 Word COM")
bulletItems.Push("适合后台批处理")
bulletItems.Push("可继续扩展 OOXML 能力")
if (!Docx_SessionAppendList(doc, bulletItems, "bullet", 1))
DemoFail("追加项目符号失败")
if (!Docx_SessionSetHeaderFooter(doc, "DocxTplAHK 示例页眉", "DocxTplAHK 示例页脚", true, true))
DemoFail("设置页眉页脚失败")
if (!Docx_SessionAppendSectionBreak(doc, "nextPage"))
DemoFail("插入分节符失败")
if (!Docx_SessionAppendParagraph(doc, "第二节:这是分节符后的正文。"))
DemoFail("追加分节后正文失败")
; 替换已有图片资源。这里 rId1 是前面 AppendImage 插入的第一张图片关系。
if (!Docx_SessionReplaceMedia(doc, "rId1", samplePng2))
DemoFail("替换已有图片资源失败")
if (!Docx_Close(doc, output))
DemoFail("保存主流程文档失败")
; 按占位符插入表格/图片,并编辑指定表格单元格。
baseText := "前言`nitems_table`nlogo_image`n结束"
if (!Docx_CreateSimpleTemplate(placeholderBase, baseText))
DemoFail("占位符模板创建失败")
doc := Docx_Open(placeholderBase)
if (!IsObject(doc))
DemoFail("打开占位符模板失败")
if (!Docx_SessionInsertTableAtPlaceholder(doc, "items_table", rows))
DemoFail("按占位符插入表格失败")
if (!Docx_SessionInsertImageAtPlaceholder(doc, "logo_image", samplePng, 40, 40, "插入图片"))
DemoFail("按占位符插入图片失败")
if (!Docx_SessionSetTableCell(doc, 1, 2, 2, "399.00"))
DemoFail("编辑指定表格单元格失败")
if (!Docx_Close(doc, tableEditedDoc))
DemoFail("保存占位符表格/图片文档失败")
; 更完整的表格编辑:重复表头、底纹、边框、水平/垂直对齐、合并单元格。
doc := Docx_Open(tableEditedDoc)
if (!IsObject(doc))
DemoFail("打开表格编辑文档失败")
if (!Docx_SessionSetTableHeaderRepeat(doc, 1, 1, true))
DemoFail("设置重复表头失败")
if (!Docx_SessionSetTableCellShading(doc, 1, 1, 1, "DDEEFF"))
DemoFail("设置单元格底纹失败")
if (!Docx_SessionSetTableCellBorder(doc, 1, 1, 1, "all", "double", 8, "1F4E79"))
DemoFail("设置单元格边框失败")
if (!Docx_SessionSetTableCellAlignment(doc, 1, 1, 1, "center", "center"))
DemoFail("设置单元格对齐失败")
if (!Docx_SessionMergeTableCells(doc, 1, 2, 1, 2, 2))
DemoFail("合并单元格失败")
if (!Docx_Close(doc, tableAdvancedDoc))
DemoFail("保存高级表格编辑文档失败")
; 跨 run 替换:Word 常会把同一段文字拆成多个 run,这里演示替换后保留第一个 run 的加粗样式。
if (!CreateStyledReplaceTemplate(styleTemplate))
DemoFail("样式保真模板创建失败")
if (!Docx_ReplaceText(styleTemplate, styleDoc, "跨片段文本", "保留样式替换"))
DemoFail("跨 run 样式保真替换失败")
; 书签定位替换:适合在模板中预先用 Word/WPS 放好书签。
if (!CreateBookmarkDemoDocx(bookmarkTemplate))
DemoFail("书签模板创建失败")
if (!Docx_ReplaceBookmarkContent(bookmarkTemplate, bookmarkDoc, "CustomerBookmark", Docx_BuildRunXml("书签替换后的客户名称")))
DemoFail("书签内容替换失败")
; 内容控件定位替换:适合用 tag/alias 做稳定标记。
if (!CreateSdtDemoDocx(sdtTemplate))
DemoFail("内容控件模板创建失败")
if (!Docx_ReplaceContentControlContent(sdtTemplate, sdtDoc, "CustomerName", Docx_BuildParagraphXml("内容控件替换后的客户名称")))
DemoFail("内容控件替换失败")
; 更强的定位编辑:这些示例都按占位符插入,适合后台批量生成。
locationText := "link_here`nfootnote_here`nendnote_here`ncomment_here`ntoc_here`nrevision_here"
if (!Docx_CreateSimpleTemplate(locationBase, locationText))
DemoFail("定位编辑模板创建失败")
doc := Docx_Open(locationBase)
if (!IsObject(doc))
DemoFail("打开定位编辑模板失败")
if (!Docx_SessionInsertHyperlinkAtPlaceholder(doc, "link_here", "打开官网", "https://example.com"))
DemoFail("插入超链接失败")
if (!Docx_SessionInsertFootnoteAtPlaceholder(doc, "footnote_here", "脚注位置", "这是一条后台插入的脚注。"))
DemoFail("插入脚注失败")
if (!Docx_SessionInsertEndnoteAtPlaceholder(doc, "endnote_here", "尾注位置", "这是一条后台插入的尾注。"))
DemoFail("插入尾注失败")
if (!Docx_SessionAddCommentAtPlaceholder(doc, "comment_here", "批注位置", "这是一条后台插入的批注。"))
DemoFail("插入批注失败")
if (!Docx_SessionInsertTocAtPlaceholder(doc, "toc_here", 3))
DemoFail("插入目录域失败")
if (!Docx_SessionInsertRevisionAtPlaceholder(doc, "revision_here", "这是一段带修订痕迹的新增文本", "insert"))
DemoFail("插入修订痕迹失败")
if (!Docx_Close(doc, locationOutputDoc))
DemoFail("保存定位编辑文档失败")
CleanupWorkingFiles()
FileAppend, % "已生成主文档:" . output . "`n", *
FileAppend, % "占位符表格/图片文档:" . tableEditedDoc . "`n", *
FileAppend, % "样式保真替换文档:" . styleDoc . "`n", *
FileAppend, % "书签替换文档:" . bookmarkDoc . "`n", *
FileAppend, % "内容控件替换文档:" . sdtDoc . "`n", *
FileAppend, % "高级表格编辑文档:" . tableAdvancedDoc . "`n", *
FileAppend, % "定位编辑文档:" . locationOutputDoc . "`n", *
message := "文档生成成功!`n`n"
message .= "主输出:" . output . "`n"
message .= "占位符表格/图片:" . tableEditedDoc . "`n"
message .= "样式保真替换:" . styleDoc . "`n"
message .= "书签替换:" . bookmarkDoc . "`n"
message .= "内容控件替换:" . sdtDoc . "`n"
message .= "高级表格编辑:" . tableAdvancedDoc . "`n"
message .= "定位编辑:" . locationOutputDoc
MsgBox, 64, DocxTplAHK 示例完成, %message%
ExitApp, 0
CreateStyledReplaceTemplate(path) {
temp := A_ScriptDir . "\sample_style_base.docx"
if (!Docx_CreateSimpleTemplate(temp, "样式测试:跨片段文本"))
return false
xml := Docx_ReadXmlPart(temp)
old := "<w:r><w:t xml:space=""preserve"">样式测试:跨片段文本</w:t></w:r>"
repl := "<w:r><w:rPr><w:b/></w:rPr><w:t xml:space=""preserve"">样式测试:跨片</w:t></w:r><w:r><w:t xml:space=""preserve"">段文本</w:t></w:r>"
xml := StrReplace(xml, old, repl)
ok := RepackDocumentXml(temp, path, xml)
FileDelete, %temp%
return ok
}
CreateBookmarkDemoDocx(path) {
temp := A_ScriptDir . "\sample_bookmark_base.docx"
if (!Docx_CreateSimpleTemplate(temp, "书签前:旧书签内容:书签后"))
return false
xml := Docx_ReadXmlPart(temp)
old := "<w:r><w:t xml:space=""preserve"">书签前:旧书签内容:书签后</w:t></w:r>"
repl := "<w:r><w:t xml:space=""preserve"">书签前:</w:t></w:r><w:bookmarkStart w:id=""77"" w:name=""CustomerBookmark""/>" . Docx_BuildRunXml("旧书签内容") . "<w:bookmarkEnd w:id=""77""/><w:r><w:t xml:space=""preserve"">:书签后</w:t></w:r>"
xml := StrReplace(xml, old, repl)
ok := RepackDocumentXml(temp, path, xml)
FileDelete, %temp%
return ok
}
CreateSdtDemoDocx(path) {
temp := A_ScriptDir . "\sample_sdt_base.docx"
if (!Docx_CreateSimpleTemplate(temp, "内容控件位置"))
return false
xml := Docx_ReadXmlPart(temp)
old := "<w:p><w:r><w:t xml:space=""preserve"">内容控件位置</w:t></w:r></w:p>"
repl := "<w:sdt><w:sdtPr><w:alias w:val=""CustomerName""/><w:tag w:val=""CustomerName""/></w:sdtPr><w:sdtContent>" . Docx_BuildParagraphXml("旧内容控件文本") . "</w:sdtContent></w:sdt>"
xml := StrReplace(xml, old, repl)
ok := RepackDocumentXml(temp, path, xml)
FileDelete, %temp%
return ok
}
RepackDocumentXml(inputDocx, outputDocx, documentXml) {
tempDir := Docx_CreateTempDir()
if (tempDir = "")
return false
ok := false
try {
Docx_Unzip(inputDocx, tempDir)
Docx_WriteTextFile(tempDir . "\word\document.xml", documentXml)
if (FileExist(outputDocx))
FileDelete, %outputDocx%
Docx_ZipFolder(tempDir, outputDocx)
ok := FileExist(outputDocx) ? true : false
} catch e {
ok := false
}
Docx_RemoveDir(tempDir)
return ok
}
CleanupDemoFiles() {
global template, output, samplePng, samplePng2, placeholderBase, tableEditedDoc
global styleTemplate, styleDoc, bookmarkTemplate, bookmarkDoc, sdtTemplate, sdtDoc
global tableAdvancedDoc
global locationBase, locationOutputDoc
CleanupWorkingFiles()
FileDelete, %output%
FileDelete, %tableEditedDoc%
FileDelete, %styleDoc%
FileDelete, %bookmarkDoc%
FileDelete, %sdtDoc%
FileDelete, %tableAdvancedDoc%
FileDelete, %locationOutputDoc%
FileDelete, %A_ScriptDir%\sample_output.docx
FileDelete, %A_ScriptDir%\sample_table_edited.docx
FileDelete, %A_ScriptDir%\sample_table_advanced.docx
FileDelete, %A_ScriptDir%\sample_style_replaced.docx
FileDelete, %A_ScriptDir%\sample_bookmark_replaced.docx
FileDelete, %A_ScriptDir%\sample_sdt_replaced.docx
FileDelete, %A_ScriptDir%\sample_location_edits.docx
FileDelete, %A_ScriptDir%\sample_style_base.docx
FileDelete, %A_ScriptDir%\sample_bookmark_base.docx
FileDelete, %A_ScriptDir%\sample_sdt_base.docx
}
CleanupWorkingFiles() {
global template, samplePng, samplePng2, placeholderBase
global styleTemplate, bookmarkTemplate, sdtTemplate
global locationBase
CleanupIntermediateFiles()
FileDelete, %template%
FileDelete, %samplePng%
FileDelete, %samplePng2%
FileDelete, %placeholderBase%
FileDelete, %styleTemplate%
FileDelete, %bookmarkTemplate%
FileDelete, %sdtTemplate%
FileDelete, %locationBase%
FileDelete, %A_ScriptDir%\sample_style_base.docx
FileDelete, %A_ScriptDir%\sample_bookmark_base.docx
FileDelete, %A_ScriptDir%\sample_sdt_base.docx
}
CleanupIntermediateFiles() {
FileDelete, %A_ScriptDir%\sample_step1.docx
FileDelete, %A_ScriptDir%\sample_step2.docx
FileDelete, %A_ScriptDir%\sample_step3.docx
FileDelete, %A_ScriptDir%\sample_step4.docx
FileDelete, %A_ScriptDir%\sample_step5.docx
FileDelete, %A_ScriptDir%\sample_step6.docx
FileDelete, %A_ScriptDir%\sample_step7.docx
FileDelete, %A_ScriptDir%\sample_step8.docx
FileDelete, %A_ScriptDir%\sample_step9.docx
FileDelete, %A_ScriptDir%\sample_step10.docx
FileDelete, %A_ScriptDir%\sample_placeholder_table.docx
FileDelete, %A_ScriptDir%\sample_placeholder_image.docx
FileDelete, %A_ScriptDir%\sample_table_header.docx
FileDelete, %A_ScriptDir%\sample_table_shaded.docx
FileDelete, %A_ScriptDir%\sample_table_border.docx
FileDelete, %A_ScriptDir%\sample_table_align.docx
FileDelete, %A_ScriptDir%\sample_location_link.docx
FileDelete, %A_ScriptDir%\sample_location_footnote.docx
FileDelete, %A_ScriptDir%\sample_location_endnote.docx
FileDelete, %A_ScriptDir%\sample_location_comment.docx
FileDelete, %A_ScriptDir%\sample_location_toc.docx
}
DemoFail(message) {
errorText := Docx_GetLastBackendOutput()
FileAppend, %message%`n%errorText%`n, *
MsgBox, 16, DocxTplAHK 示例失败, %message%`n`n%errorText%
ExitApp, 1
}

评论(0)