Bob's Blog

Web开发、测试框架、自动化平台、APP开发、机器学习等

返回上页首页

Python自动化操作windows桌面软件



在做自动化测试时,我们常常是面对网页或者手机app或者接口,不过我们也可以了解下如何自动化模拟用户操作windows桌面软件,这样在遇到这方面需求时不用着急。

目前使用python时可以用pywinauto来操作windows桌面软件的元素,达到类似selenium操作网页的效果。很久前还用过winguiauto。另外还可以使用pywin32自己封装相应方法,利用windows接口是比较便捷的,当然缺点就在于文档晦涩参数太多。

在windows上,基本原理是获取窗口或控件的句柄(handle)来达到唯一标示的目的,同时鼠标和键盘事件可以用来辅助,比如发送回车键tab键等。windows是由很多接口的,我们可以操作接口来做很多事情比如运行程序切换窗口,pywinauto也封装了pywin32的接口来发送windows事件比如WM_CLICK。

不过需要注意的是,windows软件自动化并没有web自动化那么稳定,当我们在使用电脑的同时也在跑脚本的话就可能导致脚本失败,这点比不了selenium。因为使用时有时切换了窗口焦点,少许情况下鼠标和键盘事件也会受到影响,另外有些行为需要真实的显示(real screen)因此远程运行脚本时会需要额外的工作。一般在跑windows软件的自动化时,建议能有一台只用于运行脚本的电脑,维持一定的稳定性。

启动windows软件

from pywinauto.application import Application

app = Application().start("notepad.exe")  #可以直接运行命令启动的程序
app = Application().start(r"c:\Program Files\Test.exe")  #可以指定可执行文件的路径

Application()这里默认是backend="win32", 也可以改为uia比如Application(backend="uia"), 两者的区别在于你需要自动化的软件类型:

  • Win32 API (backend="win32") - a default backend for now

    • MFC, VB6, VCL, simple WinForms controls and most of the old legacy apps
  • MS UI Automation (backend="uia")

    • WinForms, WPF, Store apps, Qt5, browsers

    Notes: Chrome requires --force-renderer-accessibility cmd flag before starting. Custom properties and controls are not supported because of comtypes Python library restrictions.

如果我们自己用windows接口来启动了程序,也可以使用connect连接相应的程序:

app = Application().connect(title="This is a new title")  # 也可以用process,handle等

控件是在窗口中,因此在操作控件时需要切换到不同的窗口上。

edit_window = app.window(title="Client Data")  # 也可以用title_re,class_name, class_name_re,有re的代表正则匹配

对于元素也可以分为按钮、文本、单选框、输入框等等,都有click、select、check等方法,类似selenium。

edit_window = app.window(title="Client Data")
edit_window.FirstName.type_keys("first name", with_spaces=True)
edit_window["Last Name"].set_text("last name")
edit_window.NextButton.click()
edit_window["manage link"].click_input(double=True)
edit_window["gender select"].select("male")

上面几个例子可以看出调用的不同方式,可以直接跟控件的名字,但是有些是有空格或字符的那么可以用类似字典取值的方式。

type_keys相当于追加值,set_text则相当于清空再输入。

而click和click_input容易被混淆,有些是标准的button,那么WM_CLICK的信息是可以生效的,有些不是button不接受这样的windows事件,于是用了click_input模拟了鼠标的移动和真实操作。这也代表了一种容易被影响的情况,并需要real screen。

如果我们想知道当前窗口的能识别的控件有哪些,分别叫什么名字,可以用

edit_window.print_control_identifiers()

如果窗口下还有子窗口,可以用child_window,比如

mini_menu = edit_window.child_window(auto_id="pnlstartmenu", control_type="System.Windows.Forms.Panel")

pywinauto也封装了很多wrapper,比如ListViewWrapper,我们操作listview时可以如下

country_item = edit_window.ListView.get_item(3)

for item in edit_window.ListView.items():
    if item.text() == "Year Range":
        item.click_input()
        current_year_range = item.text()

pywinauto中也有类似selenium的等待机制,有wait和wait_not用于等待窗口和元素的相应状态,状态有:exists / visible / enabled / ready / active。

edit_windows.wait("ready", timeout=30, retry_interval=0.5)
edit_windows.FirstName.wait_not("visible", timeout=30, retry_interval=0.5)

更多的方法可以查询官方文档:https://pywinauto.readthedocs.io/en/latest/contents.html

关于控件类型和支持的操作则可以参考:https://pywinauto.readthedocs.io/en/latest/controls_overview.html

在最后还是想推荐大家偶尔使用pywin32,直接调用windows接口,比如查找窗口发送消息可以用win32gui,点击和键盘事件可以用win32con

class Win32Helper:

    @classmethod
    def find_window(cls, window_name):
        try:
            duration = 10
            handle = False
            while duration:
                handle = win32gui.FindWindow(0, window_name)
                if handle != 0:
                    break
                duration -= 1
                time.sleep(1)
            return handle
        except Exception as e:
            message = "fail to find window by name: {0} with error: {1}".format(window_name, str(e))
            print(message)
            raise

    @classmethod
    def click_button(cls, handle):
        win32gui.PostMessage(handle, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, 0)
        win32gui.PostMessage(handle, win32con.WM_LBUTTONUP, win32con.MK_LBUTTON, 0)

    @classmethod
    def input_text(cls, handle, text, append=False):
        if append:
            win32gui.SendMessage(handle, win32con.EM_SETSEL, -1, 0)
        else:
            win32gui.SendMessage(handle, win32con.EM_SETSEL, 0, -1)
        win32gui.SendMessage(handle, win32con.EM_REPLACESEL, True, text)

    @classmethod
    def send_keyboard_signal(cls, window_handle, key):
        assert window_handle, "window handle cannot be None"
        assert key, "key of keyboard cannot be None"
        self.Keys = {"Tab": win32con.VK_TAB,
                     "Down": win32con.VK_DOWN,
                     "Enter": win32con.VK_RETURN,
                     "Up": win32con.VK_UP
                     }
        win32api.SendMessage(window_handle, win32con.WM_KEYDOWN, cls.Keys[key], 0)

 

下一篇:  Python管理远程windows
上一篇:  Django restframework加Vue打造前后端分离的网站(十七)封装和注册自定义组件

共有0条评论

添加评论

暂无评论