如何用 Python 子进程关闭 Excel 自动化中的弹窗


Posted in Python onMay 07, 2021

利用Python进行Excel自动化操作的过程中,尤其是涉及VBA时,可能遇到消息框/弹窗(MsgBox)。此时需要人为响应,否则代码卡死直至超时 [^1] [^2]。根本的解决方法是VBA代码中不要出现类似弹窗,但有时我们无权修改被操作的Excel文件,例如这是我们进行自动化测试的对象。所以本文记录从代码角度解决此类问题的方法。

假想场景

使用xlwings(或者其他自动化库)打开Excel文件test.xlsm,读取Sheet1!A1单元格内容。很简单的一个操作:

import xlwings as xw

wb = xw.Book('test.xlsm')
msg = wb.sheets('Sheet1').range('A1').value
print(msg)
wb.close()

然而不幸的是,打开工作簿时进行了热情的欢迎仪式:

Private Sub Workbook_Open()
    MsgBox "Welcome"
    MsgBox "to open"
    MsgBox "this file."
End Sub

第一个弹窗Welcome就卡住了Excel,Python代码相应卡死在第一行。

如何用 Python 子进程关闭 Excel 自动化中的弹窗

基本思路

主程序中不可能直接处理或者绕过此类问题,也不能奢望有人随时蹲守点击下一步——那就开启一个子线程来护航吧。因此,解决方案是利用子线程监听并随时关闭弹窗,直到主程序圆满结束。
解决这个问题,需要以下两个知识点(基础知识请课外学习):

  • Python多线程(本文采用threading.Thread)
  • Python界面自动化库(本文涉及pywinauto和pywin32)

pywinauto方案

pywinauto顾名思义是Windows界面自动化库,模拟鼠标和键盘操作窗体和控件 [^3]。不同于先获取句柄再获取属性的传统方式,pywinauto的API更加友好和pythonic。例如,两行代码搞定窗口捕捉和点击:

from pywinauto.application import Application

win = Application(backend="win32").connect(title='Microsoft Excel')
win.Dialog.Button.click()

本文采用自定义线程类的方式,启动线程后自动执行run()函数来完成上述操作。具体代码如下,注意构造函数中的两个参数:

  • title 需要捕捉的弹窗的标题,例如Excel默认弹窗的标题为Microsoft Excel
  • interval 监听的频率,即每隔多少秒检查一次
# listener.py

import time
from threading import Thread, Event
from pywinauto.application import Application


class MsgBoxListener(Thread):

    def __init__(self, title:str, interval:int):
        Thread.__init__(self)
        self._title = title 
        self._interval = interval 
        self._stop_event = Event()   

    def stop(self): self._stop_event.set()

    @property
    def is_running(self): return not self._stop_event.is_set()

    def run(self):
        while self.is_running:
            try:
                time.sleep(self._interval)
                self._close_msgbox()
            except Exception as e:
                print(e, flush=True)


    def _close_msgbox(self):
        '''Close the default Excel MsgBox with title "Microsoft Excel".'''        
        win = Application(backend="win32").connect(title=self._title)
        win.Dialog.Button.click()


if __name__=='__main__':
    t = MsgBoxListener('Microsoft Excel', 3)
    t.start()
    time.sleep(10)
    t.stop()

于是,整个过程分为三步:

  • 启动子线程监听弹窗
  • 主线程中打开Excel开始自动化操作
  • 关闭子线程
import xlwings as xw
from listener import MsgBoxListener

# start listen thread
listener = MsgBoxListener('Microsoft Excel', 3)
listener.start()

# main process as before
wb = xw.Book('test.xlsm')
msg = wb.sheets('Sheet1').range('A1').value
print(msg)
wb.close()

# stop listener thread
listener.stop()

到此问题基本解决,本地运行效果完全达到预期。但我的真实需求是以系统服务方式在服务器上进行Excel文件自动化测试,后续发现,当以系统服务方式运行时,pywinauto竟然捕捉不到弹窗!这或许是pywinauto一个潜在的问题 [^4]。

win32gui方案

那就只好转向相对底层的win32gui,所幸完美解决了上述问题。
win32gui是pywin32库的一部分,所以实际安装命令是:

pip install pywin32

整个方案和前文描述完全一致,只是替换MsgBoxListener类中关闭弹窗的方法:

import win32gui, win32con

def _close_msgbox(self):
    # find the top window by title
    hwnd = win32gui.FindWindow(None, self._title)
    if not hwnd: return

    # find child button
    h_btn = win32gui.FindWindowEx(hwnd, None,'Button', None)
    if not h_btn: return

    # show text
    text = win32gui.GetWindowText(h_btn)
    print(text)

    # click button        
    win32gui.PostMessage(h_btn, win32con.WM_LBUTTONDOWN, None, None)
    time.sleep(0.2)
    win32gui.PostMessage(h_btn, win32con.WM_LBUTTONUP, None, None)
    time.sleep(0.2)

更一般的方案

更一般地,当同时存在默认标题和自定义标题的弹窗时,就不便于采用标题方式进行捕捉了。例如

MsgBox "Message with default title.", vbInformation, 
MsgBox "Message with title My App 1", vbInformation, "My App 1"
MsgBox "Message with title My App 2", vbInformation, "My App 2"

那就扩大搜索范围,依次点击所有包含确定性描述的按钮(例如OK,Yes,Confirm)来关闭弹窗。同理替换MsgBoxListener类的_close_msgbox()方法(同时构造函数中不再需要title参数):

