步骤/目录:
无

本文首发于个人博客https://lisper517.top/index.php/archives/4259/,转载请注明出处。
本文的原文写作日期为2020年10月22日,主要目的是用Python重构按键精灵。

主体程序,主要是一些功能的实现,具体应用需要自己写。写的比较乱:

import win32gui, win32api, win32con, win32ui
import time
from time import sleep
import random
from random import uniform
import logging
from logging import info, warning, error
from config import VK_CODE
import copy
import cv2 as cv
import traceback

# mode为test意为调试模式,real则为实际应用
# In 'test' mode, it will print some hidden info for Debug
mode = 'test'

# 模式图的存储路径
# The save path of pattern pics
pattern_path = 'yourpath\\'

# 设置logging的书写格式
# Setting logging configs
logging.basicConfig(
    level=logging.DEBUG,
    format=
    '%(lineno)d : %(asctime)s : %(levelname)s : %(funcName)s : %(message)s',
    filename='yourpath\\log{}.txt'.format(
        time.strftime('-%Y-%m-%d')),
    filemode='w')

# 用当前时间初始化随机数
# Set random seed
random.seed(time.time())


def Get_PosAndHwnd(hwnd=0, ClassName=None, TitleName=None):
    '''接受句柄、类名、标题名,返回第一个匹配的窗口位置和句柄。
        Accepts Handle, ClassName or TitleName of a window, returns the rect-pos of the first window that matches.
        
        Args:
            hwnd: 目标句柄 target handle.
            ClassName: 目标窗口类名 target ClassName.
            TitleName: 目标窗口标题 target TitleName.
        
        Returns: x1, y1, x2, y2, hwnd
            前四个参数描述窗口在显示器中的位置,hwnd为句柄。
            x1-y2 describes the pos of the window in screen, last arg is the handle.
    '''
    try:
        if hwnd == 0:
            hwnd = win32gui.FindWindow(ClassName, TitleName)
        x1, y1, x2, y2 = win32gui.GetWindowRect(hwnd)
        info('找到窗口,句柄:{},标题名:{},类名:{},坐标:{}'.format(
            hwnd, win32gui.GetWindowText(hwnd), win32gui.GetClassName(hwnd),
            (x1, y1, x2, y2)))
        return x1, y1, x2, y2, hwnd
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('Get_PosAndHwnd 出现错误')


def Activate_Hwnd(hwnd):
    '''激活指定句柄的窗口。每次转换时使用。
        Activates the window with the matching hwnd.

        Args:
            hwnd: 目标句柄 target handle.
        
        Returns:
            No return.
    '''
    try:
        win32gui.SendMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0)
        # WA_CLICKACTIVE为通过鼠标激活,WA_ACTIVE为鼠标以外的东西(如键盘)激活,WA_INACTIVE为取消激活
        # WA_CLICKACTIVE means activate by mouse, WA_ACTIVE means other equipments( like keyboard).WA_INACTIVE means inactivate it.
        info('激活句柄为 {} 标题为 {} 的窗口'.format(hwnd, win32gui.GetWindowText(hwnd)))
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('激活窗口 出现错误')


def LeftClick(hwnd, x1, y1, times='first', platform='PC'):
    '''左键单击(自带延时) Left Click the window (located by hwnd) once (with human-like delay). 

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            x1, y1: 需要点击的坐标 target pos. 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').

        Returns:
            No return.
    '''
    try:
        if platform == 'PC':  # 对于桌面版 # for PC
            tmp_pos = win32api.MAKELONG(x1, y1)
        #elif flag == 'AM': # 对于模拟器 # for Android manipulator
        #click_pos = win32gui.ScreenToClient(hwnd, (x1, y1))
        # 注意坐标是整个窗口还是客户区的,用ScreenToClient转换 # switch between screen/client pos
        #tmp_pos = win32api.MAKELONG(click_pos[0], click_pos[1])
        if times == 'first':
            Activate_Hwnd(hwnd)
        win32gui.SendMessage(hwnd, win32con.WM_LBUTTONDOWN,
                             win32con.MK_LBUTTON, tmp_pos)
        # 鼠标点击格式为SendMessage(hWnd,WM_LBUTTONDOWN,fwKeys,MAKELONG(xPos,yPos));
        # fwKeys可以取:MK_CONTROL、MK_LBUTTON、MK_MBUTTON、MK_RBUTTON、MK_SHIFT等,多个值用|隔开
        sleep(uniform(0.05, 0.08))
        win32gui.SendMessage(hwnd, win32con.WM_LBUTTONUP, 0000, tmp_pos)
        # 抬起时有些电脑上fwKeys显示为0000,有些可能为MK_LBUTTON
        # Use spy++ for details
        sleep(uniform(0.07, 0.095))
        info('对句柄 {} 标题 {} 的坐标 {} 进行 左键单击 动作'.format(
            hwnd, win32gui.GetWindowText(hwnd), (x1, y1)))
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('左键单击 出现错误')


