| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- # 定义抽象类
- import json
- import logging
- import tempfile
- from abc import ABC, abstractmethod
- from typing import List
- import tkinter as tk
- from tkinter import ttk
- import uiautomator2 as u2
- from uiautomator2.exceptions import LaunchUiAutomationError
- class BaseControl(ABC):
- # ctx_dict = {
- # 'connect': 1,
- # 'platform': 2,
- # }
- # 连接信息
- connect_dict = {
- }
- app_pkg = {
- 'com.niocpeed.dna': 'DeepCoin',
- }
- def __init__(self, name: str, ctx: int = 2):
- """
- 软件UI交互的控制器
- :param name:
- :param ctx:
- """
- # 名称
- self.name = name
- self.ctx = ctx
- pass
- def init_adb(self, commands: List[str], *args, **kwargs):
- """
- 连接设备
- 使用 python shell执行命令
- """
- pass
- def devices_list(self):
- """
- 获取设备列表 adb devices
- :return:
- """
- pass
- def print_log(self, msg):
- logging.info(f'打印信息 {msg}')
- def connect_adb(self, serial: str):
- """
- 连接设备
- """
- logging.info(f'连接设[备{self.name}]:{serial}', )
- try:
- d = u2.connect(serial)
- # self.enable_click_monitor()
- d.shell('settings put system pointer_location 1')
- # d.debug = True
- return d
- except LaunchUiAutomationError as e:
- print(f"uiautomator2 连接失败: {e}")
- def re_connect(self):
- """
- 重新建立连接
- :return:
- """
- pass
- # def enable_click_monitor(self):
- # """
- # 启用点击监控,在屏幕上显示点击位置
- # """
- # # self.d.settings['operation_delay'] = (0.5, 0.5) # 增加操作延迟以便观察
- # # self.d.debug = True
- # self.d.toast.show('点击监控已启用') # 显示提示
- #
- # # 确保有悬浮窗权限
- # # self.d.set_fastinput_ime(True) # 启用ime
- # # self.d.show_float_window(True) # 显示悬浮窗
- #
- # # 可选:打开开发者选项中的"指针位置"
- # self.d.shell('settings put system pointer_location 1')
- def loadding(self, top, func):
- """
- 等待加载完成
- :return:
- """
- # 创建加载指示器
- loading_window = tk.Toplevel(top)
- loading_window.title("连接中")
- loading_window.geometry("200x100")
- loading_window.transient(top) # 设置为模态
- loading_window.grab_set() # 设置为模态
- loading_window.overrideredirect(True) # 移除窗口边框和按钮
- # 相对于父窗口居中显示
- window_width = loading_window.winfo_reqwidth()
- window_height = loading_window.winfo_reqheight()
- parent_x = top.winfo_rootx()
- parent_y = top.winfo_rooty()
- parent_width = top.winfo_width()
- parent_height = top.winfo_height()
- position_right = parent_x + (parent_width - window_width) // 2
- position_down = parent_y + (parent_height - window_height) // 2
- loading_window.geometry(f"+{position_right}+{position_down}")
- loading_label = ttk.Label(loading_window, text="连接中...", padding=20)
- loading_label.pack(expand=True)
- def thread_task():
- try:
- func()
- finally:
- # 在主线程中销毁加载指示器
- top.after(0, lambda: [loading_window.grab_release(), loading_window.destroy()])
- import threading
- thread = threading.Thread(target=thread_task)
- # thread.daemon = True # 设置为守护线程,这样主程序退出时线程会自动结束
- thread.start()
- pass
- class UIControl(ABC):
- @abstractmethod
- def event_1(self):
- """
- F1 开仓界面,仓位滑竿百分比(30-60)
- """
- pass
- @abstractmethod
- def event_2(self):
- """
- F2 确认开仓,开多 限价职中间数值
- 检查撤销限价委托单
- """
- pass
- @abstractmethod
- def event_3(self):
- """
- F3 平仓界面 平空,仓位滑竿百分比(90-100)确认开仓,限价职中间数值
- """
- pass
- @abstractmethod
- def event_4(self):
- """
- F4 确认平仓 平多 限价取中间数值 检查撤销限价委托单
- """
- pass
- @abstractmethod
- def event_5(self):
- """
- F5 开仓界面 仓位滑竿百分比(30-60)
- """
- pass
- @abstractmethod
- def event_6(self):
- """
- F6 确认开仓, 开空 限价值中间数值
- 检查撤销限价委托单
- """
- pass
- @abstractmethod
- def event_7(self):
- """
- F7 平仓界面二仓位滑竿百分比(90-100)
- """
- pass
- @abstractmethod
- def event_8(self):
- """
- F8 确认平仓, 平空 限价职中间数值
- 检查撤销限价委托单
- """
- pass
- class AbsControl(UIControl):
- def __init__(self, serial: str, ctx: dict, *args, **kwargs):
- """
- 用于平台交互的控制器
- :param name: 当前名称
- :param ctx: 控制器上下文
- :param args:
- :param kwargs:
- """
- # u2.logger.setLevel(logging.DEBUG)
- self.serial = serial
- self.ctx = ctx
- self.info = ctx['info']
- #
- self.d = ctx['d']
- # 屏幕高度
- self.height = 0
- # 屏幕宽度
- self.width = 0
- self._points = {}
- self._func = []
- self._log_func = None
- def prevent_sleep(self):
- """
- 防止设备休眠
- """
- self.d.wake()
- self.d.screen_on()
- self.d.unlock()
- def screenshot(self):
- """
- 截图
- """
- # 获取系统/tmp路径
- tmp = tempfile.gettempdir()
- return self.d.screenshot(f"{tmp}/{self.serial.replace(':', '_')}.png")
- def to_top_swipe(self, sleep=0.1, times=2):
- """
- 滑动到屏幕最顶部,通过多次滑动确保到达顶部
- :param sleep: 滑动后等待时间
- :param times: 滑动次数
- :return:
- """
- width, height = self.get_screen_size()
- # 循环滑动直到无法继续滑动
- for _ in range(times):
- self.d.swipe(width // 2, height * 0.8, width // 2, height) # 向上滑动
- # 短暂等待确保滑动完成
- self.d.sleep(sleep)
- def click_point(self, x: int, y: int):
- """
- 点击指定坐标
- :param x: x坐标
- :param y: y坐标
- """
- self.d.click(x, y)
- def click_xpath(self, xpath: str):
- """
- 点击指定xpath
- """
- el = self.d.xpath(xpath).get()
- if el:
- el.click()
- else:
- logging.warning(f"未找到元素: {xpath}")
- def to_next_swipe(self, sleep=0.1):
- """
- 向上滑动一屏
- """
- width, height = self.get_screen_size()
- # 从屏幕下方向上滑动到顶部
- self.d.swipe(width // 2, height * 0.8, width // 2, height * 0.2) # 向上滑动
- # 短暂等待确保滑动完成
- self.d.sleep(sleep)
- def get_screen_size(self):
- """
- 获取屏幕尺寸,宽度和高度
- """
- self.width, self.height = self.d.window_size()
- logging.info(f"屏幕尺寸: {self.width}x{self.height}")
- return self.width, self.height
- def drag_slider_ext(self, start_x: int, start_y: int, end_x: int, end_y: int, steps: int = 50):
- """
- 精确拖动滑块
- :param start_x: 起始x坐标
- :param start_y: 起始y坐标
- :param end_x: 结束x坐标
- :param end_y: 结束y坐标
- :param steps: 步数,值越大滑动越平滑
- """
- self.d.swipe_ext(start_x, start_y, end_x, end_y, steps)
- def add_point(self, point=None):
- """
- 添加坐标点
- point = {
- "name": "",
- "x": 0,
- "y": 0,
- "desc": "",
- "xpath": ""
- }
- """
- logging.info("保存坐标点: %s", point)
- self._points[point['name']] = point
- def print_log(self, msg):
- """
- 打印日志
- """
- logging.info(f'>>111>{msg}')
- print(f'>222>>{msg},{self._log_func}')
- if self._log_func:
- self._log_func(msg)
- def add_func(self, func):
- """
- 添加坐标点采集函数
- """
- self._func.append(func)
- def set_log_func(self, func):
- """
- 添加日志函数
- """
- self._log_func = func
- def save_point(self):
- """
- 保存坐标
- """
- self.to_top_swipe()
- self.d.sleep(2)
- for func in self._func:
- func()
- __tmp__ = {}
- for k, v in self._points.items():
- __tmp__[k] = v
- with open(self.point_path, "w", encoding="utf-8") as f:
- json.dump(__tmp__, f, ensure_ascii=False, indent=4)
- def get_point(self, info: dict):
- """
- 获取坐标点
- :param info: 数据结构
- {
- "name": "btn_mairu_kaiduo",
- "desc": "买入/开多",
- "xpath": '//*[@content-desc="买入/开多"]'
- }
- :return: x, y, el
- """
- x, y, el = self.get_point_by_xpath(info['xpath'])
- self.add_point({
- "name": info['name'],
- "x": x,
- "y": y,
- "desc": info['desc'],
- "xpath": info['xpath'],
- })
- return x, y, el
- def get_point_by_xpath(self, xpath: str):
- """
- :param xpath: xpath
- 获取坐标第一个匹配的点
- info 数据结构
- :return: x, y, el
- """
- el = self.d.xpath(xpath).get()
- x, y = el.center()
- return x, y, el
- def get_points_by_xpath(self, xpath: str):
- """
- 获取所有坐标点
- info 数据结构
- """
- els = self.d.xpath(xpath).all()
- items = []
- for el in els:
- x, y = el.center()
- items.append((x, y, el))
- return items
- def check_element_exists(self, xpath: str, timeout: float = 0.05) -> bool:
- """
- 判断指定XPath的元素是否存在
- :param xpath: 元素的XPath
- :param timeout: 超时时间(秒)
- :return: 是否存在
- """
- self.d.sleep(timeout)
- return self.d.xpath(xpath).exists
- def input_xpath(self, xpath: str, text: str, clear: bool = True):
- """
- 给指定xpath的输入框输入文本
- :param xpath: 输入框的xpath
- :param text: 要输入的文本
- :param clear: 输入前是否清空原有内容
- """
- element = self.d.xpath(xpath).get()
- if element:
- if clear:
- element.text.set_text("") # 清空原有内容
- element.set_text(text) # 输入新内容
- else:
- logging.warning(f"未找到输入框: {xpath}")
- def input_by_position(self, x: int, y: int, text: str, clear: bool = True):
- """
- 通过坐标点击并在输入框中输入文本
- :param x: 输入框x坐标
- :param y: 输入框y坐标
- :param text: 要输入的文本
- :param clear: 是否清空原有内容
- """
- self.click_point(x, y) # 先点击获取焦点
- if clear:
- self.d.clear_text() # 清空原有内容
- self.d.send_keys(text) # 输入新文本
- @abstractmethod
- def event_1(self):
- """
- F1 开仓界面,仓位滑竿百分比(30-60)
- """
- pass
- @abstractmethod
- def event_2(self):
- """
- F2 确认开仓,开多 限价职中间数值
- 检查撤销限价委托单
- """
- pass
- @abstractmethod
- def event_3(self):
- """
- F3 平仓界面 平空,仓位滑竿百分比(90-100)确认开仓,限价职中间数值
- """
- pass
- @abstractmethod
- def event_4(self):
- """
- F4 确认平仓 平多 限价取中间数值 检查撤销限价委托单
- """
- pass
- @abstractmethod
- def event_5(self):
- """
- F5 开仓界面 仓位滑竿百分比(30-60)
- """
- pass
- @abstractmethod
- def event_6(self):
- """
- F6 确认开仓, 开空 限价值中间数值
- 检查撤销限价委托单
- """
- pass
- @abstractmethod
- def event_7(self):
- """
- F7 平仓界面二仓位滑竿百分比(90-100)
- """
- pass
- @abstractmethod
- def event_8(self):
- """
- F8 确认平仓, 平空 限价职中间数值
- 检查撤销限价委托单
- """
- pass
|