├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README_CN.md ├── __init__.py ├── add-in.png ├── pyaddin ├── __init__.py ├── addin.py ├── main.py ├── resources │ ├── CustomUI.xml │ ├── main.cfg │ ├── main.py │ ├── scripts │ │ ├── __init__.py │ │ ├── sample.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ └── context.py │ ├── vba │ │ ├── UserRibbon.bas │ │ ├── general.bas │ │ └── ribbon.bas │ └── xlam │ │ ├── [Content_Types].xml │ │ ├── _rels │ │ └── .rels │ │ ├── docProps │ │ ├── app.xml │ │ └── core.xml │ │ └── xl │ │ ├── _rels │ │ └── workbook.xml.rels │ │ ├── printerSettings │ │ └── printerSettings1.bin │ │ ├── styles.xml │ │ ├── theme │ │ └── theme1.xml │ │ ├── vbaProject.bin │ │ ├── workbook.xml │ │ └── worksheets │ │ ├── _rels │ │ └── sheet1.xml.rels │ │ └── sheet1.xml ├── share.py └── xlam │ ├── __init__.py │ ├── ui.py │ └── vba.py ├── readme.md ├── requirements.txt └── setup.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Create release and publish to Pypi when pushing tags 2 | 3 | name: publish 4 | 5 | # Trigger the workflow on push tags, matching vM.n.p, i.e. v3.2.10 6 | on: 7 | push: 8 | tags: 9 | - 'v[0-9]+.[0-9]+.[0-9]+' 10 | 11 | # Jobs to do: 12 | # - build package 13 | # - create release with asset 14 | # - publish to Pypi 15 | jobs: 16 | 17 | # ----------------------------------------------------------- 18 | # create python env and setup package 19 | # ----------------------------------------------------------- 20 | build: 21 | 22 | # The type of runner that the job will run on 23 | runs-on: ubuntu-latest 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | - name: Check out code 28 | uses: actions/checkout@v2 29 | 30 | - name: Set up Python 3.x 31 | uses: actions/setup-python@v1 32 | with: 33 | python-version: '3.x' 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install setuptools wheel 39 | 40 | # build package for tags, e.g. 3.2.1 extracted from 'refs/tags/v3.2.1' 41 | - name: Build package 42 | run: | 43 | echo ${GITHUB_REF#refs/tags/v} > version.txt 44 | python setup.py sdist --formats=gztar,zip 45 | python setup.py bdist_wheel 46 | 47 | 48 | # upload the artifacts for further jobs 49 | # - app-tag.tar.gz 50 | # - app-tag.zip 51 | # - app-tag-info.whl 52 | - name: Archive package 53 | uses: actions/upload-artifact@v2 54 | with: 55 | name: dist 56 | path: ./dist 57 | 58 | # ----------------------------------------------------------- 59 | # create release and upload asset 60 | # ----------------------------------------------------------- 61 | release: 62 | runs-on: ubuntu-latest 63 | 64 | # Run this job after build completes successfully 65 | needs: build 66 | 67 | steps: 68 | - name: Checkout code 69 | uses: actions/checkout@v2 70 | 71 | # download artifacts from job: build 72 | - name: Download artifacts from build 73 | uses: actions/download-artifact@v2 74 | with: 75 | name: dist 76 | path: dist 77 | 78 | # create release and upload assets 79 | - name: Create release with assets 80 | uses: softprops/action-gh-release@v1 81 | with: 82 | files: | 83 | ./dist/*.tar.gz 84 | ./dist/*.zip 85 | ./dist/*.whl 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | 89 | 90 | # ----------------------------------------------------------- 91 | # publish to Pypi 92 | # ----------------------------------------------------------- 93 | publish: 94 | runs-on: ubuntu-latest 95 | 96 | # Run this job after both build and release completes successfully 97 | needs: [build, release] 98 | 99 | steps: 100 | - name: Checkout code 101 | uses: actions/checkout@v2 102 | 103 | - name: Download artifacts from release 104 | uses: actions/download-artifact@v2 105 | with: 106 | name: dist 107 | path: dist 108 | 109 | # Error when two sdist files created in build job are uploaded to Pipi: 110 | # HTTPError: 400 Bad Request from https://upload.pypi.org/legacy/ 111 | # Only one sdist may be uploaded per release. 112 | - name: Remove duplicated sdist 113 | run: rm ./dist/*.zip 114 | 115 | - name: Publish to PyPI 116 | uses: pypa/gh-action-pypi-publish@master 117 | with: 118 | password: ${{ secrets.PYPI_PASSWORD }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # building results 2 | build/ 3 | dist/ 4 | *egg-info/ 5 | 6 | # test 7 | test/ 8 | sample/ 9 | *.xlam 10 | 11 | # temp file 12 | *.pyc 13 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 dothinking 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include LICENSE 3 | include requirements.txt 4 | include add-in.png 5 | 6 | include pyaddin/resources/main.py 7 | include pyaddin/resources/main.cfg 8 | include pyaddin/resources/CustomUI.xml 9 | recursive-include pyaddin/resources/scripts *.py 10 | recursive-include pyaddin/resources/vba * 11 | recursive-include pyaddin/resources/xlam * -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | Chinese 2 | 3 | # PyAddin 4 | 5 | [![pypi-version](https://img.shields.io/pypi/v/pyaddin.svg)](https://pypi.python.org/pypi/pyaddin/) 6 | ![license](https://img.shields.io/pypi/l/pyaddin.svg) 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 | ![add-in.png](add-in.png) 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 |