def LeftClick_sequence(hwnd, points, times='second', platform='PC'):
    '''左键依序单击(依次点击多个点) Click a sequence of points in order.

        Args: 
            hwnd: 目标窗口的句柄 target hwnd. 
            points: 需要点击的坐标 target points in the form like ((x1,y1),(x2,y2)...). 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').
        
        Returns:
            No return.
    '''
    try:
        if times == 'first':
            Activate_Hwnd(hwnd)
        for point in points:
            LeftClick(hwnd, point[0], point[1], 'second', platform)
        info('对句柄 {} 标题 {} 的坐标 {} 进行 左键依序单击 动作'.format(
            hwnd, win32gui.GetWindowText(hwnd), points))
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('左键单击 出现错误')


def LeftDoubleClick(hwnd, x1, y1, times='first', platform='PC'):
    '''左键同一位置双击 Click the same pos twice.

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            x1, y1: 需要点击的坐标 target pos. 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').

        Returns:
            No return.
    '''
    try:
        if times == 'first':
            Activate_Hwnd(hwnd)
        info('左键同一位置双击--第一次点击')
        LeftClick(hwnd, x1, y1, 'second', platform)
        info('左键同一位置双击--第二次点击')
        LeftClick(hwnd, x1, y1, 'second', platform)
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('左键同一位置双击 出现错误')


def LeftDifDoubleClick_Rect(hwnd,
                            x1,
                            y1,
                            times='first',
                            platform='PC',
                            delta=(0, 4)):
    '''左键不同位置双击,范围为双正方形 Left click 2 pos, in a double-square-like area.
        _________
        |  ___  |
        | |   | |
        | |___| |
        |_______|
        (x1, y1) is at the center, (x2, y2) between the squares (borders included).

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            x1, y1: 第一次点击的坐标 first pos to click. 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').
            delta: 第二个点的横坐标在x1+delta[0] 到 x1+delta[1]间(包括端点),纵坐标同样。
                x2 varies between x1+delta[0] to x1+delta[1], y2 the same (both ends included). 

        Returns:
            No return.
    '''
    try:
        if times == 'first':
            Activate_Hwnd(hwnd)
        info('左键正方形双击--第一次点击')
        LeftClick(hwnd, x1, y1, 'second', platform)
        point_list = []
        for delta_x in range(delta[0], delta[1] + 1):
            for delta_y in range(delta[0], delta[1] + 1):
                point_list.append((x1 + delta_x, y1 + delta_y))
                point_list.append((x1 + delta_x, y1 - delta_y))
                point_list.append((x1 - delta_x, y1 + delta_y))
                point_list.append((x1 - delta_x, y1 - delta_y))
        point_list = list(set(point_list))
        point_list.remove((x1, y1))
        x1, y1 = random.sample(point_list, 1)[0]
        info('左键正方形双击--第二次点击')
        LeftClick(hwnd, x1, y1, 'second', platform)
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('左键正方形双击 出现错误')


def LeftDifDoubleClick_Cir(hwnd, x1, y1, times='first', platform='PC', r=4):
    '''左键不同位置双击,范围为圆形。 Left Click 2 pos in a circle.
        (x1, y1)为圆心,r为半径,(x2, y2)在圆内及圆上。
        (x1, y1) is the center, r the radius. Generates (x2, y2) in the circle (or on the border).

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            x1, y1: 第一次点击的坐标 first pos to click. 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').
            r: 圆的半径。
                radius. 
        
        Returns:
            返回两次点击的坐标(x1,y1,x2,y2)。
            Returns 2 clicked pos in (x1,y1,x2,y2). 
    '''
    try:
        if times == 'first':
            Activate_Hwnd(hwnd)
        info('左键圆形双击--第一次点击')
        LeftClick(hwnd, x1, y1, 'second', platform)
        m, n = x1, y1
        point_list = []
        final_point_list = []
        for delta_x in range(r + 1):
            for delta_y in range(r + 1):
                point_list.append((delta_x, delta_y))
        point_list.remove((0, 0))
        for i in range(len(point_list)):
            x = point_list[i][0]
            y = point_list[i][1]
            if x * x + y * y <= r * r:
                final_point_list.append((x1 + x, y1 + y))
                final_point_list.append((x1 + x, y1 - y))
                final_point_list.append((x1 - x, y1 + y))
                final_point_list.append((x1 - x, y1 - y))
        final_point_list = list(set(final_point_list))
        x1, y1 = random.sample(final_point_list, 1)[0]
        info('左键圆形双击--第二次点击')
        LeftClick(hwnd, x1, y1, 'second', platform)
        return (m, n, x1, y1)
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('左键圆形双击 出现错误')


