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.

  1. Relationship Id does not match with parts
  2. Error in content_types.xml file
  3. Error in parts (document.xml or any other parts)
  4. 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)