一个月前,应邀在网易云课堂做了一次关于 “VBA 与 Python 对比” 的在线讲座(回看录像放在网易《全民一起VBA基础篇》第3课时中,网址 是https://study.163.com/course/courseMain.htm?courseId=1003088001)。
由于同学们的热烈提问,直播时间比计划超出了半个多小时,也让我又一次深刻感受到大家对办公自动化技术的关注和期盼。
不过这几天准备《Python实战篇 之 办公自动化》课程时,我突然想起这次讲座中有一个地方讲述得不够严谨 :我提到 VBA 相比于 Python 的优势之一在于,VBA能够非常方便的自动响应Excel事件(比如在用户点击单元格时自动修改单元格颜色等等),而用 Python 处理这种需求就会非常麻烦。比如在讲座中演示过的那样,先要在 Excel 中安装 xlwings 等第三方 python 库的插件,然后再手动设置对应的Python程序等等。
然而在备课时回想这个问题,我发现用 Python 响应 Excel 事件虽然比不上VBA方便,但也算不上 “非常麻烦”。事实上,如果我们只是想实现基本的响应功能、不考虑多线程、容错等等健壮性问题,那么核心代码只比VBA多出 6 行,而且并不是必须安装第三方模块。
所以,考虑到《Python办公自动化》还要过一段时间推出,我们今天就先用文字的方式讲解一下Python响应Excel事件的基本方法,以正视听。
关键工具 —— win32com
如果不想在Excel中安装 xlwings 等python插件、直接使用普通的Python程序来响应Excel事件,那么我们就要在Python程序与Excel程序之间,建立一个让他俩 “互相通信” 的管道。
通过这个管道,一旦用户在Excel中执行某项操作(比如单击单元格)、激发了一个Excel事件,那么Excel 就会立即将这个事件消息经由该管道,通知给正在运行的 Python 程序。而Python程序一旦接到这个消息,就会马上运行相应的函数代码,并把处理结果经由管道反馈给Excel,让Excel按此结果进行处理。
但是怎样创建这个通信管道呢?最方便的办法,就是使用大名鼎鼎的第三方库 —— Windows办公自动化的终极杀器之一 —— win32com 。
关于 wincom32 的详细应用,我们会在《Python实战篇 之 办公自动化》课程中专题介绍,现在大家只需使用 pip install pywin32 安装到自己电脑上即可。事实上由于很多知名第三方模块都用到了win32com,所以如果先用 pip list 命令查看一下已安装模块,你很可能发现电脑上已经有了这个库。
为什么 win32com 可以起到这个“管道”的作用呢?通俗一点说,这是windows操作系统给我们提供的一个 “设计福利” 。为了让 Windows 平台上各种应用程序之间能够 “互联互通、互相调用” ,微软公司在30年前设计了名为 “COM组件” 的技术体系。在这个体系下,凡是按照COM组件规范开发的程序(比如我们常见的 .dll 文件),都可以被Windows中的其他程序(比如Python)直接调用。
作为微软公司自家的主力产品,Office软件理所当然地成为COM技术的模范应用!具体表现就是:Word、Excel 等等工具既能作为一个独立软件运行,也能够作为一个COM组件,由其他程序指挥和调用(就像我们在Python程序中调用另一个Python程序一样)。
因此顾名思义,win32com这个库的功能,就是让 Python 能够在Windows系统上调用COM组件。而既然 Excel 同时也是一个 COM 组件,那么 python 当然可以通过 win32com 控制 Excel 。
2. 核心代码
了解了 win32com ,接下来我们就可以用它实现Excel的事件响应。具体来说,主要包括 3 个步骤:
(1)编写事件类
首先需要指明:假如Excel那边传来一个消息,比如告诉我们说用户单击选中了B4单元格,那么我们希望运行哪些代码、执行怎样的操作?换句话说,就是编写一个事件响应函数。
学过《全民一起VBA 提高篇》的同学都知道,VBA编写事件响应函数非常简单 —— 在工程窗口双击工作表对象,就可以在右侧选择创建指定事件函数:
类似的,Python中处理过程也差不多,只不过我们要把这些函数写在一个类中(类名随意),而这些函数的名字,需要按下列规则把VBA的函数名转换过来:
比如,下面这段代码定义了一个名为 MyWBEvents 的类,其中两个方法
OnSheetSelectionChange 与 OnSheetBeforeDoubleClick 分别对应“在工作表中单击选中某个单元格” 和 “在工作表中双击某个单元格” 两种事件。
这段代码的意思是:只要用户执行了上面两种操作,Excel 就会自动发消息给这两个方法,并将被单击或双击的单元格对象放到参数 args[1] 中。而这两个函数则会通知Excel,把 args[1] 这个单元格的背景颜色( Range.Interior.Color 属性)改成绿色或黄色(即颜色码65280或65535)。
(2)将事件类绑定到Excel
写好了事件处理函数,接下来就要通知Excel:“从现在开始,用户每执行一次操作,你就要通知我们编写的 MyWBEvents 类!”
怎样通知Excel呢?在 win32com 中非常简单 —— 调用 WithEvents 方法,把 Excel 对象与这个事件类 “绑” 在一起就行。具体代码及注释如下:
请注意:在这个示例里,我们假设用户已经打开了Excel、并且打开了想处理的工作簿文件,然后再运行这个Python程序。因此上面的代码中,我们先用 win32com 的 GetActiveObject 方法找到当前正在运行的Excel程序;然后再用 Workbooks[1] 找到当前Excel中打开的第一份工作簿文件;最后再用 WithEvents 把这个工作簿与我们的事件处理类绑定在一起。
这样,只要这个python程序还在运行,用户点击这个工作簿中各个工作表,都会自动激活我们编写的事件函数。而如果用户在Excel中打开了很多个工作簿,然后操作了其他工作簿中的工作表,那么我们的事件函数就不会启动,因为绑定对象只是第一个工作簿。
(3) 循环等待Excel的通知
如果代码只写到这里,那么我们运行之后,程序会迅速地从第一行执行到最后一行,然后全部执行完毕、退出运行。所以当我们转身操作Excel的时候,Python程序早已离开,根本不会等待我们发消息、做处理。
因此,为了让python程序能够一直在内存中等待我们、时刻准备着响应Excel的事件通知,在程序最后我们需要编写一个“死循环”,让它永远不会执行结束。
但是学过《Python基础篇》的同学应该能想到:陷入死循环的程序会独占计算机资源、影响Excel等其他应用,甚至造成假死机。因此我们要对其稍作处理:每循环一次,就让Python程序暂停0.1秒,以便Excel可以在这0.1秒中正常响应用户操作。这个暂停操作,可以使用《基础篇》中讲过的 time.sleep 实现。
除此之外,每次循环中我们还要与 Excel 程序进行一次沟通,比如查看当前工作簿数量,否则仅靠 time.sleep ,仍然无法保证Excel的正常运行。同时为了应对 “双击进入编辑状态导致异常” 等问题,代码中还需要使用 try except 结构,忽略所有异常。
3. 最终代码与效果
经过上述步骤,我们的演示程序最终如下。为节省篇幅,删去了前面各段代码的详细注释:
它的执行效果也完全如同预期。用户启动Excel、打开工作簿文件,然后再运行我们这个程序,就可以发现:只要单击第一个工作簿的单元格,它就会变成绿色,而双击则会变成黄色。
学过《全民一起VBA》的同学可以看出来,这个Python程序在功能上与下面的VBA代码完全等价。相比之下,确实VBA编写事件响应函数要简洁得多:
但是仔细想想会发现,Python程序虽然麻烦,但也有一些额外的优势。比如:这段VBA代码只能保存在 xlsm 等格式的工作簿中,因此也只能响应xlsm文件的事件,对于普通的 xlsx 文件则不好处理。但是如果使用这个Python程序,无论对什么样的Excel文件,我们都可以实现事件响应,可以说随时随地、灵活方便。
当然,麻烦的地方也有很多,比如需要在工作电脑上单独安装Python环境、每次需要同时启动Excel和Python两个应用等等。总之还是那个道理:工具都各有长短,酌情才是正途。