def Random_Areas_LeftClicks(hwnd,
                            click_areas,
                            times='first',
                            platform='PC',
                            r=4,
                            click_strategy=1):
    '''从几种点击策略中选一种执行,在一定范围对目标窗口进行点击。
        Select a click strategy/combination, click the window in some given areas.  
        ---------   单个点击区域如右。A single click_area looks like left. 
        |  ---- |   如果只点击一次或者点击两次中的第一个坐标,该坐标位于小矩形中(包括边界)。
        |  |  | |   If only click once, or the first click in 2 clicks, it locates in the minor rect (borders included).
        |  |  | |   小矩形的每条边距大矩形距离为r个像素点。
        |  |  | |   Each border of the minor rect is r-pixel far from the outside rect.
        |  ---- |
        ---------
        第二次点击的区域不会超出大矩形的范围(包括边界))。对于一个点击区域[x1,y1,x2,y2],其中的小矩形为[x1+r,y1+r,x2-r,y2-r]。
        The second is in the big rect (borders included). For a click_area like (x1,y1,x2,y2), the minor rect is [x1+r,y1+r,x2-r,y2-r]. 

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            click_areas: 可选的点击范围(矩形)。格式为[[x1,y1,x2,y2],[x3,y3,x4,y4]...],只有一个区域则可以写成[x1,y1,x2,y2]。
                Possible rect-areas to click, given in the form of [[x1,y1,x2,y2],[x3,y3,x4,y4]...]. 
                If only one area, [x1,y1,x2,y2] is also acceptable. 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').
            r: 圆的半径。
                radius. 
            click_strategy: 选择的点击策略。看看源码,很容易写自己的策略。主要包括点击方式和概率。
                A strategy for clicking, includes combination of clicking functions and its weights. 
                Look at the src code, easy to construct your own click_strategy. 
        
        Returns:
            返回两次点击的坐标(x1,y1,x2,y2)。如果只点击一次,返回(x1, y1, -1, -1)
            Returns 2 clicked pos in (x1,y1,x2,y2). If only clicked once, return (x1, y1, -1, -1). 
    '''
    try:
        # 缩小边界,并加上一个值,表示区域点的坐标个数(包含边界)
        total = 0
        areas = copy.deepcopy(
            click_areas
        )  # python中传列表参是浅拷贝,为了防止改变原click_areas需要写这一句。传字符串、数字是深拷贝
        if isinstance(areas[0], int):
            areas = [areas]
        for i in range(len(areas)):
            areas[i][0] = areas[i][0] + r
            areas[i][1] = areas[i][1] + r
            areas[i][2] = areas[i][2] - r
            areas[i][3] = areas[i][3] - r
            areas[i].append((areas[i][2] - areas[i][0] + 1) *
                            (areas[i][3] - areas[i][1] + 1))
            total = total + areas[i][4]
        rand = random.randint(0, total - 1)
        # 找出第一次点击位置落在哪个区域,并找出具体坐标
        # find out the first click is in which area, and the exact pos.
        x = 0
        y = 0
        for i in range(len(areas)):
            rand = rand - areas[i][4]
            if rand < 0:
                rand = rand + areas[i][4]
                w = areas[i][2] - areas[i][0] + 1
                x = rand % w + areas[i][0]
                y = rand // w + areas[i][1]  # 现在python中整型之间用/也会得到float
                break
        if click_strategy == 1:  # 1号策略,包括左键单击、左键同一位置双击、左键圆形双击三种
            #总结果数为173,左键单击:左键同一位置双击:左键圆形双击 的比例为 131:29:13
            rand = random.randint(1, 173)
            if times == 'first':
                Activate_Hwnd(hwnd)
            if rand <= 131:  # weighs 131
                info('随机区域左键 选择 左键单击,坐标{}'.format((x, y)))
                LeftClick(hwnd, x, y, 'second', platform)
                return (x, y, -1, -1)
            elif rand >= 161:  # weighs 13
                info('随机区域左键 选择 左键圆形双击,第一次点击坐标{}'.format((x, y)))
                return LeftDifDoubleClick_Cir(hwnd, x, y, 'second', platform,
                                              r)
            else:  # weighs 29
                info('随机区域左键 选择 左键同位置双击,坐标{}'.format((x, y)))
                LeftDoubleClick(hwnd, x, y, 'second', platform)
                return (x, y, x, y)
        elif click_strategy == 2:  # 2号策略,仅包括左键单击,用于只能单击的情况
            if times == 'first':
                Activate_Hwnd(hwnd)
            info('随机区域左键 选择 左键单击,坐标{}'.format((x, y)))
            LeftClick(hwnd, x, y, 'second', platform)
            return (x, y, -1, -1)
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('随机区域左键 出现错误')


