使用pywin32制作Windows自动化工具

使用pywin32制作Windows自动化工具

最近在实验室用ENVI标了一批图,好多操作都是机械的重复的,于是想有没有可能做一个自动化的小工具来简化操作。查了一些资料发现pywin32应该是可以。于是先尝试简单做一个小例子练练手。

这篇做的是打开记事本,输入helloworld,选择编码格式为utf-8并保存。

使用了spy++工具来辅助

程序的启动

这里有两种写法,用下面哪一句都可

def start_app(path):
    win32api.ShellExecute(0, 'open', path,'','',1)
    # os.startfile(path)  # 也可用这条

窗口的定位

每一个窗口都有唯一的一个handle,调用win32API的时候,这个handle是操作每一个窗口和控件必须的参数。

要找到窗口的handle,需要两个函数

  • FindWindow(lpClassName=None, lpWindowName=None):
    • 描述:自顶层窗口(也就是桌面)开始搜索条件匹配的窗体,并返回这个窗体的句柄。不搜索子窗口、不区分大小写。找不到就返回0
    • 参数:
      lpClassName:字符型,是窗体的类名,这个可以在Spy++里找到。
      lpWindowName:字符型,是窗口名,也就是标题栏上你能看见的那个标题。
    • 说明:这个函数我们仅能用来找主窗口。
  • FindWindowEx(hwndParent=0, hwndChildAfter=0, lpszClass=None, lpszWindow=None);
    • 描述:搜索类名和窗体名匹配的窗体,并返回这个窗体的句柄。不区分大小写,找不到就返回0。
    • 参数:
      hwndParent:若不为0,则搜索句柄为hwndParent窗体的子窗体。
      hwndChildAfter:若不为0,则按照z-index的顺序从hwndChildAfter向后开始搜索子窗体,否则从第一个子窗体开始搜索。
      lpClassName:字符型,是窗体的类名,这个可以在Spy++里找到。
      lpWindowName:字符型,是窗口名,也就是标题栏上你能看见的那个标题。
    • 说明:找到了主窗口以后就靠它来定位子窗体啦。

参考https://blog.csdn.net/bluehawksky/article/details/44957627

首先要获取主窗口的handle

notepad_path = r'C:\Windows\notepad.exe'
start_app(notepad_path)
while True:
    win = win32gui.FindWindow('Notepad','')
    if win != 0:
        win32api.Sleep(200)
        break

这里用了一个循环是因为程序启动需要时间,后面出现有延迟的地方,都是在实际测试的时候,有时候窗口没有完全打开导致查询不到窗口。

封装了几个函数,可以直接调用

def printHandle(func):
    def wrapper(*args, **kw):
        handle = func(*args, **kw)
        print('%x' % (handle))
        return handle
    return wrapper


def find_idxSubHandle(pHandle, winClass, index=0):
    """
    已知子窗口的窗体类名
    寻找第index号个同类型的兄弟窗口
    """
    assert type(index) == int and index >= 0
    handle = win32gui.FindWindowEx(pHandle, 0, winClass, None)
    while index > 0:
        handle = win32gui.FindWindowEx(pHandle, handle, winClass, None)
        index -= 1
    return handle


def find_subHandle(pHandle, winClassList):
    """
    递归寻找子窗口的句柄
    pHandle是祖父窗口的句柄
    winClassList是各个子窗口的class列表,父辈的list-index小于子辈
    """
    assert type(winClassList) == list
    if len(winClassList) == 1:
        return find_idxSubHandle(pHandle, winClassList[0][0], winClassList[0][1])
    else:
        pHandle = find_idxSubHandle(pHandle, winClassList[0][0], winClassList[0][1])
        return find_subHandle(pHandle, winClassList[1:])


@printHandle
def find_handle_by_wndlist(pHandle, winClassList):
    return find_subHandle(pHandle, winClassList)

说明:printHandle是一个装饰器,打印出返回的handle,结合spy++对比返回的窗口是不是我们想要的,主要用来调试。

