如何用 Python 子进程关闭 Excel 自动化中的弹窗
作者:crazyhat 发布时间:2023-10-16 15:47:32
目录
假想场景
基本思路
pywinauto方案
win32gui方案
更一般的方案
利用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多线程(本文采用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)
最后,实例演示结束全文,以后再也不用担心意外弹窗了。
来源:https://mp.weixin.qq.com/s/t36llQH-edcP3ShTT4Q5tQ
猜你喜欢
- 主要用到的js代码function getSelectedText(){ &nbs
- 本文主要是基于Python Opencv 实现的图像分割,其中使用到的opencv的函数有:使用 OpenCV 函数 cv::filter2
- 本文详解的讲解了使用Pillow库进行图片的简单处理,使用PyCharm开发Python的详细过程和各种第三方库的安装与使用。目标1.熟悉P
- 想做个和IBM公司一样的网站LOGO,试了半天也没有做出来,郁闷之下,只好求高手帮助!先在这里谢谢了!方法一1、写上IBM,调节字号颜色2、
- PHP 中文工具类,支持汉字转拼音、拼音分词、简繁互转。PHP Chinese Tool class, support Chinese pi
- 对所有数据进行整合与管理当你使用SQL Server 2008企业级的数据仓库平台时,你可以高效的操纵所有数据,并对其进行统一管理存储。◆合
- 模式库 在模式库里,我将列出所有电子商务网站需要的模式.以下将罗列出经典常用的模式案例,我也试图让这些模式看起来更有趣味性与实用性。(Yah
- file_get_contents的超时处理话说,从PHP5开始,file_get_content已经支持context了(手册上写着:5.
- 前言本文介绍的主要内容是 Redux-Toolkit 在 React + TypeScript 大型应用中的实践,主要解决的问题是使用 cr
- (1)、back_log:要求 MySQL 能有的连接数量。当主要MySQL线程在一个很短时间内得到非常多的连接请求,这就起作用,然后主线程
- 刚刚换用windows7 64位旗舰版,使用其自带的iis7作为调试工具,今天调试一个ASP+ACCESS的网站的时候遇到了“ADODB.C
- ASP给图片加水印是需要组件的...常用的有aspjpeg软件和中国人自己开发的wsImage软件,可以上网搜索下载这两个软件,推荐使用咱们
- PyQt5动态(可拖动控件大小)布局控件QSplitter简介PyQt还提供了特殊的布局管理器QSplitter。它可以动态地拖动子控件之间
- 程序运行效率程序的运行效率分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。时间复杂度主要
- 原来工作中曾经碰到过UL列表里一些异常的表现,加上昨天看到了http://bbs.blueidea.com/thread-2984871-1
- 类的私有属性和方法Python是个开放的语言,默认情况下所有的属性和方法都是公开的 或者叫公有方法,不像C++和 Java中有明确的publ
- 我用 ip=Request.ServerVariables ("
- 优雅的设计经常包含一些特殊的字体,而这些字体并不存在于用户的字体库中,我们并不能奢求每一个访客都是设计师。 :-)虽然CSS3标
- 孤立帐户,就是某个数据库的帐户只有用户名而没有登录名,这样的用户在用户库的sysusers系统表中存在,而在master数据库的syslog
- <P><HTML><HEAD><TITLE>javascriptboy</TITLE&