def press_key(hwnd, key='esc', times='second'):
    '''单次按键(自带延时) Press a key once (with human-like delay).

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            key: 需要按的键,详见config.VK_CODE。
                The key to press, see also in config.VK_CODE.
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
        
        Returns:
            No return. 

    '''
    try:
        if times == 'first':
            Activate_Hwnd(hwnd)
        win32api.SendMessage(hwnd, win32con.WM_KEYDOWN, VK_CODE[key],
                             0)  # 暂时将lParam写为0
        # 这里不知道是否要暂停
        # I don't konw whether to pause here.
        time.sleep(0.01)
        win32api.SendMessage(hwnd, win32con.WM_CHAR, VK_CODE[key], 0)
        sleep(uniform(0.05, 0.08))
        win32api.SendMessage(hwnd, win32con.WM_KEYUP, VK_CODE[key], 0)
        sleep(uniform(0.07, 0.095))
        info('对句柄 {} 标题 {} 的窗口 按 {} 键一次'.format(hwnd,
                                                win32gui.GetWindowText(hwnd),
                                                key))
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('单次按键 出现错误')


def press_keys(hwnd, keys, times='second'):
    '''依序按键 Press a sequence of keys in order.

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            keys: 需要按的键,详见config.VK_CODE。
                Keys to press, see also in config.VK_CODE.
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
        
        Returns:
            No return. 
    '''
    try:
        if times == 'first':
            Activate_Hwnd(hwnd)
        for key in keys:
            press_key(hwnd, key, 'second')
        info('对句柄 {} 标题 {} 的窗口 依序按 {} 键'.format(hwnd,
                                                win32gui.GetWindowText(hwnd),
                                                keys))
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('依序按键 出现错误')


def ClientRect_PrtSc(hwnd, area=None, filename=''):
    '''根据句柄、截图位置和图片路径,对窗口的客户区截图并存到指定位置。
        Press PrtSc for a window's client area, save the pic in the indicated path. 
        从https://blog.csdn.net/zhuisui_woxin/article/details/84345036和https://www.cnblogs.com/Evan-fanfan/p/11097850.html改编
        Made some changes and corrected some errors from 2 links above (took me some time to figure the errors).

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            area: 需要的区域。为None时表示整个客户区。
                Needed area. If None, means get the whole client area. 
            filename: 指定路径。为''时表示文件名根据hwnd生成。
                Indicate the path. If '', means 'path\{}.bmp'.format.  
        
        Returns:
            No return. 
    '''
    try:
        hwnd = hwnd
        if filename == '':
            filename = 'yourpath\\{}.bmp'.format(
                hwnd)
        hwndDC = win32gui.GetDC(
            hwnd)  # 获取窗口的设备上下文Device Context。GetWindowDC包括了非客户区,而GetDC仅为客户区
        # GetWindowDC also gets the non-client area, while GetDC only gets the client area.
        mfcDC = win32ui.CreateDCFromHandle(hwndDC)  # 获取mfcDC
        saveDC = mfcDC.CreateCompatibleDC()  # 创建可兼容DC
        saveBitMap = win32ui.CreateBitmap()  # 创建bitmap以保存图片
        '''MonitorDev = win32api.EnumDisplayMonitors(
            None, None)  # 获取显示器信息,枚举显示器,笔记本据说可能有问题'''
        x1, y1, x2, y2 = win32gui.GetClientRect(
            hwnd)  #GetClientRect获取客户区窗口位置,GetWindowRect获取整个窗口的位置信息
        x, y, w, h = (0, 0, 0, 0)
        if area == None:
            x = 0
            y = 0
            w = x2 - x1
            h = y2 - y1
        else:
            x, y, m, n = area
            w = m - x
            h = n - y
        saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)  # 为bitmap开辟空间
        # 对saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)的理解:
        # 1.mfc相当于一个虚拟屏幕。这里的参数w和h决定了这个屏幕的大小。
        # 2.屏幕的初始状态是黑色,每个坐标都是#000000
        # 3.之前有mfcDC = win32ui.CreateDCFromHandle(hwndDC),又有hwndDC = win32gui.GetDC(hwnd)
        #   mfcDC和hwnd窗口之间建立了某种关联,可以将hwnd窗口中的图像放到虚拟屏幕上
        saveDC.SelectObject(saveBitMap)  # 将截图保存到saveBitMap中
        saveDC.BitBlt((0, 0), (w, h), mfcDC, (x, y), win32con.SRCCOPY)
        # 对saveDC.BitBlt(坐标1, (w, h), mfcDC, 坐标2, win32con.SRCCOPY)的理解:
        # BitBlt的功能大概是把从hwnd窗口截到的图放到虚拟屏幕上,信息转入saveDC。
        # 1.坐标1是针对窗口截图的,指定截图放在黑色背景上的位置(指定左上角)
        # 2.w和h窗口截图的长宽,而坐标2指定了开始截图的位置
        #   这两个参数决定了从hwnd窗口的哪里截图、截多大的图
        # 3.mfcDC已经和hwnd窗口建立了关联,所以不需要指定虚拟屏幕从哪个窗口获得截图
        # 4.SRCCOPY意为将截图直接拷贝到虚拟屏幕中
        # 接下来的saveBitMap.SaveBitmapFile(saveDC, filename)则是对虚拟屏幕截图并保存到指定位置
        # Google for more info.
        saveBitMap.SaveBitmapFile(saveDC, filename)
        # 清除数据
        # GetDC一类的需要用ReleaseDC释放,CreateDC一类的用DeleteDC释放,DeleteObject则删除一个逻辑笔、画笔、字体、位图、区域或者调色板
        win32gui.DeleteObject(saveBitMap.GetHandle())
        saveDC.DeleteDC()
        mfcDC.DeleteDC()
        win32gui.ReleaseDC(hwnd, hwndDC)
        info('对句柄 {} 标题 {} 的窗口截图并保存'.format(hwnd,
                                            win32gui.GetWindowText(hwnd)))
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('客户区截图 出现错误')