这里结合spy++举个例子
比如我们想定位记事本的输入区域
使用pywin32制作Windows自动化工具_第1张图片
打开spy++,右键刷新一下窗口,点击工具栏的望远镜,弹出窗口搜索,把靶心标志的图标拖到记事本的编辑区域,即返回句柄标题类等信息。
使用pywin32制作Windows自动化工具_第2张图片
点击确定,定位出该窗口的位置
使用pywin32制作Windows自动化工具_第3张图片
外面的“无标题-记事本”是最外面的窗口,Edit是他的一个子窗口

定位这个窗口就这样写就可以了,返回Edit窗口的handle

edit_handle = find_handle_by_wndlist(win, [('Edit', 0)])

如果有链式的关系就像这样写:

save_name_handle = find_handle_by_wndlist(save_win, [('DUIViewWndClassName',0),('DirectUIHWND',0),\
                                                         ('FloatNotifySink',0),('ComboBox',0),('Edit',0)])

tuple中第一个参数是窗口的类名,第二个参数是寻找窗口时,如果有相同类名的窗口,这些窗口之间就有一个顺序,从0开始编,用不同的数字区分这些窗口。

往编辑区填内容

win32这些几乎所有的操作都是通过消息,消息的参数太复杂了,我就把常用到的操作封装起来了

def fill_content(handle, content):
    win32gui.SendMessage(handle, win32con.WM_SETTEXT, None, content)

这句的handle填Edit窗口的句柄,content是要输入的内容

操作菜单

使用pywin32制作Windows自动化工具_第4张图片
需要几个函数:

  • GetMenu(handle)
    返回菜单栏的handle,参数填主窗口的handle,返回菜单栏的handle
  • GetSubMenu(menu_handle, index)
    子菜单,menu_handle填GetMenu返回的handle,或者上一级的handle
  • GetMenuItemID(menu_handle, index)
    获取菜单栏中某一项的handle,是接下来传递消息时需要的参数

封装了一个函数:

@printHandle
def get_menu_item(handle, posilist):
    menu_handle = win32gui.GetMenu(handle)
    for index in posilist:
        if index is posilist[-1]:
            cmd_ID = win32gui.GetMenuItemID(menu_handle, index)
            break
        menu_handle = win32gui.GetSubMenu(menu_handle, index)
    return cmd_ID

handle是主窗口的handle,posilist是描述菜单栏中某一项的位置的,比如前面那张图,另存为的位置是[0, 3]。注意:如果有分割线,分割线也是计数的

这个函数这样使用:

saveas_posilist = [0, 3]
saveas_handle = get_menu_item(win, saveas_posilist)

打开菜单栏中某一项(另存为)

使用这个函数

def open_menu_item(handle, menu_item_id):
    win32gui.PostMessage(handle, win32con.WM_COMMAND, menu_item_id, 0)

handle是主窗口的handle,menu_item_id是上一步返回的结果

open_menu_item(win, saveas_handle)

模拟点击

点击确定按钮的时候会用到,下面两个函数测试过都可以

def leftClick(handle):
    win32gui.SendMessage(handle, win32con.WM_LBUTTONDOWN, None, None)
    win32gui.SendMessage(handle, win32con.WM_LBUTTONUP, None, None)


def send_click_msg(Mhandle, confirmBTN_handle):
    win32api.SendMessage(Mhandle, win32con.WM_COMMAND, 1, confirmBTN_handle)

操作comboBox

使用pywin32制作Windows自动化工具_第5张图片
这里选择utf-8编码,具体操作参考了前面提到那篇文章,比较复杂,封装起来直接用:

def select_comboBox_item(PCB_handle, CB_handle, select_index):
    if win32api.SendMessage(CB_handle, win32con.CB_SETCURSEL, select_index, 0) == select_index:
        win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, win32con.CBN_SELENDOK & 16+0, CB_handle)  # 控件的ID是0,所以低位直接加0
        win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, win32con.CBN_SELCHANGE & 16+0, CB_handle)
    else:
        raise Exception("Change saving type failed")

这里PCB_handle是comboBox的主窗口的handle,CB_handle是comboBox的handle,select_index是定位选项的数

这里参考的文章写的PCB_handle是父窗口,我实际填进去的是主窗口(最外面的那一层,也就是“另存为”所在窗口),运行没有问题

完整程序

import win32gui
import win32con
import win32api


def printHandle(func):
    def wrapper(*args, **kw):
        handle = func(*args, **kw)
        print('%x' % (handle))
        return handle
    return wrapper


