Python 为 VBA 加载项文件构建脚本
Python Build Script for VBA Add-In file
我已经编写了一个 python 脚本,它将用作我支持的启用宏的 PowerPoint 文件的 "build script"。
该脚本创建一个新的空 PowerPoint 演示文稿,导入所有 VBA 模块,保存文件并将其转换为 ZIP 存档以插入 RibbonUI 配置(ribbon_xml.xml 文件和 mylogo.jpg 文件)。
所有这些都或多或少地按预期工作——直到我尝试使用输出文件(手动将 .zip 重命名为 .pptm 并在 PowerPoint 中打开它)。
错误 代码完全退出,但输出存档 (copy.zip) 在转换为 PPTM 文件时无法完全打开。
我收到配置有问题的警告,PowerPoint 将尝试修复该文件。
PowerPoint,其本质当然没有说明问题是什么,只是它找到了"unreadable content"并且这样的内容有"been removed"...我唯一能看到的比较我手动创建的一些文件后,CustomUI 的 XML 属性似乎使用某种 GUID 作为其 id 属性的一部分
当前解决方法: 函数 build_ribbon 可以使用 CustomUI Editor 工具手动完成,大约需要 3 分钟可靠地生成 PPTM 输出。
所以这不是一个特别的 "Python" 问题,因为它是关于 CustomUI XML / ribbon XML 接口的实现的问题。
完整代码:
import win32com.client
import os
import zipfile
import uuid
#### PARAMETERS
vba_source_control_path = r"C:\Repos\MyAddIn\VBA\ChartBuilder_PPT\Modules"
output_path = r"C:\debug\output.pptm"
ribbon_xml_path = r"C:\Repos\MyAddIn\Ribbon XML\ribbon_xml.xml"
ribbon_logo_path = r"C:\Repos\MyAddIn\Ribbon XML\mylogo.jpg"
def build_addin(pres, path):
"""
This procedure does the following:
1. adds all of the VBComponents to the working PPTM file
The .PPTM file is used for local development & debugging and
is only usually packaged as a PPAM for Testing and Distribution
"""
for fn in [fn for fn in os.listdir(path) if not(fn.endswith(".frx"))]:
pres.VBProject.VBComponents.Import(path + "\" + fn)
# Clean up old files, if any
if os.path.isfile(output_path):
os.remove(output_path)
if os.path.isfile(output_path.replace(".pptm", ".zip")):
os.remove(output_path.replace(".pptm", ".zip"))
# Save the new file with VBProject components
pres.SaveAs(output_path)
pres.Close()
def build_ribbon_zip():
"""
build_ribbon_zip handles manipulation of the .ZIP contents and places the
necessary components within the PPTM ZIP archive structure
2. converts the PPTM to a .ZIP
3. Adds the CustomUI XML and logo.jpg to the .ZIP directory
4. converts the .ZIP to a PPTM
"""
id = '<Relationship Id='
schema = 'http://schemas.openxmlformats.org/officeDocument/2006/'
_path = output_path.replace(".pptm", ".zip")
copy_path = r"C:\debug\copy.zip"
# Convert to ZIP archive
os.rename(output_path, _path)
zip = zipfile.ZipFile(_path, 'a')
copy = zipfile.ZipFile(copy_path, 'w')
guid = str(uuid.uuid4()).replace('-', '')[:16]
for itm in [itm for itm in zip.infolist() if itm.filename != r'_rels/.rels']:
buffer = zip.read(itm.filename)
copy.writestr(itm, buffer)
# Append the Logo file to the .zip and create the archive
copy.write(ribbon_logo_path, r'\CustomUI\images\jdplogo.jpg')
# append the CustomUI xml part to the .zip and create the archive
copy.write(ribbon_xml_path, r'\CustomUI\customUI14.xml')
# append the .rels file to CustomUI\_rels
rels_xml = r'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
rels_xml += r'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
rels_xml += r'<Relationship Id="jdplogo" Type="'+schema+'relationships/image" Target="images/jdplogo.jpg"/>'
rels_xml += r'</Relationships>'
copy.writestr(r'CustomUI\_rels\customUI14.xml.rels', rels_xml.encode('utf-8'))
# get the existing _rels/.rels XML content and append the UI:
rels_xml = zip.read(r'_rels/.rels').rstrip()[:-16]
rels_xml += id + r'"R'+guid+'" Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility"'
rels_xml += r'Target="customUI/customUI14.xml"/></Relationships>'
rels_xml = rels_xml.replace(os.linesep, '')
# this file-like object is read-only, and the writestr method will create another .rels file...
copy.writestr(r'_rels/.rels', rels_xml.encode('utf-8'))
zip.close()
copy.close()
if __name__ == "__main__":
"""
Procedure to create a new PowerPoint Presentation and insert the Code Modules from source control
"""
ppApp = win32com.client.Dispatch("PowerPoint.Application")
pres = ppApp.Presentations.Add(False)
pres.Slides.AddSlide(1, pres.SlideMaster.CustomLayouts(1))
build_addin(pres, vba_source_control_path)
ppApp.Quit()
build_ribbon_zip()
输出中缺少一些引用,导致 PowerPoint 异常。像这样解决这个问题:
def build_addin(pres, path):
"""
This procedure does the following:
1. adds all of the VBComponents to the working PPTM file
2. adds required project references
The .PPTM file is used for local development & debugging and
is only usually packaged as a PPAM for Testing and Distribution
"""
version = str(int(float(pres.Application.version)))
# import the VB Components
for fn in [fn for fn in os.listdir(path) if not(fn.endswith(".frx"))]:
pres.VBProject.VBComponents.Import(path + "\" + fn)
# add the required project references
pres.VBProject.References.AddFromFile(r'C:\Program Files (x86)\Microsoft Office\Office'+version+'\EXCEL.EXE')
# MSForms TreeView Control
pres.VBProject.References.AddFromFile(r'C:\Windows\SysWOW64\MSCOMCTL.OCX')
# MSXML2
pres.VBProject.References.AddFromFile(r'C:\Windows\System32\msxml6.dll')
# ADODB
pres.VBProject.References.AddFromFile(r'C:\Program Files (x86)\Common Files\System\ado\msado15.dll')
# VBE Extensibility
我还在 build_ribbon
中发现了一些可能格式错误的 XML 并修复了它,但仍然不是 100%,因为 PowerPoint 在首次打开时仍然需要 "repair" 文件(一次),但在那之后,它似乎按预期工作。
我注意到自定义徽标没有出现在功能区中,我发现 "unreadable content" 可能与加载到其中一个功能区控件上的 JPG 图像文件有关。来自 this forum on OpenXMLDeveloper:
This kind of problem occurs when there is an issue in one of the
following areas.
- Relationship Id does not match with parts
- Error in content_types.xml file
- Error in parts (document.xml or any other parts)
- Mismatched link between slide master-slide layout/slide layout-slide
我仔细检查了 [Content_Types]。xml 文件不包含 .jpg 文件扩展名的元素。
我为 ElementTree 添加导入语句:
import xml.etree.ElementTree as ET
然后修改build_ribbon_zip
如下:
def build_ribbon_zip():
"""
build_ribbon_zip handles manipulation of the .ZIP contents and places the
necessary components within the PPTM ZIP archive structure
3. converts the PPTM to a .ZIP
4. Adds the CustomUI XML to the .ZIP directory
5. converts the .ZIP to a PPTM
"""
bom = u'\ufeff'
_path=output_path.replace('.pptm', '.zip')
copy_path=r'C:\debug\copy.zip'
# Convert to ZIP archive
os.rename(output_path, _path)
z=zipfile.ZipFile(_path, 'a', zipfile.ZIP_DEFLATED)
copy=zipfile.ZipFile(copy_path, 'w', zipfile.ZIP_DEFLATED)
guid=str(uuid.uuid4()).replace('-', '')[:16]
"""
the .rels files are written directly from XML string built in procedure
the [Content_Types].xml file needs to include additional parameter for the 'jpg' extension
"""
for itm in [itm for itm in z.infolist() if itm.filename != r'_rels/.rels']:
buffer = z.read(itm.filename)
if itm.filename == "[Content_Types].xml":
# Modify the [Content_Types].xml file to include the jpg reference
# <Default Extension="jpg" ContentType="image/.jpg" />
# copy the XML from the original zip archive, this file has not been copied in the above loop
root = ET.fromstring(buffer)
ET.SubElement(root, '{http://schemas.openxmlformats.org/package/2006/content-types}Default', {'Extension': 'jpg', 'ContentType': 'image/.jpg'})
copy.writestr(itm, ET.tostring(root).encode('utf-8'))
# Append the Logo file to the .zip and create the archive
copy.write(ribbon_logo_path, r'\customUI\images\jdplogo.jpg')
else:
copy.writestr(itm, buffer)
# append the CustomUI xml part to the .zip and create the archive
copy.write(ribbon_xml_path, r'\customUI\customUI14.xml')
# create the string & append the .rels to CustomUI\_rels
rels_xml = """<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="images/jdplogo.jpg" Id="jdplogo" />
</Relationships>"""
copy.writestr(r'customUI\_rels\customUI14.xml.rels', rels_xml.encode('utf-8'))
# get the existing _rels/.rels XML content and copy to the copied archiveI:
rels_xml = r'<?xml version="1.0" encoding="utf-8" ?>'
rels_xml += r'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/'
rels_xml += r'core-properties" '
rels_xml += r'Target="docProps/core.xml" Id="rId3" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" '
rels_xml += r'Target="docProps/thumbnail.jpeg" Id="rId2" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" '
rels_xml += r'Target="ppt/presentation.xml" Id="rId1" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" '
rels_xml += r'Target="docProps/app.xml" Id="rId4" /><Relationship '
rels_xml += r'Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility" '
rels_xml += r'Target="/customUI/customUI14.xml" Id="R'+guid+'" /></Relationships>'
copy.writestr(r'_rels\.rels', rels_xml.encode('utf-8'))
z.close()
copy.close()
os.remove(_path)
os.rename(copy_path, output_path)
我已经编写了一个 python 脚本,它将用作我支持的启用宏的 PowerPoint 文件的 "build script"。
该脚本创建一个新的空 PowerPoint 演示文稿,导入所有 VBA 模块,保存文件并将其转换为 ZIP 存档以插入 RibbonUI 配置(ribbon_xml.xml 文件和 mylogo.jpg 文件)。
所有这些都或多或少地按预期工作——直到我尝试使用输出文件(手动将 .zip 重命名为 .pptm 并在 PowerPoint 中打开它)。
错误 代码完全退出,但输出存档 (copy.zip) 在转换为 PPTM 文件时无法完全打开。
我收到配置有问题的警告,PowerPoint 将尝试修复该文件。
PowerPoint,其本质当然没有说明问题是什么,只是它找到了"unreadable content"并且这样的内容有"been removed"...我唯一能看到的比较我手动创建的一些文件后,CustomUI 的 XML 属性似乎使用某种 GUID 作为其 id 属性的一部分
当前解决方法: 函数 build_ribbon 可以使用 CustomUI Editor 工具手动完成,大约需要 3 分钟可靠地生成 PPTM 输出。
所以这不是一个特别的 "Python" 问题,因为它是关于 CustomUI XML / ribbon XML 接口的实现的问题。
完整代码:
import win32com.client
import os
import zipfile
import uuid
#### PARAMETERS
vba_source_control_path = r"C:\Repos\MyAddIn\VBA\ChartBuilder_PPT\Modules"
output_path = r"C:\debug\output.pptm"
ribbon_xml_path = r"C:\Repos\MyAddIn\Ribbon XML\ribbon_xml.xml"
ribbon_logo_path = r"C:\Repos\MyAddIn\Ribbon XML\mylogo.jpg"
def build_addin(pres, path):
"""
This procedure does the following:
1. adds all of the VBComponents to the working PPTM file
The .PPTM file is used for local development & debugging and
is only usually packaged as a PPAM for Testing and Distribution
"""
for fn in [fn for fn in os.listdir(path) if not(fn.endswith(".frx"))]:
pres.VBProject.VBComponents.Import(path + "\" + fn)
# Clean up old files, if any
if os.path.isfile(output_path):
os.remove(output_path)
if os.path.isfile(output_path.replace(".pptm", ".zip")):
os.remove(output_path.replace(".pptm", ".zip"))
# Save the new file with VBProject components
pres.SaveAs(output_path)
pres.Close()
def build_ribbon_zip():
"""
build_ribbon_zip handles manipulation of the .ZIP contents and places the
necessary components within the PPTM ZIP archive structure
2. converts the PPTM to a .ZIP
3. Adds the CustomUI XML and logo.jpg to the .ZIP directory
4. converts the .ZIP to a PPTM
"""
id = '<Relationship Id='
schema = 'http://schemas.openxmlformats.org/officeDocument/2006/'
_path = output_path.replace(".pptm", ".zip")
copy_path = r"C:\debug\copy.zip"
# Convert to ZIP archive
os.rename(output_path, _path)
zip = zipfile.ZipFile(_path, 'a')
copy = zipfile.ZipFile(copy_path, 'w')
guid = str(uuid.uuid4()).replace('-', '')[:16]
for itm in [itm for itm in zip.infolist() if itm.filename != r'_rels/.rels']:
buffer = zip.read(itm.filename)
copy.writestr(itm, buffer)
# Append the Logo file to the .zip and create the archive
copy.write(ribbon_logo_path, r'\CustomUI\images\jdplogo.jpg')
# append the CustomUI xml part to the .zip and create the archive
copy.write(ribbon_xml_path, r'\CustomUI\customUI14.xml')
# append the .rels file to CustomUI\_rels
rels_xml = r'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
rels_xml += r'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
rels_xml += r'<Relationship Id="jdplogo" Type="'+schema+'relationships/image" Target="images/jdplogo.jpg"/>'
rels_xml += r'</Relationships>'
copy.writestr(r'CustomUI\_rels\customUI14.xml.rels', rels_xml.encode('utf-8'))
# get the existing _rels/.rels XML content and append the UI:
rels_xml = zip.read(r'_rels/.rels').rstrip()[:-16]
rels_xml += id + r'"R'+guid+'" Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility"'
rels_xml += r'Target="customUI/customUI14.xml"/></Relationships>'
rels_xml = rels_xml.replace(os.linesep, '')
# this file-like object is read-only, and the writestr method will create another .rels file...
copy.writestr(r'_rels/.rels', rels_xml.encode('utf-8'))
zip.close()
copy.close()
if __name__ == "__main__":
"""
Procedure to create a new PowerPoint Presentation and insert the Code Modules from source control
"""
ppApp = win32com.client.Dispatch("PowerPoint.Application")
pres = ppApp.Presentations.Add(False)
pres.Slides.AddSlide(1, pres.SlideMaster.CustomLayouts(1))
build_addin(pres, vba_source_control_path)
ppApp.Quit()
build_ribbon_zip()
输出中缺少一些引用,导致 PowerPoint 异常。像这样解决这个问题:
def build_addin(pres, path):
"""
This procedure does the following:
1. adds all of the VBComponents to the working PPTM file
2. adds required project references
The .PPTM file is used for local development & debugging and
is only usually packaged as a PPAM for Testing and Distribution
"""
version = str(int(float(pres.Application.version)))
# import the VB Components
for fn in [fn for fn in os.listdir(path) if not(fn.endswith(".frx"))]:
pres.VBProject.VBComponents.Import(path + "\" + fn)
# add the required project references
pres.VBProject.References.AddFromFile(r'C:\Program Files (x86)\Microsoft Office\Office'+version+'\EXCEL.EXE')
# MSForms TreeView Control
pres.VBProject.References.AddFromFile(r'C:\Windows\SysWOW64\MSCOMCTL.OCX')
# MSXML2
pres.VBProject.References.AddFromFile(r'C:\Windows\System32\msxml6.dll')
# ADODB
pres.VBProject.References.AddFromFile(r'C:\Program Files (x86)\Common Files\System\ado\msado15.dll')
# VBE Extensibility
我还在 build_ribbon
中发现了一些可能格式错误的 XML 并修复了它,但仍然不是 100%,因为 PowerPoint 在首次打开时仍然需要 "repair" 文件(一次),但在那之后,它似乎按预期工作。
我注意到自定义徽标没有出现在功能区中,我发现 "unreadable content" 可能与加载到其中一个功能区控件上的 JPG 图像文件有关。来自 this forum on OpenXMLDeveloper:
This kind of problem occurs when there is an issue in one of the following areas.
- Relationship Id does not match with parts
- Error in content_types.xml file
- Error in parts (document.xml or any other parts)
- Mismatched link between slide master-slide layout/slide layout-slide
我仔细检查了 [Content_Types]。xml 文件不包含 .jpg 文件扩展名的元素。
我为 ElementTree 添加导入语句:
import xml.etree.ElementTree as ET
然后修改build_ribbon_zip
如下:
def build_ribbon_zip():
"""
build_ribbon_zip handles manipulation of the .ZIP contents and places the
necessary components within the PPTM ZIP archive structure
3. converts the PPTM to a .ZIP
4. Adds the CustomUI XML to the .ZIP directory
5. converts the .ZIP to a PPTM
"""
bom = u'\ufeff'
_path=output_path.replace('.pptm', '.zip')
copy_path=r'C:\debug\copy.zip'
# Convert to ZIP archive
os.rename(output_path, _path)
z=zipfile.ZipFile(_path, 'a', zipfile.ZIP_DEFLATED)
copy=zipfile.ZipFile(copy_path, 'w', zipfile.ZIP_DEFLATED)
guid=str(uuid.uuid4()).replace('-', '')[:16]
"""
the .rels files are written directly from XML string built in procedure
the [Content_Types].xml file needs to include additional parameter for the 'jpg' extension
"""
for itm in [itm for itm in z.infolist() if itm.filename != r'_rels/.rels']:
buffer = z.read(itm.filename)
if itm.filename == "[Content_Types].xml":
# Modify the [Content_Types].xml file to include the jpg reference
# <Default Extension="jpg" ContentType="image/.jpg" />
# copy the XML from the original zip archive, this file has not been copied in the above loop
root = ET.fromstring(buffer)
ET.SubElement(root, '{http://schemas.openxmlformats.org/package/2006/content-types}Default', {'Extension': 'jpg', 'ContentType': 'image/.jpg'})
copy.writestr(itm, ET.tostring(root).encode('utf-8'))
# Append the Logo file to the .zip and create the archive
copy.write(ribbon_logo_path, r'\customUI\images\jdplogo.jpg')
else:
copy.writestr(itm, buffer)
# append the CustomUI xml part to the .zip and create the archive
copy.write(ribbon_xml_path, r'\customUI\customUI14.xml')
# create the string & append the .rels to CustomUI\_rels
rels_xml = """<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="images/jdplogo.jpg" Id="jdplogo" />
</Relationships>"""
copy.writestr(r'customUI\_rels\customUI14.xml.rels', rels_xml.encode('utf-8'))
# get the existing _rels/.rels XML content and copy to the copied archiveI:
rels_xml = r'<?xml version="1.0" encoding="utf-8" ?>'
rels_xml += r'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/'
rels_xml += r'core-properties" '
rels_xml += r'Target="docProps/core.xml" Id="rId3" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" '
rels_xml += r'Target="docProps/thumbnail.jpeg" Id="rId2" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" '
rels_xml += r'Target="ppt/presentation.xml" Id="rId1" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" '
rels_xml += r'Target="docProps/app.xml" Id="rId4" /><Relationship '
rels_xml += r'Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility" '
rels_xml += r'Target="/customUI/customUI14.xml" Id="R'+guid+'" /></Relationships>'
copy.writestr(r'_rels\.rels', rels_xml.encode('utf-8'))
z.close()
copy.close()
os.remove(_path)
os.rename(copy_path, output_path)