def G(pattern_filename):
    '''将pattern的文件名加上路径前缀与类型后缀并返回
        Add path prefix and '.bmp' for a pattern pic file and return it. 

        Args:
            pattern_filename: 模式图片名。Pattern pic file name. 
        
        Returns: 
            opencv2 可用的路径。The usable path for opencv2. 
    '''
    try:
        global pattern_path  # 在全局变量中 # a global variable.
        return pattern_path + pattern_filename + '.bmp'
    except:
        global mode
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('加模式图路径 出现错误')


def Find_Pic(hwnd,
             pattern,
             return_pos='l',
             find_pattern_in_area=None,
             filename=''):
    '''寻找图片,找到则返回小图在大图中的位置坐标及模板图大小,否则返回(-1,-1,tw,th)。
        Find the pattern pic in the target pic. Return the location and pattern's size if found, or (-1,-1,pattern-weight,pattern-height) if not.
        https://blog.csdn.net/wz2671/article/details/102751549和https://www.cnblogs.com/ssyfj/p/9271883.html很有帮助。
        Above 2 links help. 

        Args:
            hwnd: 目标窗口的句柄 target hwnd. 
            pattern: 模式图,小图。
                The pic to be found, pattern pic, smaller pic. 
            return_pos: 为'l'时返回找到的左上角坐标,'c'则返回中心(偏左上)。
                If 'l', return the left-top corner, 'c' the centor pos (if odd, lefter and topper). 
            find_pattern_in_area: 指定在大图的哪个区域查找。None表示在全部区域中找。
                In which area of the bigger pic to find. None means the whole area. 
            filename: 指定路径。为''时表示文件名根据hwnd生成。
                Indicate the bigger-pic's path. If '', means 'path\{}.bmp'.format.  
        
        Returns:
            如果找到,以(x, y, pat_w, pat_h)形式返回小图在大图中的位置;没找到则返回(-1, -1, pat_w, pat_h)。
            The location and pattern's size in the form of (x, y, pat_w, pat_h) if found, or (x, y, pat_w, pat_h) if not.
    '''
    try:
        hwnd = hwnd
        ClientRect_PrtSc(hwnd, find_pattern_in_area, filename)
        if filename == '':  # 没有提供文件名则需要改成默认文件名
            filename = 'youpath\\{}.bmp'.format(
                hwnd)
        pat = cv.imread(pattern)  # pattern
        src = cv.imread(filename)  # source
        # imread的文件名必须带上格式后缀
        theight, twidth = pat.shape[:2]
        # 获取pattern的高和宽
        result = cv.matchTemplate(src, pat, cv.TM_SQDIFF_NORMED)
        # 执行模板匹配,采用的匹配方式cv2.TM_SQDIFF_NORMED
        # matchTemplate(大图像, 小图像(子图像、模式图像)m, 匹配方式, result 和 mask 可选)
        # 返回一个矩阵result,大小为(W-w+1)*(H-h+1),比大图像少了右、下一圈,是子图像左上顶点在大图像中可能出现的位置
        # result矩阵中的每个位置,其值表示子图像左上顶点放在该位置时,子图像与大图像上相应区域的匹配度
        # result中的点位置和大图像保持一致。
        # 关于匹配方式:()
        # 1.TM_SQDIFF是平方差匹配;TM_SQDIFF_NORMED是标准平方差匹配。利用平方差来进行匹配,最好匹配为0;匹配越差,匹配值越大。
        # 2.TM_CCORR是相关性匹配;TM_CCORR_NORMED是标准相关性匹配。采用模板和图像间的乘法操作,数越大表示匹配程度较高, 0表示最坏的匹配效果。
        # 3.TM_CCOEFF是相关性系数匹配;TM_CCOEFF_NORMED是标准相关性系数匹配。将模版对其均值的相对值与图像对其均值的相关值进行匹配,1表示完美匹配,-1表示糟糕的匹配,0表示没有任何相关性(随机序列)。
        # 总结:随着从简单的测量(平方差)到更复杂的测量(相关系数),我们可获得越来越准确的匹配(同时也意味着越来越大的计算代价)。
        min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result)
        # minMaxLoc(矩阵Mat),在一个矩阵中找出最大值、最小值、对应序号
        # 对于TM_SQDIFF_NORMED模式下最匹配的位置,如果不匹配值大于1%,就认为没有找到。
        if min_val > 0.01:
            return (-1, -1, twidth, theight)
        global mode
        if mode == 'test':
            # 以下代码绘制矩形边框,并将匹配区域标注出来,用于调试
            # codes after are used for debug, draw the big/small pic and the location.
            cv.rectangle(src, min_loc,
                         (min_loc[0] + twidth, min_loc[1] + theight),
                         (0, 0, 225), 1)  # 在src图像矩阵中加上矩形,范围是模板图的范围
            # rectangle(目标图像, 矩形的一个顶点, 矩形对角线上的另一个顶点, 矩形线条颜色, 线条粗细)返回空值
            strmin_val = str(min_val)
            print('\n匹配度' + strmin_val)
            cv.imshow("MatchResult----MatchingValue=" + strmin_val,
                      src)  # 将画上了矩形的图像在窗口中显示出来
            cv.imshow('pattern pic', pat)
            # imshow(窗口名称, 图像矩阵),在窗口中显示图像,窗口自动调整到图像大小
            # cv.imwrite('1.png', template, [int(cv.IMWRITE_PNG_COMPRESSION), 9])
            # imwrite(文件名, 图像矩阵, 格式编码) 将图像矩阵以特定编码保存,暂时用不到
            # IMWRITE_PNG_COMPRESSION为png格式,0-9为压缩级别,9为不压缩,消耗时间最少
            cv.waitKey(0)  # 等待键盘输入任意值,参数表示等待时间(ms),0表示一直等
            cv.destroyAllWindows()  # 关闭所有窗口
        if return_pos == 'c':
            x = min_loc[0] + twidth // 2
            y = min_loc[1] + theight // 2
        elif return_pos == 'l':
            x = min_loc[0]
            y = min_loc[1]
        info('在句柄 {} 标题 {} 的窗口找到模式{}'.format(hwnd,
                                             win32gui.GetWindowText(hwnd),
                                             pattern))
        return (x, y, twidth, theight)
    except:
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('找图 出现错误')