def find_idxSubHandle(pHandle, winClass, index=0):
    """
    已知子窗口的窗体类名
    寻找第index号个同类型的兄弟窗口
    """
    assert type(index) == int and index >= 0
    handle = win32gui.FindWindowEx(pHandle, 0, winClass, None)
    while index > 0:
        handle = win32gui.FindWindowEx(pHandle, handle, winClass, None)
        index -= 1
    return handle


def find_subHandle(pHandle, winClassList):
    """
    递归寻找子窗口的句柄
    pHandle是祖父窗口的句柄
    winClassList是各个子窗口的class列表,父辈的list-index小于子辈
    """
    assert type(winClassList) == list
    if len(winClassList) == 1:
        return find_idxSubHandle(pHandle, winClassList[0][0], winClassList[0][1])
    else:
        pHandle = find_idxSubHandle(pHandle, winClassList[0][0], winClassList[0][1])
        return find_subHandle(pHandle, winClassList[1:])


@printHandle
def find_handle_by_wndlist(pHandle, winClassList):
    return find_subHandle(pHandle, winClassList)


def fill_content(handle, content):
    win32gui.SendMessage(handle, win32con.WM_SETTEXT, None, content)


def start_app(path):
    win32api.ShellExecute(0, 'open', path,'','',1)
    # os.startfile(path)  # 也可用这条


def select_comboBox_item(PCB_handle, CB_handle, select_index):
    if win32api.SendMessage(CB_handle, win32con.CB_SETCURSEL, select_index, 0) == select_index:
        win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, win32con.CBN_SELENDOK & 16+0, CB_handle)  # 控件的ID是0,所以低位直接加0
        win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, win32con.CBN_SELCHANGE & 16+0, CB_handle)
    else:
        raise Exception("Change saving type failed")


@printHandle
def get_menu_item(handle, posilist):
    menu_handle = win32gui.GetMenu(handle)
    for index in posilist:
        if index is posilist[-1]:
            cmd_ID = win32gui.GetMenuItemID(menu_handle, index)
            break
        menu_handle = win32gui.GetSubMenu(menu_handle, index)
    return cmd_ID


def open_menu_item(handle, menu_item_id):
    win32gui.PostMessage(handle, win32con.WM_COMMAND, menu_item_id, 0)


def leftClick(handle):
    win32gui.SendMessage(handle, win32con.WM_LBUTTONDOWN, None, None)
    win32gui.SendMessage(handle, win32con.WM_LBUTTONUP, None, None)


def send_click_msg(Mhandle, confirmBTN_handle):
    win32api.SendMessage(Mhandle, win32con.WM_COMMAND, 1, confirmBTN_handle)


if __name__ == '__main__':
    save_path = r'd:\test.txt'
    notepad_path = r'C:\Windows\notepad.exe'
    start_app(notepad_path)
    saveas_posilist = [0, 3]
    while True:
        win = win32gui.FindWindow('Notepad','')
        if win != 0:
            win32api.Sleep(200)
            break
    edit_handle = find_handle_by_wndlist(win, [('Edit', 0)])
    fill_content(edit_handle, 'hello world')
    saveas_handle = get_menu_item(win, saveas_posilist)
    open_menu_item(win, saveas_handle)
    while True:
        if win32gui.FindWindow(None, '另存为')!=0:
            win32api.Sleep(2000)
            break
        else:
            win32api.Sleep(200)
    save_win = win32gui.FindWindow(None, '另存为')
    save_name_handle = find_handle_by_wndlist(save_win, [('DUIViewWndClassName',0),('DirectUIHWND',0),\
                                                         ('FloatNotifySink',0),('ComboBox',0),('Edit',0)])
    fill_content(save_name_handle, save_path)
    comboBox_handle = find_handle_by_wndlist(save_win, [('ComboBox', 0)])
    select_comboBox_item(save_win,comboBox_handle,3)
    save_btn_handle = find_handle_by_wndlist(save_win, [('Button', 0)])
    send_click_msg(save_win, save_btn_handle)

运行结果:打开新的文本文档,输入hello world,保存为utf-8编码,存在d:\test.txt

你可能感兴趣的:(Python,技术解决问题)