在使用Python自动化Excel操作的过程中,尤其是涉及到VBA时,可能会遇到消息框/弹窗(MsgBox)。此时需要人工响应,否则代码将冻结,直到超时[^1][^2]。根本的解决办法是在VBA代码中避免类似的弹窗,但是有时候我们没有权限修改正在操作的Excel文件,比如这是我们自动化测试的对象。所以本文从代码的角度记录解决此类问题的方法。假设场景使用xlwings(或其他自动化库)打开Excel文件test.xlsm并读取Sheet1!A1单元格的内容。很简单的一个操作:importxlwingsasxwwb=xw.Book('test.xlsm')msg=wb.sheets('Sheet1').range('A1').valueprint(msg)wb.close()遗憾的是,热烈的欢迎仪式在工作簿打开时执行:PrivateSubWorkbook_Open()MsgBox"Welcome"MsgBox"toopen"MsgBox"thisfile."EndSub第一个弹窗Welcome卡住Excel,Python代码相应卡在第一行。基本思路这类问题不可能在主程序中直接处理或绕过,也不可能指望有人等着随时点下一步——然后开个子线程护航。因此,解决方案是使用子线程随时监控并关闭弹窗,直到主程序圆满结束。解决这个问题需要以下两个知识点(基础知识课外学习):Python多线程(本文使用threading.Thread)Python接口自动化库(本文涉及pywinauto和pywin32)鼠标和键盘操作窗体和控件[^3]。不同于传统的先获取句柄再获取属性的方式,pywinauto的API更加友好和pythonic。例如两行代码处理窗口捕获和点击:frompywinauto.applicationimportApplicationwin=Application(backend="win32").connect(title='MicrosoftExcel')win.Dialog.Button.click()本文采用自定义的方式thread类,线程启动后,会自动执行run()函数,完成上述操作。具体代码如下,注意构造函数中的两个参数:title需要抓取的弹窗标题,例如Excel中默认弹窗的标题为MicrosoftExcel区间监听的频率,即每秒检查一次intervalself._stop_event=Event()defstop(self):self._stop_event.set()@propertydefis_running(self):returnnotself._stop_event.is_set()defrun(self):whileself.is_running:try:time.sleep(self._interval)self._close_msgbox()exceptExceptionase:print(e,flush=True)def_close_msgbox(self):'''ClosethedefaultExcelMsgBoxwithtitle"MicrosoftExcel".'''win=Application(backend="win32").connect(title=self._title)win.Dialog.Button.click()if__name__=='__main__':t=MsgBoxListener('MicrosoftExcel',3)t.start()time.sleep(10)t.stop()所以,整个过程分为三步:启动子线程监听弹窗,在主线程中打开Excel,启动自动运行,关闭子线程importxlwingsasxwfromlistenerimportMsgBoxListener#startlistenthreadlistener=MsgBoxListener('MicrosoftExcel',3)listener.start()#mainprocessasbeforewb=xw.Book('test.xlsm')msg=wb.sheets('Sheet1').range('A1').valueprint(msg)wb.close()#stoplistenerthreadlistener.stop()基本解决了这个问题,本地运行效果完全达到预期,但是我真正的需求是以系统服务的形式对服务端的Excel文件进??行自动化测试。后来发现在使用系统服务运行的时候,pywinauto是抓不到弹窗的!这可能是pywinauto[^4]的潜在问题。对于win32gui的解决方案,我们不得不求助于相对低级的win32gui。幸运的是,以上问题都得到了完美的解决。win32gui是pywin32库的一部分,所以实际的安装命令是:pipinstallpywin32整个方案和前面的描述完全一样,只是把MsgBoxListener类中关闭弹窗的方法换成:importwin32gui,win32conf_close_msgbox(self):#findthetopwindowbytitlehwnd=win32gui.FindWindow(None,self._title)ifnothwnd:return#findchildbuttonh_btn=win32gui.FindWindowEx(hwnd,None,'Button',None)ifnoth_btn:return#showtexttext=win32gui.GetWindowText(h_btn)打印(文本)#clickbuttonwin32gui.PostMessage(h_btn,NMW.win32con.None,None)time.sleep(0.2)win32gui.PostMessage(h_btn,win32con.WM_LBUTTONUP,None,None)time.sleep(0.2)更一般的方案更一般的,当有同时带有默认标题和自定义标题的弹窗,不方便使用标题方式进行抓取。例如MsgBox"Messagewithdefaulttitle.",vbInformation,MsgBox"MessagewithtitleMyApp1",vbInformation,"MyApp1",MsgBox"MessagewithtitleMyApp2",vbInformation,"MyApp2",然后扩大搜索范围,点击所有包含确定性描述的按钮(如好的,是的,确认)关闭弹出窗口。同样,替换MsgBoxListener类的_close_msgbox()方法(构造函数中不再需要title参数):def_close_msgbox(self):'''Clickanybutton("OK","Yes"or"Confirm")toclosemessagebox.'''#gethandlesofalltopwindows_windows=[]win32gui.EnumWindows(lambdahWnd,param:param.append(hWnd),h_windows)#checheachwindowforh_windowinh_windows:#getchildbuttonwithtextOK,YesorConfirmofgivenwindowh_btn=win32gui.FindWindowEx(h_window,None,'Button',Nonenifnoth):#continue_checkbuttontexttext=win32gui.GetWindowText(h_btn)ifnottext.lower()in('ok','yes','confirm'):continue#clickbuttonwin32gui.PostMessage(h_btn,win32con.WM_LBUTTONDOWN,无,无)time.sleep(0.2)win32图形用户界面。PostMessage(h_btn,win32con.WM_LBUTTONUP,None,None)time.sleep(0.2)终于,实例演示全文结束,再也不用担心意外弹窗了。[^1]:在MicrosoftExcel中处理VBA弹出消息框[^2]:尝试在xlwings中捕获MsgBox文本并按下按钮[^3]:什么是pywinauto[^4]:远程执行指南