def print_hitted_points(hitted_points, hwnd, deltas):
    '''测试用函数,根据hwnd找到截图文件(不要重新截图)并标注出点击过的点。
        Used for debug, find the big pic by hwnd (No PrtSc again) and mark the clicked pos.

        Args:
            hitted_points: 点击过的点。clicked points. 
            hwnd: 用于找到大图。Used to find the big pic.
            deltas: 如果大图是客户区的一部分截图,该变量可描述大图左上角顶点在整个客户区中的位置。
                If the big pic is only part of the window, deltas would help describe big-pic's location (deltas is the left-top corner pos). 
        
        Returns:
            No return. 
    '''
    try:
        x1, y1, x2, y2 = hitted_points
        x1, x2 = x1 - deltas[0], x2 - deltas[0]
        y1, y2 = y1 - deltas[1], y2 - deltas[1]
        print('deltas = {}, '.format(deltas))
        print('hitted_points = ({},{})  ({},{})'.format(x1, y1, x2, y2))
        # 这样就得到了应该标注的坐标
        filename = 'yourpath\\{}.bmp'.format(
            hwnd)
        src = cv.imread(filename)
        if x2 < 0 and y2 < 0:
            cv.circle(src, (x1, y1), 1, (0, 0, 225), 4)
            # cv.circle(img, point, point_size, point_color, thickness)本来用来画圆的
            # 参数分别是图像矩阵、圆心、半径、线的颜色、稠度
            # 稠度可以取0、4、8,画出来点的大小不同
            cv.imshow('single one----hitted points', src)
        elif x1 == x2 and y1 == y2:
            cv.circle(src, (x1, y1), 1, (0, 0, 225), 4)
            cv.imshow('double same----hitted points', src)
        else:
            cv.circle(src, (x1, y1), 1, (0, 0, 225), 4)
            cv.circle(src, (x2, y2), 1, (0, 0, 225), 4)
            cv.imshow('double diff----hitted points', src)
        cv.waitKey(0)
        cv.destroyAllWindows()
    except:
        traceback.print_exc()