def _close_msgbox(self):
    '''Click any button ("OK", "Yes" or "Confirm") to close message box.'''
    # get handles of all top windows
    h_windows = []
    win32gui.EnumWindows(lambda hWnd, param: param.append(hWnd), h_windows) 

    # check each window    
    for h_window in h_windows:            
        # get child button with text OK, Yes or Confirm of given window
        h_btn = win32gui.FindWindowEx(h_window, None,'Button', None)
        if not h_btn: continue

        # check button text
        text = win32gui.GetWindowText(h_btn)
        if not text.lower() in ('ok', 'yes', 'confirm'): continue

        # click button
        win32gui.PostMessage(h_btn, win32con.WM_LBUTTONDOWN, None, None)
        time.sleep(0.2)
        win32gui.PostMessage(h_btn, win32con.WM_LBUTTONUP, None, None)
        time.sleep(0.2)

最后,实例演示结束全文,以后再也不用担心意外弹窗了。

如何用 Python 子进程关闭 Excel 自动化中的弹窗

以上就是如何用 Python 子进程关闭 Excel 自动化中的弹窗的详细内容,更多关于Python 子进程关闭 Excel 弹窗的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python实现从网络下载文件并获得文件大小及类型的方法
Apr 28 Python
python计算方程式根的方法
May 07 Python
在Python中使用zlib模块进行数据压缩的教程
Jun 26 Python
pycharm远程调试openstack代码
Nov 21 Python
python 3利用Dlib 19.7实现摄像头人脸检测特征点标定
Feb 26 Python
Django uwsgi Nginx 的生产环境部署详解
Feb 02 Python
python实现杨氏矩阵查找
Mar 02 Python
使用 Python 玩转 GitHub 的贡献板(推荐)
Apr 04 Python
TensorBoard 计算图的查看方式
Feb 15 Python
python线性插值解析
Jul 05 Python
PIP和conda 更换国内安装源的方法步骤
Sep 21 Python
使用sublime text3搭建Python编辑环境的实现
Jan 12 Python
PyTorch的Debug指南
May 07 #Python
基于Python的EasyGUI学习实践
Python列表删除重复元素与图像相似度判断及删除实例代码
使用python如何删除同一文件夹下相似的图片
May 07 #Python
python学习之panda数据分析核心支持库
Python基于Tkinter开发一个爬取B站直播弹幕的工具
May 06 #Python
Python爬虫之爬取最新更新的小说网站
May 06 #Python
You might like
PHP VS ASP
2006/10/09 PHP
PHP中使用mktime获取时间戳的一个黑色幽默分析
2012/05/31 PHP
php版微信公众平台开发之验证步骤实例详解
2016/09/23 PHP
php实现微信企业转账功能
2018/10/02 PHP
JavaScript 脚本将当地时间转换成其它时区
2009/03/19 Javascript
有道JavaScript监听浏览器的问题
2010/06/23 Javascript
Javascript闭包用法实例分析
2015/01/23 Javascript
深入理解JavaScript系列(21):S.O.L.I.D五大原则之接口隔离原则ISP详解
2015/03/05 Javascript
js上传图片及预览功能实例分析
2015/04/24 Javascript
3种不同的ContextMenu右键菜单实现代码
2016/11/03 Javascript
Thinkphp5微信小程序获取用户信息接口的实例详解
2017/09/26 Javascript
VueJS事件处理器v-on的使用方法
2017/09/27 Javascript
JS 中document.write()的用法和清空的原因浅析
2017/12/04 Javascript
vue axios登录请求拦截器
2018/04/02 Javascript
vue最简单的前后端交互示例详解
2018/10/11 Javascript
微信小程序单选radio及多选checkbox按钮用法示例
2019/04/30 Javascript
Vue路由前后端设计总结
2019/08/06 Javascript
JS页面获取 session 值,作用域和闭包学习笔记
2019/10/16 Javascript
javascript将扁平的数据转为树形结构的高效率算法
2020/02/27 Javascript
[09:37]DOTA2卡尔工作室 英雄介绍圣堂刺客篇
2013/06/13 DOTA
[55:39]DOTA2-DPC中国联赛 正赛 VG vs LBZS BO3 第二场 1月19日
2021/03/11 DOTA
python通过yield实现数组全排列的方法
2015/03/18 Python
利用 python 对目录下的文件进行过滤删除
2017/12/27 Python
Python批量合并有合并单元格的Excel文件详解
2018/04/05 Python
python中的文件打开与关闭操作命令介绍
2018/04/26 Python
python对离散变量的one-hot编码方法
2018/07/11 Python
Django model反向关联名称的方法
2018/12/15 Python
详解Python修复遥感影像条带的两种方式
2020/02/23 Python
一款纯css3实现的非常实用的鼠标悬停特效演示
2014/11/05 HTML / CSS
abstract 可以和 virtual 一起使用吗?可以和 override 一起使用吗?
2012/10/15 面试题
介绍一下MYSQL常用的优化技巧
2012/10/25 面试题
户籍证明书标准模板
2014/09/10 职场文书
2015年校长新年寄语
2014/12/08 职场文书
2015年七一建党节慰问信
2015/03/23 职场文书
中学感恩教育活动总结
2015/05/05 职场文书
幼儿园语言教学反思
2016/02/23 职场文书