键鼠操作
YouQu
键鼠操作模块集成了多个键鼠操作的方案:PyAutoGUI
、Xdotool
、wayland_autotool
;
有同学肯定要问,我之前就只用 PyAutoGUI
也都挺好的,好像用不到这么多键鼠操作的东西吧;
任何模块当然是希望越简洁通用越好,但问题是没有一种方案是通用的,它们都有自己存在的问题或者说不适用的场景,如果你还没有遇到,只能说使用的场景还不够多。
常规键鼠操作
YouQu
的键鼠操作模块主要有两个:MouseKey
、ShortCut
分别是 键鼠的基本操作模块 和 快捷键模块,基本上你能用到的键鼠操作,这里都包含了,在用例里面只需要通过方法层的类对象直接调用即可,比如:
python
from apps.autotest_deepin_music.widget.deepin_music_widget import DeepinMusicWidget
DeepinMusicWidget.click()
DeepinMusicWidget.ctrl_c()
当然你也可以直接通过 src
导入使用:
python
from src import Src
Src.click()
Src.ctrl_c()
再或者直接导入键鼠模块使用:
python
from src.mouse_key import MouseKey
from src.shortcut import ShortCut
MouseKey.click()
ShortCut.ctrl_c()
我们推荐第一种使用方法,因为你写用例层肯定是会导入方法层出口类的,你不需要有额外的导入代码即可使用到所有的方法。
特殊场景键鼠操作
一些特殊场景下,无法使用上述的键鼠工具,比如在注销登录界面(没有进入系统),调用上述方法会报错,我们提供了另外一种解决方案:ydotool
python
from src.mouse_key import MouseKey
MouseKey.press_key("enter", _ydotool=True)
此方案用到的场景不多,目前仅对 press_key
方法加入了此功能,如果还需要其他的操作方法,请提 issues
或 PR
;
方法明细
python
#!/usr/bin/env python3
# _*_ coding:utf-8 _*_
# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
# SPDX-License-Identifier: GPL-2.0-only
import os
import sys
from time import sleep
from src import logger
from src.cmdctl import CmdCtl
os.environ["DISPLAY"] = ":0"
from setting.globalconfig import GlobalConfig
class HiddenPrints:
def __enter__(self):
self._original_stdout = sys.stdout
sys.stdout = open(os.devnull, "w")
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stdout.close()
sys.stdout = self._original_stdout
with HiddenPrints():
if GlobalConfig.IS_WAYLAND:
# pylint: disable=ungrouped-imports
from src.depends.pyautogui import _pyautogui_wayland as pyautogui
from src.depends.pyautogui._pyautogui_wayland import popen
else:
from src.depends import pyautogui
pyautogui.FAILSAFE = False
# pyautogui.PAUSE = 1
class MouseKey:
"""
鼠标和键盘的常用操作
"""
__author__ = "Mikigo <huangmingqiang@uniontech.com>, Litao <litaoa@uniontech.com>"
MOUSE = {1: pyautogui.PRIMARY, 2: pyautogui.MIDDLE, 3: pyautogui.RIGHT}
@classmethod
def screen_size(cls):
"""
获取屏幕大小
:return: width, height
"""
width, height = pyautogui.size()
logger.debug(f"获取屏幕分辨率 {width}*{height}")
return width, height
@classmethod
def current_location(cls, out_log=True):
"""
获取当前鼠标位置
:return: 鼠标当前的坐标
"""
position = pyautogui.position()
if out_log:
logger.debug(f"当前鼠标坐标 {position}")
return position
@classmethod
def move_to(cls, _x, _y, duration=0.4):
"""
移动到指定位置
:param _x: x
:param _y: y
:param duration:移动的速度
:return:
"""
logger.debug(f"鼠标移动至 ({_x, _y}, 速度:{duration})")
pyautogui.moveTo(int(_x), int(_y), duration=duration)
@classmethod
def move_rel(cls, _x, _y, duration=0.4):
"""
相对移动到位置
:param _x:
:param _y:
:param duration:
:return:
"""
logger.debug(f"鼠标移动相对坐标位置 ({_x, _y}), 速度:{duration}")
pyautogui.moveRel(xOffset=int(_x), yOffset=int(_y), duration=duration)
@classmethod
def click(cls, _x=None, _y=None, _type="pyautogui"):
"""
点击鼠标左键
:param _x:
:param _y:
:param _type: 使用 PyAutoGUI or Xdotool 点击
:return:
"""
logger.debug(f"点击坐标 {(_x, _y) if _x else cls.current_location(out_log=False)}")
if _type == "pyautogui":
pyautogui.click(x=_x, y=_y)
else:
if GlobalConfig.IS_WAYLAND:
pyautogui.click(x=_x, y=_y)
else:
CmdCtl.run_cmd(f"xdotool mousemove {_x} {_y} click 1")
@classmethod
def move_rel_and_click(cls, _x, _y):
"""
move relative and click
:param _x:
:param _y:
:return:
"""
cls.move_rel(_x, _y)
cls.click()
@classmethod
def middle_click(cls):
"""
单击鼠标滚轮中间
"""
logger.debug("单击鼠标滚轮中间")
pyautogui.middleClick()
@classmethod
def right_click(cls, _x=None, _y=None):
"""
单击鼠标右键
:param _x:
:param _y:
:return:
"""
logger.debug(f"鼠标右键坐标 {(_x, _y) if _x else cls.current_location(out_log=False)}")
pyautogui.rightClick(x=_x, y=_y)
sleep(1)
@classmethod
def double_click(cls, _x=None, _y=None, interval=0.3):
"""
双击鼠标左键
:param _x:
:param _y:
:param interval: 两次点击的间隔,默认 0.3s
:return:
"""
logger.debug(f"鼠标左键双击坐标 {(_x, _y) if _x else cls.current_location(out_log=False)}")
pyautogui.doubleClick(x=_x, y=_y, interval=interval)
# CmdCtl.run_cmd(f"xdotool mousemove {_x} {_y} click --repeat 2 1")
sleep(1)
@classmethod
def triple_click(cls, _x=None, _y=None):
"""
三击鼠标左键
:param _x:
:param _y:
:return:
"""
logger.debug(f"鼠标三连击坐标 {(_x, _y) if _x else cls.current_location(out_log=False)}")
pyautogui.tripleClick(x=_x, y=_y, interval=0.3)
sleep(1)
@classmethod
def drag_to(cls, _x, _y, duration=0.4, delay=1):
"""
拖拽到指定位置(绝对位置)
:param _x: 拖拽到的位置x
:param _y: 拖拽到的位置y
:param duration: 拖拽的时长
:param delay: 拖拽后等待的时间
:return:
"""
logger.debug(f"鼠标从当前位置拖拽到坐标 ({_x, _y})")
# 默认duration=0.4时,拖拽到_y<=95不生效,将拖拽的时长调整为0.9,
# 至于为啥_y<=95且duration<=0.8 时不生效,目前还不清楚,先解决问题
if _y <= 95:
duration = 0.9
pyautogui.dragTo(x=int(_x), y=int(_y), duration=duration, mouseDownUp=True)
sleep(delay)
@classmethod
def drag_rel(cls, _x, _y):
"""
按住鼠标左键,拖拽到指定位置(相对位置)
:param _x: 拖拽的相对位置x,正数向右,负数向左
:param _y: 拖拽的相对位置y,正数向下,负数向上
:return:
"""
logger.debug(f"鼠标从当前位置拖拽到相对坐标 ({_x, _y})")
pyautogui.dragRel(xOffset=int(_x), yOffset=int(_y), duration=0.4, mouseDownUp=True)
sleep(1)
@classmethod
def mouse_down(cls, _x=None, _y=None, button=1):
"""
按住鼠标键不放
:param _x:
:param _y:
:param button: 1 左键, 2 中键, 3 右键
:return:
"""
logger.debug(
f"在坐标 {(_x, _y) if _x else cls.current_location(out_log=False)} "
f"处按住鼠标{['左', '中', '右'][button - 1]}键不放"
)
pyautogui.mouseDown(x=_x, y=_y, button=cls.MOUSE.get(button, pyautogui.PRIMARY))
@classmethod
def mouse_up(cls, button=1):
"""
松开鼠标左键
:param button: 1 左键, 2 中键, 3 右键
:return:
"""
logger.debug(f"松开鼠标{['左', '中', '右'][button - 1]}键")
pyautogui.mouseUp(button=cls.MOUSE.get(button, pyautogui.PRIMARY))
sleep(1)
@classmethod
def mouse_scroll(cls, amount_of_scroll, duration=1):
"""
滚动鼠标滚轮,the_amount_of_scroll为传入滚轮数,正数为向上,负数为向下
:param amount_of_scroll: 滚轮数
:param duration:
:return:
"""
pyautogui.scroll(amount_of_scroll)
if amount_of_scroll > 0:
direct = "上"
else:
direct = "下"
logger.debug(f"向<{direct}>滑动滚轮")
sleep(duration)
@classmethod
def input_message(
cls,
message,
delay_time: int = 300,
interval: [int, float] = 0.2,
wayland_shift: bool = False,
_ydotool: bool = False,
):
"""
输入字符串
:param message: 输入的内容
:param delay_time: 延迟时间
:param interval:
:return:
"""
logger.debug(f"输入字符串<{message}>")
message = str(message)
def check_chinese():
for _ch in message:
if "\u4e00" <= _ch <= "\u9fff":
return True
return False
if GlobalConfig.IS_X11:
if check_chinese():
CmdCtl.run_cmd(f"xdotool type --delay {delay_time} '{message}'", timeout=60)
else:
pyautogui.typewrite(message=str(message), interval=interval)
# wayland上
else:
if check_chinese():
# 复制
os.system(f"wl-copy \"{message}\"")
# 有些地方可能不支持ctrl+v粘贴,比如终端,需要使用ctrl+shift+v
_hk = ["ctrl", "v"]
if wayland_shift:
_hk.insert(1, "shift")
cls.hot_key(*_hk)
else:
for key in message:
if _ydotool:
from src import ydotool
ydotool.press(key)
else:
pyautogui.press(key, interval=interval)
input = input_message
@classmethod
def press_key(cls, key: str, interval=0.0, _ydotool: bool = False):
"""
键盘上指定的按键
:param key: 键盘按键
:param interval:
:return:
"""
logger.debug(f"点击键盘上指定的按键<{key}>, 间隔<{interval}>")
if _ydotool:
from src import ydotool
ydotool.press(key)
else:
pyautogui.press(key, interval=interval)
@classmethod
def press_key_down(cls, key: str):
"""
按住键盘按键不放
:param key: 键盘按键
:return:
"""
logger.debug(f"按下<{key}>按键")
pyautogui.keyDown(key)
@classmethod
def press_key_up(cls, key: str):
"""
放松按键
:param key: 键盘按键
:return:
"""
logger.debug(f"放松<{key}>按键")
pyautogui.keyUp(key)
@classmethod
def hot_key(cls, *args, interval=0.03):
"""
键盘组合按键操作
:param args: 键盘组合键,比如:"ctrl","alt","a"
:return:
"""
logger.debug(f"快捷键 {args}")
pyautogui.hotkey(*args, interval=interval)
@classmethod
def hot_key_down(cls, *args):
"""
组合按键按下不放
:param args:
:return:
"""
for _c in args:
if len(_c) > 1:
_c = _c.lower()
cls.press_key_down(_c)
sleep(0.03)
@classmethod
def hot_key_up(cls, *args):
"""
组合按键释放
:param args:
:return:
"""
for c in reversed(args):
if len(c) > 1:
c = c.lower()
cls.press_key_up(c)
sleep(0.03)
@classmethod
def move_to_and_click(cls, _x, _y):
"""
移动到某个位置点击
:param _x: 移动到的位置 x
:param _y: 移动到的位置 y
:return:
"""
cls.move_to(_x, _y)
cls.click()
@classmethod
def move_to_and_right_click(cls, _x, _y):
"""
移动到某个位置点击右键
:param _x: 移动到的位置 x
:param _y: 移动到的位置 y
:return:
"""
cls.move_to(_x, _y)
cls.right_click()
@classmethod
def move_to_and_double_click(cls, _x, _y):
"""
移动到某个位置点击双击
:param _x: 移动到的位置 x
:param _y: 移动到的位置 y
:return:
"""
cls.move_to(_x, _y)
cls.double_click()
@classmethod
def move_on_and_drag_to(cls, start: tuple, end: tuple):
"""
指定拖动的起始-终止坐标
:param start: 开始坐标
:param end: 终止坐标
:return:
"""
cls.move_to(*start)
sleep(1)
cls.drag_to(*end)
@classmethod
def move_on_and_drag_rel(cls, start: tuple, end: tuple):
"""
指定拖动的起始-终止坐标
:param start: 开始坐标
:param end: 终止坐标
:return:
"""
cls.move_to(*start)
sleep(1)
cls.drag_rel(*end)
@classmethod
def select_menu(cls, number: int):
"""
选择桌面右键菜单中的选项(从上到下)
:param number: 在菜单中的位置数
:return:
"""
for _ in range(number):
cls.press_key("down")
sleep(0.3)
cls.press_key("enter")
logger.debug(f"选择右键菜单中的选项(从上到下)第{number}项")
@classmethod
def reverse_select_menu(cls, number: int):
"""
选择桌面右键菜单中的选项(从下到上)
:param number: 在菜单中的位置数
:return:
"""
for _ in range(number):
cls.press_key("up")
sleep(0.3)
cls.press_key("enter")
logger.debug(f"选择右键菜单中的选项(从下到上)第{number}项")
@classmethod
def select_submenu(cls, number: int):
"""
选择右键菜单中的子菜单选项(从上到下)
:param number: 在菜单中的位置数
:return:
"""
for _ in range(1, number):
cls.press_key("down")
cls.press_key("enter")
@classmethod
def locate_all_on_screen(cls, image_path):
"""
识别所有匹配的图像
:param image_path: 图像的路径
:return: 所有匹配的位置的元组组成的列表
"""
return pyautogui.locateAllOnScreen(image_path)
@classmethod
def draw_line(cls, start_x, start_y, rel_x, rel_y):
"""
从某个坐标开始画线(框选)
:param start_x: 开始的坐标的横坐标
:param start_y: 开始的坐标的纵坐标
:param rel_x: 向量的横坐标
:param rel_y: 向量的纵坐标
:return:
"""
cls.move_to(start_x, start_y)
cls.drag_rel(rel_x, rel_y)
@classmethod
def clear(cls):
"""
清空表单
:return:
"""
logger.debug("清空表单")
cls.hot_key("ctrl", "a")
cls.press_key("backspace")