文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-03 05:20

关注

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

假想场景

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

  1. import xlwings as xw  
  2. wb = xw.Book('test.xlsm')  
  3. msg = wb.sheets('Sheet1').range('A1').value  
  4. print(msg)  
  5. wb.close() 

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

  1. Private Sub Workbook_Open()  
  2.     MsgBox "Welcome"  
  3.     MsgBox "to open"  
  4.     MsgBox "this file."  
  5. End Sub 

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

基本思路

主程序中不可能直接处理或者绕过此类问题,也不能奢望有人随时蹲守点击下一步——那就开启一个子线程来护航吧。因此,解决方案是利用子线程监听并随时关闭弹窗,直到主程序圆满结束。

解决这个问题,需要以下两个知识点(基础知识请课外学习):

pywinauto方案

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

  1. from pywinauto.application import Application  
  2. win = Application(backend="win32").connect(title='Microsoft Excel' 
  3. win.Dialog.Button.click() 

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

  1. # listener.py  
  2. import time  
  3. from threading import Thread, Event  
  4. from pywinauto.application import Application  
  5. class MsgBoxListener(Thread):  
  6.     def __init__(self, title:str, interval:int):  
  7.         Thread.__init__(self)  
  8.         self._title = title   
  9.         self._interval = interval   
  10.         self._stop_event = Event()     
  11.     def stop(self): self._stop_event.set()  
  12.     @property  
  13.     def is_running(self): return not self._stop_event.is_set()  
  14.     def run(self):  
  15.         while self.is_running:  
  16.             try:  
  17.                 time.sleep(self._interval)  
  18.                 self._close_msgbox()  
  19.             except Exception as e:  
  20.                 print(e, flush=True 
  21.     def _close_msgbox(self):  
  22.         '''Close the default Excel MsgBox with title "Microsoft Excel".'''        
  23.          win = Application(backend="win32").connect(title=self._title)  
  24.         win.Dialog.Button.click()  
  25. if __name__=='__main__': 
  26.     t = MsgBoxListener('Microsoft Excel', 3)  
  27.     t.start()  
  28.     time.sleep(10)  
  29.     t.stop() 

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

  1. import xlwings as xw  
  2. from listener import MsgBoxListener  
  3. # start listen thread  
  4. listener = MsgBoxListener('Microsoft Excel', 3)  
  5. listener.start()  
  6. # main process as before  
  7. wb = xw.Book('test.xlsm')  
  8. msg = wb.sheets('Sheet1').range('A1').value  
  9. print(msg)  
  10. wb.close()  
  11. # stop listener thread  
  12. listener.stop() 

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

win32gui方案

那就只好转向相对底层的win32gui,所幸完美解决了上述问题。

win32gui是pywin32库的一部分,所以实际安装命令是: 

  1. pip install pywin32 

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

  1. import win32gui, win32con  
  2. def _close_msgbox(self):  
  3.     # find the top window by title  
  4.     hwnd = win32gui.FindWindow(None, self._title)  
  5.     if not hwnd: return  
  6.     # find child button  
  7.     h_btn = win32gui.FindWindowEx(hwnd, None,'Button', None)  
  8.     if not h_btn: return  
  9.     # show text  
  10.     text = win32gui.GetWindowText(h_btn) 
  11.     print(text)  
  12.     # click button         
  13.     win32gui.PostMessage(h_btn, win32con.WM_LBUTTONDOWN, None, None)  
  14.     time.sleep(0.2)  
  15.     win32gui.PostMessage(h_btn, win32con.WM_LBUTTONUP, None, None)  
  16.     time.sleep(0.2) 

更一般的方案

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

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

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

  1. def _close_msgbox(self): 
  2.     '''Click any button ("OK", "Yes" or "Confirm") to close message box.'''  
  3.     # get handles of all top windows  
  4.     h_windows = []  
  5.     win32gui.EnumWindows(lambda hWnd, param: param.append(hWnd), h_windows)   
  6.     # check each window      
  7.     for h_window in h_windows:          
  8.          # get child button with text OK, Yes or Confirm of given window  
  9.         h_btn = win32gui.FindWindowEx(h_window, None,'Button', None)  
  10.         if not h_btn: continue  
  11.         # check button text  
  12.         text = win32gui.GetWindowText(h_btn)  
  13.         if not text.lower() in ('ok', 'yes', 'confirm'): continue  
  14.         # click button  
  15.         win32gui.PostMessage(h_btn, win32con.WM_LBUTTONDOWN, None, None)  
  16.         time.sleep(0.2)  
  17.         win32gui.PostMessage(h_btn, win32con.WM_LBUTTONUP, None, None)  
  18.         time.sleep(0.2) 

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

[^1]: Handling VBA popup message boxes in Microsoft Excel

[^2]: Trying to catch MsgBox text and press button in xlwings

[^3]: What is pywinauto

[^4]: Remote Execution Guide 

 

来源:Python中文社区 (ID:python-china)内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