def FindPic_RandomLeftClick(hwnd,
                            pattern,
                            max_try=20,
                            time_interval=0.5,
                            click_strategy=1,
                            times='first',
                            click_areas=None,
                            find_pattern_in_area=None,
                            r=4,
                            platform='PC',
                            click_pos='l',
                            filename=''):
    '''从指定的句柄窗口寻找图片,若最大次数内找到则执行随机区域左键点击,否则返回False。返回点击的坐标(x1,y1,x2,y2),若只点击了一次则返回(x1,y1,-1,-1)。
        这是一个整合程度高函数,可能要看一下其中用到的其他函数。
        Find a pattern pic in the hwnd window, if not found after max_try, return False; if found, operate a click_strategy, and return clicked pos. 
        If clicked only once, return (x1,y1,-1,-1). 
        This func is highly intergrated. Turn to some called sub-funcs if confused. 

        Args in detail:
            hwnd: 要截图并点击的窗口句柄。The window to PrtSc and click. 
            pattern: 要寻找的模式小图。The pattern-pic's path to be found. 
            max_try: 指定最多寻找的次数。If not found, after max_try, it will let go. 
            time_interval指定每次寻找的间隔时间(实际查找间隔比该时间多一点)。
                Interval between each try (a little longer than given, in fact).
            click_strategy表示点击策略,详见Random_Areas_LeftClicks函数。
                Indicate the click strategy, see also in Random_Areas_LeftClicks(). 
            times: 'first'表示从其他窗口转过来点击,用于调用Activate_Hwnd(hwnd)。
                If 'first', means target window is inactive/unfocused, thus call Activate_Hwnd(hwnd). 
            click_areas: 可选的点击范围(矩形)。格式为[[x1,y1,x2,y2],[x3,y3,x4,y4]...],只有一个区域则可以写成[x1,y1,x2,y2]。为None时表示就点模式小图所在的区域。
                Possible rect-areas to click, given in the form of [[x1,y1,x2,y2],[x3,y3,x4,y4]...]. 
                If only one area, [x1,y1,x2,y2] is also acceptable. 
                If None, means to click the area where pattern pic is found. 
            find_pattern_in_area: 指定在窗口客户区的哪个区域寻找模式图,为None时表示在整个客户区寻找。
                Which rect-area of the client area of the window to find the pattern pic. If None, means find in the whole area of the client area.
                find_pattern_in_area从技术上是只截取客户区该区域的图实现的。
                If not None, technically, it works by only PrtSc the partly area of the window's client area. 
            r: 指示对指定的点击区域,应该内收多少个像素点。对于高或宽<4的模式图,可能需要自己把r设置成更小的值。
                Shrink r pixels for each click_area. For pat-pic smaller than 4 pixels, adjust r to a smaller value. 
            platform: 目前无用。'PC'表示当前操作的是桌面版,不是安卓模拟器('AM')。
                Useless now. 'PC' means target window is PC, not Android manipulator ('AM').
            click_pos: 可取值'l'或'c',l表示left_top,后者表示center。详见Find_Pic()。
                Possible values are 'l' and 'c'. See also in Find_Pic(). 
                用于指示在窗口中找到模式图后,是返回其左上坐标还是中心点坐标(模式图长宽为奇数时返回值偏左上)。
                'l' means left-top corner pos, 'c' means center pos, used by Find_Pic(). 
                现在暂时只用'l'模式,'c'模式可能用于以后功能拓展。
                For now 'l' is enough, 'c' may help expand furthur functions. 
            filename: 手动指定大图文件,从该文件中查找模式图。一般用不到。
                Indicate the big-pic's path manually, not used in ordinary applications.  
        
        Returns:
            若最大次数后仍未找到模式图则返回False。
            若找到模式图,返回点击的坐标(x1,y1,x2,y2),若只点击了一次则返回(x1,y1,-1,-1)。
            If pattern pic not found after max_try, return False;
            if found, return clicked pos in (x1,y1,x2,y2). 
            Return (x1,y1,-1,-1) if only clicked once. 
    '''
    try:
        global mode
        x, y = -1, -1
        deltas = (
            -1, -1
        )  # 这是截图区域与整个客户区的左上顶点的相对位置。用于测试 # Used for debug, in print_hitted_points().
        for i in range(max_try):
            x, y, twidth, theight = Find_Pic(hwnd, pattern, 'l',
                                             find_pattern_in_area, filename)
            if x == -1 and y == -1:
                if mode == 'test':
                    print('\r第{}次没找到'.format(i + 1), end='')
                sleep(time_interval)
                continue
            else:
                if find_pattern_in_area == None:  # 若使用默认的全客户区查找,返回的坐标是相对于整个客户区的
                    delta_x, delta_y = 0, 0
                else:  # 指定了查找区域,则返回的坐标是相对于查找区域的,需要转换一下
                    delta_x, delta_y = find_pattern_in_area[
                        0], find_pattern_in_area[1]
                if click_areas == None:  # 不给出点击区域,则默认点击区域是窗口找到模式图片的区域
                    click_areas = [[
                        x + delta_x, y + delta_y, x + twidth + delta_x,
                        y + theight + delta_y
                    ]]
                if mode == 'test':
                    deltas = (delta_x, delta_y)
                break
        if x == -1 and y == -1:
            info('未能找到并随机点击:对句柄 {} 标题 {} 的窗口未找到模式{},等待了{}次*{}秒'.format(
                hwnd, win32gui.GetWindowText(hwnd), pattern, max_try,
                time_interval))
            return False
        else:
            if mode == 'test':  # 在测试模式下,画出点击了窗口上的哪些点
                hitted_points = Random_Areas_LeftClicks(
                    hwnd, click_areas, times, platform, r, click_strategy)
                print_hitted_points(hitted_points, hwnd, deltas)
                return hitted_points
            elif mode == 'real':
                return Random_Areas_LeftClicks(hwnd, click_areas, times,
                                               platform, r, click_strategy)
    except:
        if mode == 'test':
            traceback.print_exc()
        elif mode == 'real':
            error('找图并随机左键点击 出现错误')


