|
| 1 | +中文 | [English](./README_CN.md) |
| 2 | + |
| 3 | +# PyAddin |
| 4 | + |
| 5 | +[](https://pypi.python.org/pypi/pyaddin/) |
| 6 | + |
| 7 | + |
| 8 | +VBA日渐式微,但 Excel 依旧是表格数据存储、处理和传阅的通用工具。对于一些复杂的业务逻辑,一个带宏的 Excel 文件(xlsm)通常包含数据本身和处理数据的VBA脚本。当需要频繁复用这些脚本时,例如按相同的业务逻辑处理月度数据,建议将其拆分为两部分: Excel 纯数据文件(xlsx),以及一个负责数据处理的 Excel 插件(xlam)。 |
| 9 | + |
| 10 | +`PyAddin`是一个辅助创建上述 Excel 插件的命令行工具,同时支持使用 Python 语言来实现原本 VBA 负责的数据处理流程。两个基本功能如下: |
| 11 | + |
| 12 | +- 创建一个模板插件,支持自定义菜单功能区(Ribbon)和近“无缝”调用 Python 脚本。 |
| 13 | +- 在开发插件过程中,方便根据自定义的`CustomUI.xml`更新插件菜单功能区。 |
| 14 | + |
| 15 | +集成 VBA 和 Python 的主要思路:VBA 通过后台运行控制台程序调用 Python 脚本,Python 脚本执行计算并以写临时文件的方式保存返回值,最后 VBA 读取返回值。此外,借助 Python 第三方库`pywin32`,可以直接在 Python 脚本中进行与 Excel 的交互,例如获取/设置单元格内容,设置单元格样式等等。 |
| 16 | + |
| 17 | + |
| 18 | +## 限制 |
| 19 | + |
| 20 | +- 仅支持 Windows 平台 |
| 21 | +- 要求 Microsoft Excel 2007 及以上 |
| 22 | +- VBA 和 Python 之间传参仅限于 **字符串** 格式简单数据类型 |
| 23 | +- 与 Excel 的交互能力取决于 `pywin32/win32com` |
| 24 | + |
| 25 | + |
| 26 | +## 安装 |
| 27 | + |
| 28 | +支持 `Pypi` 或者本地安装: |
| 29 | + |
| 30 | +``` |
| 31 | +# pypi |
| 32 | +pip install pyaddin |
| 33 | +
|
| 34 | +# local |
| 35 | +python setup.py install |
| 36 | +
|
| 37 | +# local in development mode |
| 38 | +python setup.py develop |
| 39 | +``` |
| 40 | + |
| 41 | +使用 `pip` 卸载: |
| 42 | + |
| 43 | +``` |
| 44 | +pip uninstall pyaddin |
| 45 | +``` |
| 46 | + |
| 47 | +## 命令说明 |
| 48 | + |
| 49 | +- 创建模板插件 |
| 50 | + |
| 51 | +``` |
| 52 | +pyaddin init --name=xxx --quiet=True|False |
| 53 | +``` |
| 54 | + |
| 55 | +- 更新插件功能区 |
| 56 | + |
| 57 | +``` |
| 58 | +pyaddin update --name=xxx --quiet=True|False |
| 59 | +``` |
| 60 | + |
| 61 | +其中,`quiet`是可选参数,表明是否以后台模式创建插件(不显式打开 Excel)。默认值`True`,即后台模式。 |
| 62 | + |
| 63 | +## 使用帮助 |
| 64 | + |
| 65 | +### 1. 初始化模板插件 |
| 66 | + |
| 67 | +``` |
| 68 | +D:\WorkSpace>pyaddin init --name=sample |
| 69 | +``` |
| 70 | + |
| 71 | +在当前目录下新建了文件夹`sample`,其中包含模板插件`sample.xlam`,以及实现VBA 与 Python 互联所需的支持文件。目录结构如下: |
| 72 | + |
| 73 | +``` |
| 74 | +sample\ |
| 75 | +|- scripts\ |
| 76 | +| |- utils\ |
| 77 | +| | |- __init__.py |
| 78 | +| | |- context.py |
| 79 | +| |- __init__.py |
| 80 | +| |- sample.py |
| 81 | +|- main.cfg |
| 82 | +|- main.py |
| 83 | +|- CustomUI.xml |
| 84 | +|- sample.xlam |
| 85 | +``` |
| 86 | + |
| 87 | +其中, |
| 88 | + |
| 89 | +- `main.py`是 VBA 调用 Python 脚本的入口文件。 |
| 90 | +- `main.cfg`是基本配置参数文件,例如指定 Python 解释器的路径。 |
| 91 | +- `scripts`存放处理具体业务的 Python 脚本,例如`sample.py`是其中的一个示意模块,开发者根据需要在此目录下创建其他模块。 |
| 92 | +- `CustomUI.xml`定义了插件的 Ribbon 界面,例如包含的控件及样式。 |
| 93 | +- `sample.xlam`为模板插件,开发者可以在此基础上添加和扩展自定义的功能。 |
| 94 | + |
| 95 | +当前模板的 Ribbon 区域参考下图。 |
| 96 | + |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +### 2. 自定义 Ribbon 区域 |
| 101 | + |
| 102 | +在上一步创建的 [`CustomUI.xml`](./pyaddin/resources/CustomUI.xml) 的基础上,根据具体需求设计界面和样式,然后运行以下命令将其更新到插件中去。当然, Excel 2007及以上的文件本质上是一个压缩文件,因此可以手动解压后替换相应的`CustomUI.xml`,或者直接借助其他的 Ribbon XML 编辑器进行修改。 |
| 103 | + |
| 104 | + |
| 105 | +``` |
| 106 | +D:\WorkSpace\sample>pyaddin update --name=sample |
| 107 | +``` |
| 108 | + |
| 109 | + |
| 110 | +以此模板为例,定义了两个分组: |
| 111 | + |
| 112 | +- `setting`组提供基础功能,请直接保留在你的项目中。例如,运行前需要设置 Python 解释器路径。 |
| 113 | +- `Your Group 1`是一个示例分组,其中设计了两个按钮。实际开发中请替换为需要的控件,或者增加其他更多分组。 |
| 114 | + |
| 115 | +关于 Ribbon 界面的具体介绍及格式规范,参考以下链接。 |
| 116 | + |
| 117 | +- [General Format of XML Markup Files](https://docs.microsoft.com/en-us/previous-versions/office/developer/office-2007/aa338202(v%3doffice.12)#general-format-of-xml-markup-files) |
| 118 | +- [Custom UI](https://docs.microsoft.com/en-us/openspecs/office_standards/ms-customui/edc80b05-9169-4ff7-95ee-03af067f35b1) |
| 119 | + |
| 120 | + |
| 121 | +### 3. 实现 Ribbon 控件响应函数 |
| 122 | + |
| 123 | +这一步需要在 VBA 中实现 `CustomUI.xml`定义的控件事件及其响应函数。 |
| 124 | + |
| 125 | +以模板插件为例,下面的 XML 片段表明按钮`Sample 1`的点击事件由 VBA 过程`CB_Sample_1`响应。 |
| 126 | + |
| 127 | +```xml |
| 128 | +<button id="sample1" label="Sample 1" |
| 129 | + imageMso="AppointmentColor3" size="large" |
| 130 | + onAction="CB_Sample_1" |
| 131 | +...> |
| 132 | +``` |
| 133 | + |
| 134 | +进一步,查看插件的`UserRibbon`模块中定义的两个响应过程: |
| 135 | + |
| 136 | + |
| 137 | +```vb |
| 138 | +Sub CB_Sample_1(control As IRibbonControl) |
| 139 | + '''onAction for control: Sample 1''' |
| 140 | + Dim res As Object |
| 141 | + Dim x As Integer: x = Range("A1").Value |
| 142 | + Dim y As Integer: y = Range("A2").Value |
| 143 | + |
| 144 | + Set res = RunPython("scripts.sample.run_example_1", x, y) |
| 145 | + Range("A3") = res("value") |
| 146 | +End Sub |
| 147 | + |
| 148 | +Sub CB_Sample_2(control As IRibbonControl) |
| 149 | + '''onAction for control: Sample 2''' |
| 150 | + RunPython "scripts.sample.run_example_2" |
| 151 | +End Sub |
| 152 | +``` |
| 153 | + |
| 154 | +可以看到,二者都是通过`RunPython()`函数来调用 Python 脚本并获取返回值,函数签名如下: |
| 155 | + |
| 156 | +```vb |
| 157 | +Function RunPython(methodName As String, ParamArray args()) As Object |
| 158 | +``` |
| 159 | + |
| 160 | +- 第一个参数`methodName`表示调用的 Python 方法,具体格式为`package.module.method`。本例 "scripts.sample.run_example_1" 表示调用脚本`sample/scripts/sample.py`中的`run_example_1`方法。 |
| 161 | + |
| 162 | +- 第二个参数`args`是可变长参数,可以传入任意个数的参数给相应 Python 方法。注意,仅支持字符串、数字等简单数据类型,并且经过控制台参数传递,最终到 Python 端都被转成了字符串格式。 |
| 163 | + |
| 164 | +- 返回值为 VBA 的`Dictionary`格式,其中包含两个键: |
| 165 | + - `status`: 调用成功与否,True 或者 False |
| 166 | + - `value`: 返回值(错误信息如果`status`为 False) |
| 167 | + |
| 168 | + |
| 169 | +### 4. 实现 Python 脚本 |
| 170 | + |
| 171 | +根据 VBA 端`RunPython()`调用路径和参数列表,创建相应的 Python 脚本文件及函数。 |
| 172 | + |
| 173 | +以模板插件为例,在项目目录下查看`scripts\sample.py`文件: |
| 174 | + |
| 175 | +```python |
| 176 | +# sample.py |
| 177 | +from .utils import context |
| 178 | + |
| 179 | +def run_example_1(x:str, y:str): |
| 180 | + return int(x) + int(y) |
| 181 | + |
| 182 | +def run_example_2(): |
| 183 | + # get the workbook calling this method, then do anything with win32com |
| 184 | + wb = context.get_caller() |
| 185 | + sheet = wb.ActiveSheet |
| 186 | + |
| 187 | + # get cells value |
| 188 | + x = sheet.Range('A1').Value |
| 189 | + y = sheet.Range('A2').Value |
| 190 | + |
| 191 | + # set cell value |
| 192 | + sheet.Range('A3').Value = x + y |
| 193 | +``` |
| 194 | + |
| 195 | +根据上下文可知,上述两个方法实现相同的事情,将单元格 A1、A2 的和写到单元格 A3,进一步可以分为三步:取值,求和,设置值。但实现的思路略有不同: |
| 196 | + |
| 197 | +- `run_example_1`的思路是仅将 Excel 无关的流程(求和)交由 Python 实现,所有 Excel 相关的操作(取值、设置值)仍通过 VBA 执行,二者的桥梁是参数传递。 |
| 198 | + |
| 199 | +- `run_example_2`的思路是在 Python 端获取到调用此脚本的工作簿,然后直接对其做所有需要的操作(取值,求和,设置值)。 |
| 200 | + |
| 201 | +对比可以发现,前者按各自强项分工,运行效率较高,但 VBA 和 Python 之间存在强耦合,且容易受限于参数传递的复杂度;后者几乎完全解耦 VBA 和 Python,且避免了参数传递,但操作 Excel 的能力受限于`pywin32`,某些操作可能无法实现。 |
| 202 | + |
| 203 | + |
| 204 | +### 5. 交付插件 |
| 205 | + |
| 206 | +因为依赖关系,整个工程应作为整体(第一步中所示结构)交付,且需要最终用户配置好 Python 环境及相应第三方库(主要是`pywin32`)。有时候 Python 环境对用户来说是一个挑战,因此可以考虑可移植的便携式 Python,事先安装好依赖库后随插件工程一起发布,如此便于最终用户开箱即用。 |
| 207 | + |
| 208 | + |
| 209 | +## 许可 |
| 210 | + |
| 211 | +MIT License |
0 commit comments