if __name__ == '__main__':
    try:
        click_areas = [292, 175, 314, 192]
        find_pattern_in_area = [44, 82, 245, 313]  # [44, 82, 100, 150]
        print(FindPic_RandomLeftClick(3212840, G('pattern1'),
                                      click_strategy=2))
    except:
        traceback.print_exc()

配置文件:

# 键盘码
VK_CODE = {
    'backspace': 0x08,
    'tab': 0x09,
    'clear': 0x0C,
    'enter': 0x0D,
    'shift': 0x10,
    'ctrl': 0x11,
    'alt': 0x12,
    'pause': 0x13,
    'caps_lock': 0x14,
    'esc': 0x1B,
    'spacebar': 0x20,
    'page_up': 0x21,
    'page_down': 0x22,
    'end': 0x23,
    'home': 0x24,
    'left_arrow': 0x25,
    'up_arrow': 0x26,
    'right_arrow': 0x27,
    'down_arrow': 0x28,
    'select': 0x29,
    'print': 0x2A,
    'execute': 0x2B,
    'print_screen': 0x2C,
    'ins': 0x2D,
    'del': 0x2E,
    'help': 0x2F,
    '0': 0x30,
    '1': 0x31,
    '2': 0x32,
    '3': 0x33,
    '4': 0x34,
    '5': 0x35,
    '6': 0x36,
    '7': 0x37,
    '8': 0x38,
    '9': 0x39,
    'a': 0x41,
    'b': 0x42,
    'c': 0x43,
    'd': 0x44,
    'e': 0x45,
    'f': 0x46,
    'g': 0x47,
    'h': 0x48,
    'i': 0x49,
    'j': 0x4A,
    'k': 0x4B,
    'l': 0x4C,
    'm': 0x4D,
    'n': 0x4E,
    'o': 0x4F,
    'p': 0x50,
    'q': 0x51,
    'r': 0x52,
    's': 0x53,
    't': 0x54,
    'u': 0x55,
    'v': 0x56,
    'w': 0x57,
    'x': 0x58,
    'y': 0x59,
    'z': 0x5A,
    'numpad_0': 0x60,
    'numpad_1': 0x61,
    'numpad_2': 0x62,
    'numpad_3': 0x63,
    'numpad_4': 0x64,
    'numpad_5': 0x65,
    'numpad_6': 0x66,
    'numpad_7': 0x67,
    'numpad_8': 0x68,
    'numpad_9': 0x69,
    'multiply_key': 0x6A,
    'add_key': 0x6B,
    'separator_key': 0x6C,
    'subtract_key': 0x6D,
    'decimal_key': 0x6E,
    'divide_key': 0x6F,
    'F1': 0x70,
    'F2': 0x71,
    'F3': 0x72,
    'F4': 0x73,
    'F5': 0x74,
    'F6': 0x75,
    'F7': 0x76,
    'F8': 0x77,
    'F9': 0x78,
    'F10': 0x79,
    'F11': 0x7A,
    'F12': 0x7B,
    'F13': 0x7C,
    'F14': 0x7D,
    'F15': 0x7E,
    'F16': 0x7F,
    'F17': 0x80,
    'F18': 0x81,
    'F19': 0x82,
    'F20': 0x83,
    'F21': 0x84,
    'F22': 0x85,
    'F23': 0x86,
    'F24': 0x87,
    'num_lock': 0x90,
    'scroll_lock': 0x91,
    'left_shift': 0xA0,
    'right_shift ': 0xA1,
    'left_control': 0xA2,
    'right_control': 0xA3,
    'left_menu': 0xA4,
    'right_menu': 0xA5,
    'browser_back': 0xA6,
    'browser_forward': 0xA7,
    'browser_refresh': 0xA8,
    'browser_stop': 0xA9,
    'browser_search': 0xAA,
    'browser_favorites': 0xAB,
    'browser_start_and_home': 0xAC,
    'volume_mute': 0xAD,
    'volume_Down': 0xAE,
    'volume_up': 0xAF,
    'next_track': 0xB0,
    'previous_track': 0xB1,
    'stop_media': 0xB2,
    'play/pause_media': 0xB3,
    'start_mail': 0xB4,
    'select_media': 0xB5,
    'start_application_1': 0xB6,
    'start_application_2': 0xB7,
    'attn_key': 0xF6,
    'crsel_key': 0xF7,
    'exsel_key': 0xF8,
    'play_key': 0xFA,
    'zoom_key': 0xFB,
    'clear_key': 0xFE,
    '+': 0xBB,
    ',': 0xBC,
    '-': 0xBD,
    '.': 0xBE,
    '/': 0xBF,
    '`': 0xC0,
    ';': 0xBA,
    '[': 0xDB,
    '\\': 0xDC,
    ']': 0xDD,
    "'": 0xDE,
    '`': 0xC0
}

标签: none

添加新评论