2 커밋 6972d0d189 ... 20c8e5e335

작성자 SHA1 메시지 날짜
  xiao.qiang 20c8e5e335 Merge remote-tracking branch 'origin/master' 6 달 전
  xiao.qiang f11adb19ec 更新工具类 7 달 전
18개의 변경된 파일1466개의 추가작업 그리고 188개의 파일을 삭제
  1. 35 0
      .gitignore
  2. 12 2
      README.md
  3. BIN
      docs/test-01.jpg
  4. 275 0
      gui/phone_list.py
  5. 333 0
      gui/settings.py
  6. 14 2
      main.py
  7. 162 41
      plat/base/base_control.py
  8. 19 8
      plat/deepcoin_control.py
  9. 79 0
      plat/settings_control.py
  10. 3 1
      pyproject.toml
  11. 83 83
      test/deepcoin/deepcoin_test.py
  12. 17 6
      test/deepcoin/test01.py
  13. 58 45
      test/deepcoin/test_deepcoin_control.py
  14. 158 0
      test/test-adb.py
  15. 66 0
      test/test-adb1.py
  16. 16 0
      test/test-ui-1.py
  17. 49 0
      test/test-ui-2.py
  18. 87 0
      test/test-ui-3.py

+ 35 - 0
.gitignore

@@ -126,3 +126,38 @@ crashlytics-build.properties
 /.venv/lib64/
 /poetry.lock
 /.venv/lib64/
+!/build/
+
+!/*.spec
+
+!/dist/
+
+!/poetry.lock
+/.venv/bin/f2py
+/.venv/bin/helpviewer
+/.venv/bin/img2png
+/.venv/bin/img2py
+/.venv/bin/img2xpm
+/.venv/bin/numpy-config
+/.venv/bin/py.test
+/.venv/bin/pycrust
+/.venv/bin/pyi-archive_viewer
+/.venv/bin/pyi-bindepend
+/.venv/bin/pyi-grab_version
+/.venv/bin/pyi-makespec
+/.venv/bin/pyi-set_version
+/.venv/bin/pyinstaller
+/.venv/bin/pyshell
+/.venv/bin/pyslices
+/.venv/bin/pyslicesshell
+/.venv/bin/pytest
+/.venv/bin/pywxrc
+/.venv/bin/uvicorn
+/.venv/bin/wxdemo
+/.venv/bin/wxdocs
+/.venv/bin/wxget
+/.venv/lib64/
+/mobile-tools.spec
+/poetry.lock
+/test02.spec
+/1.xml

+ 12 - 2
README.md

@@ -1,4 +1,4 @@
-# mobel-rpa
+# mobile-rpa
 ## 参考 
 [参考文档](https://developer.android.com/reference/android/support/test/uiautomator/UiSelector)
 
@@ -12,6 +12,9 @@ https://www.genymotion.com/product-desktop/download/
 ```shell
 export HTTP_PROXY=http://127.0.0.1:7899
 export HTTPS_PROXY=http://127.0.0.1:7899
+
+# window  修改编码
+# chcp 65001
 poetry install
 
 #sudo apt-get install python3-tk
@@ -64,6 +67,13 @@ point 可以 使用weditor获取到的坐标
 
 ## 打包
 ```shell
-pyinstaller  --clean  -i assets/logo/32x32.ico test/deepcoin/test02.py 
+#C:\Users\fzxs\anaconda3\envs\rpa\Lib\site-packages\uiautomator2\assets
+pyinstaller  --clean  --name mobile-tools  -i assets/logo/32x32.ico main.py  --add-data "C:/Users/fzxs/anaconda3/envs/rpa/Lib/site-packages/uiautomator2/assets:uiautomator2/assets" 
 ```
 
+```shell
+pyinstaller  --clean --name mobile-tools  -w -i assets/logo/32x32.ico main.py
+```
+
+
+python -m uiautomator2 init

BIN
docs/test-01.jpg


+ 275 - 0
gui/phone_list.py

@@ -0,0 +1,275 @@
+import logging
+import tkinter as tk
+from tkinter import ttk
+
+from plat.base.base_control import BaseControl
+
+
+class LeftPanel(tk.LabelFrame):
+    def __init__(self, master=None):
+        super().__init__(master, text="手机列表")
+        self.master = master
+        self.phone_items_data = []  # 用于存储每个手机项的数据 (包含 CheckVar)
+        self.create_widgets()
+        self._check_scrollable()  # 初始检查是否需要滚动条
+
+    def create_widgets(self):
+
+        title_label = ttk.Frame(self, relief="flat", borderwidth=1)
+        title_label.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
+
+        # 添加分割线
+        separator = ttk.Separator(title_label, orient="horizontal")
+        separator.grid(row=1, column=0, columnspan=4, sticky="ew", padx=5, pady=5)
+
+        name_label = ttk.Label(title_label, text='全选')
+        name_label.grid(row=0, column=1, padx=5, pady=10, sticky="e")
+        # 选择框
+        check_all = tk.BooleanVar()
+        check_button = ttk.Checkbutton(title_label, variable=check_all, command=lambda v=check_all: self.toggle_check(v))
+        check_button.grid(row=0, column=2, padx=5, pady=10, sticky="e")
+
+        # 添加数据按钮
+        add_data_button = ttk.Button(title_label, text="刷新", command=self.add_data)
+        add_data_button.grid(row=0, column=3, padx=5, pady=10, sticky="e")
+
+        # 创建 Canvas 和 Scrollbar
+        self.canvas = tk.Canvas(self)
+        self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
+        self.canvas.configure(yscrollcommand=self.scrollbar.set)
+
+        self.canvas.grid(row=1, column=0, columnspan=4, sticky="nsew")
+        self.scrollbar.grid(row=1, column=4, sticky="ns")
+
+        # 创建 Frame 放入 Canvas 中
+        self.list_frame = ttk.Frame(self.canvas)
+        self.canvas.create_window((0, 0), window=self.list_frame, anchor="nw")
+        self.list_frame.bind("<Configure>", self._on_frame_configure)
+        self.canvas.bind("<Enter>", self._bind_mousewheel_enter)
+        self.canvas.bind("<Leave>", self._bind_mousewheel_leave)
+
+        # 配置 Grid 的行和列的权重,使 Canvas 可以扩展
+        self.grid_rowconfigure(1, weight=1)
+        self.grid_columnconfigure(0, weight=1)
+        self.grid_columnconfigure(1, weight=0)  # 全选按钮不扩展
+        self.grid_columnconfigure(2, weight=0)  # 添加数据按钮不扩展
+        self.grid_columnconfigure(3, weight=0)  # 取消全选按钮不扩展
+        self.grid_columnconfigure(4, weight=0)  # 滚动条列不扩展
+
+        self._populate_list()
+
+    def _populate_list(self):
+        """根据 self.phone_data 填充列表"""
+        for widget in self.list_frame.winfo_children():
+            widget.destroy()
+        self.phone_items_data = []
+        # for phone in self.phone_data:
+        #     self.create_list_item(phone["name"], phone["status"])
+
+        for pkg, device in BaseControl.connect_dict.items():
+            logging.info(f'==添加手机信息=>{pkg} {device}')
+            self.create_list_item(device)
+
+        self._on_frame_configure(None)  # 更新滚动区域
+
+    def create_list_item(self, device):
+        item_frame = ttk.Frame(self.list_frame, padding=(5, 5))
+        item_frame.pack(fill="x", expand=True)
+
+        name = device['device']
+        status = device['status']
+
+        # 选择框
+        check_var = tk.BooleanVar()
+        check_var.set(True)
+        check_button = ttk.Checkbutton(item_frame, variable=check_var)
+        check_button.pack(side="left")
+
+        # 手机名称
+        name_label = ttk.Label(item_frame, text=f'{device["name"]}:【{device["pkg"]}】', anchor="w", width=20)
+        name_label.pack(side="left", padx=5)
+
+        # 状态指示器
+        status_canvas = tk.Canvas(item_frame, width=10, height=10, highlightthickness=0)
+        status_canvas.pack(side="left", padx=5)
+        if status == "online":
+            status_canvas.create_oval(0, 0, 10, 10, fill="green")
+        else:
+            status_canvas.create_oval(0, 0, 10, 10, fill="red")
+
+        # 状态按钮 (这里我们用一个简单的 Label 模拟)
+        status_button = ttk.Label(item_frame, text="@", width=3, anchor="center", relief="groove")
+        status_button.pack(side="right", padx=5)
+
+        self.phone_items_data.append((check_var, name, status))  # 存储 CheckVar 和数据
+
+    def toggle_check(self, check_var):
+        if check_var.get():
+            print("复选框被选中")
+        else:
+            print("复选框被取消选中")
+        for var, _, _ in self.phone_items_data:
+            var.set(check_var.get())
+        pass
+
+    def add_data(self):
+        """
+        动态添加数据
+        """
+        # logging.info(f"==手机信息=>{BaseControl.connect_dict}")
+        # new_index = len(self.phone_data) + 1
+        # new_phone = {"name": f"Tel - {new_index:02d} [New Device]", "status": "pending"}
+        # self.phone_data.append(new_phone)
+        self._populate_list()  # 重新填充列表以显示新数据
+        self._check_scrollable()  # 添加数据后检查是否需要滚动条
+
+    def _on_frame_configure(self, event):
+        """当列表 Frame 大小改变时,更新 Canvas 的滚动区域并检查是否需要绑定滚轮事件"""
+        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
+        self._check_scrollable()
+
+    def _check_scrollable(self):
+        """检查内容是否需要滚动条,并决定是否需要显示"""
+        if self.canvas.winfo_height() < self.canvas.bbox("all")[3]:
+            self.scrollbar.grid(row=1, column=4, sticky="ns")  # 显示滚动条
+        else:
+            self.scrollbar.grid_remove()  # 隐藏滚动条
+
+    def _bind_mousewheel_enter(self, event):
+        """鼠标进入 Canvas 时绑定滚轮事件"""
+        if self.canvas.winfo_height() < self.canvas.bbox("all")[3]:  # 只有在需要滚动时才绑定
+            self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
+            self.canvas.bind_all("<Button-4>", self._on_mousewheel)  # Linux
+            self.canvas.bind_all("<Button-5>", self._on_mousewheel)  # Linux
+
+    def _bind_mousewheel_leave(self, event):
+        """鼠标离开 Canvas 时解绑滚轮事件"""
+        self.canvas.unbind_all("<MouseWheel>")
+        self.canvas.unbind_all("<Button-4>")
+        self.canvas.unbind_all("<Button-5>")
+
+    def _on_mousewheel(self, event):
+        """鼠标滚轮滚动事件处理"""
+        if event.delta:
+            self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
+        else:
+            if event.num == 4:
+                self.canvas.yview_scroll(-1, "units")
+            elif event.num == 5:
+                self.canvas.yview_scroll(1, "units")
+
+
+class RightPanel(tk.Frame):
+    def __init__(self, master=None, *args, **kwargs):
+        super().__init__(master)
+        self.master = master
+        self.create_args()
+        self.create_info_ares()
+        self.grid_columnconfigure(0, weight=1)  # 让列可以水平扩展
+        self.grid_rowconfigure(0, weight=0)  # 参数设定区域不需要扩展
+        self.grid_rowconfigure(1, weight=1)  # 让操作区域可以垂直扩展
+
+    def create_args(self):
+        # 右侧区域的组件可以在这里创建
+        # label = ttk.Label(self, text="右侧区域", font=("Arial", 12, "bold"))
+        # label.pack(padx=10, pady=10)
+        #
+        # # 添加更多组件...
+        # button = ttk.Button(self, text="按钮", command=lambda: print("按钮被点击"))
+        # button.pack(padx=10, pady=10)
+
+        fieldset = ttk.LabelFrame(self, text="参数设定")
+        fieldset.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+        # fieldset.grid_columnconfigure(0, weight=1)
+        # fieldset.grid_rowconfigure(1, weight=1)
+        name_label = ttk.Label(fieldset, text="币种:", width=5)
+        name_label.grid(row=0, column=0, padx=5, pady=5, sticky="nw")
+
+        name_val = tk.Text(fieldset, width=20, height=1.2, wrap='word')
+        name_val.grid(row=0, column=1, padx=5, pady=5, sticky="nw")
+        name_val.insert('1.0', 'HMSTR')  # 插入文本
+        name_val.config(state='disabled')  # 再禁用
+
+        cmd_label = ttk.Label(fieldset, text="数量:", width=5)
+        cmd_label.grid(row=0, column=2, padx=5, pady=5, sticky="nw")
+
+        cmd_text = tk.Text(fieldset, height=1.2, width=10, wrap='word')
+        cmd_text.grid(row=0, column=3, padx=5, pady=5, sticky="nw")
+
+    def create_info_ares(self):
+        # 创建一个 Frame 来模拟 <fieldset>
+        fieldset = ttk.Frame(self)
+        fieldset.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
+
+        # 设置 fieldset 的网格权重配置
+        fieldset.grid_columnconfigure(0, weight=3)  # 让信息区域占据更多水平空间
+        fieldset.grid_columnconfigure(1, weight=1)  # 操作区域占据较少水平空间
+        fieldset.grid_rowconfigure(0, weight=1)  # 让内容区域可以垂直扩展
+
+        fieldset_action = ttk.LabelFrame(fieldset, text="操作")
+        fieldset_action.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+        fieldset_action.grid_columnconfigure(0, weight=1)
+        fieldset_action.grid_rowconfigure(1, weight=1)
+
+        fieldset_log = ttk.LabelFrame(fieldset, text="操作日志")
+        fieldset_log.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
+        fieldset_log.grid_columnconfigure(0, weight=1)  # 文本框列可扩展
+        fieldset_log.grid_rowconfigure(1, weight=1)  # 文本框行可扩展
+
+        #  给fieldset_log 添加一个多行文本区域
+        log_text = tk.Text(fieldset_log, wrap='word')
+        log_text.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
+        log_text_scroll = ttk.Scrollbar(fieldset_log, command=log_text.yview)
+        log_text_scroll.grid(row=1, column=1, sticky="ns")
+        log_text.config(yscrollcommand=log_text_scroll.set)
+
+        def handle_keypress(event):
+            if event.keysym in ('Return', 'KP_Enter'):
+                log_text.insert('end', '\n')
+                return 'break'
+            return None
+
+        log_text.bind('<KeyPress>', handle_keypress)
+        log_text.focus_set()  # 让文本框获得焦点
+        log_text.focus_set()  # 让文本框获得焦点
+
+        # 给fieldset_action 添加7个按钮,每行2个 共3行
+        green_btn_style = ttk.Style()
+        green_btn_style.configure('Green.TButton', background='#00cc00', foreground='#009900')
+        red_btn_style = ttk.Style()
+        red_btn_style.configure('Red.TButton', background='#ff4444', foreground='#cc0000')
+
+        gray_btn_style = ttk.Style()
+        gray_btn_style.configure('Gray.TButton', background='#888888', foreground='#666666')
+        button1 = ttk.Button(fieldset_action, text="买入/开多", command=lambda: print("买入/开多"), style='Green.TButton')
+        button1.grid(row=0, column=0, padx=5, pady=5, sticky="nw")
+
+        button2 = ttk.Button(fieldset_action, text="买入/平多", command=lambda: print("买入/平多"), style='Green.TButton')
+        button2.grid(row=0, column=1, padx=5, pady=5, sticky="nw")
+        button3 = ttk.Button(fieldset_action, text="卖出/开空", command=lambda: print("卖出/开空"), style='Red.TButton')
+        button3.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
+        button4 = ttk.Button(fieldset_action, text="卖出/平空", command=lambda: print("卖出/平空"), style='Red.TButton')
+        button4.grid(row=1, column=1, padx=5, pady=5, sticky="nw")
+        button5 = ttk.Button(fieldset_action, text="撤销委托", command=lambda: print("撤销委托"))
+        button5.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
+        button6 = ttk.Button(fieldset_action, text="一键撤单", command=lambda: print("一键撤单"))
+        button6.grid(row=2, column=1, padx=5, pady=5, sticky="nw")
+
+
+class PhoneListUI(tk.Frame):
+    """
+    手机列表控制页面
+    """
+
+    def __init__(self, control: BaseControl = None, master=None, cnf=None, **kw):
+        super().__init__(master, cnf if cnf is not None else {}, **kw)
+        self.control = control
+        self.init_ui()
+
+    def init_ui(self):
+        left_panel = LeftPanel(self)
+        left_panel.pack(side="left", fill="y")
+
+        # 为了演示,我们创建一个简单的右侧区域
+        right_panel = RightPanel(self, width=400, height=400)
+        right_panel.pack(side="left", fill="both", expand=True)

+ 333 - 0
gui/settings.py

@@ -0,0 +1,333 @@
+import logging
+import tkinter as tk
+from tkinter import ttk
+from typing import List
+
+from plat.base.base_control import BaseControl, AbsControl
+from utils.config import version
+
+
+class _SettingsConnect(tk.Frame):
+    """
+    连接云手机
+    """
+
+    def __init__(self, master=None, control: AbsControl = None, cnf=None, **kw):
+        super().__init__(master, cnf if cnf is not None else {}, **kw)
+
+        self.control = control
+        self.init_ui()
+        self.pack(padx=10, pady=10)
+
+    def init_ui(self):
+        """
+            初始化连接
+            云手机开启 ABD后无法自动识别,需要通过shell 连接一下。
+            :return:
+
+            """
+        logging.info(f"连接云手机--->")
+        content = ttk.Frame(self)
+        content.pack(fill=tk.BOTH, expand=True)
+
+        fieldset = ttk.LabelFrame(content, text="连接信息")
+        fieldset.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+        content.grid_columnconfigure(0, weight=1)
+        content.grid_rowconfigure(0, weight=1)
+        fieldset.grid_columnconfigure(0, weight=1)
+        fieldset.grid_rowconfigure(1, weight=1)
+
+        cmd_label = ttk.Label(fieldset, text="ADB连接(一行一个):")
+        cmd_label.grid(row=0, column=0, padx=5, pady=5, sticky="nw")
+
+        cmd_text = tk.Text(fieldset, height=10)
+        cmd_text.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
+
+        test_btn = ttk.Button(fieldset, text="连接云手机", command=lambda: self.control.init_adb(cmd_text.get('1.0', tk.END).split('\n'), log_text, **{'ui': self}))
+        test_btn.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
+
+        # 日志区域
+        log_label = ttk.Label(content, text="连接信息:")
+        log_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
+
+        log_text = tk.Text(content, height=10)
+        log_text.grid(row=2, column=0, padx=10, pady=(0, 10), sticky="nsew")
+        content.grid_rowconfigure(2, weight=1)
+
+    # def _connect_adb(self, cmd_text, log_text):
+    #     # 创建加载指示器
+    #
+    #     def func():
+    #         self.control.init_adb(cmd_text.get('1.0', tk.END).split('\n'), log_text)
+    #
+    #     self.control.loadding(self, func)
+    # loading_window = tk.Toplevel(self)
+    # loading_window.title("连接中")
+    # loading_window.geometry("200x100")
+    # loading_window.transient(self)  # 设置为模态
+    # loading_window.grab_set()  # 设置为模态
+    # loading_window.overrideredirect(True)  # 移除窗口边框和按钮
+    # # 相对于父窗口居中显示
+    # window_width = loading_window.winfo_reqwidth()
+    # window_height = loading_window.winfo_reqheight()
+    # parent_x = self.winfo_rootx()
+    # parent_y = self.winfo_rooty()
+    # parent_width = self.winfo_width()
+    # parent_height = self.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:
+    #         self.control.init_adb(cmd_text.get('1.0', tk.END).split('\n'), log_text)
+    #     finally:
+    #         # 在主线程中销毁加载指示器
+    #        self.after(0, lambda: [loading_window.grab_release(), loading_window.destroy()])
+    #
+    # import threading
+    # thread = threading.Thread(target=thread_task)
+    # # thread.daemon = True  # 设置为守护线程,这样主程序退出时线程会自动结束
+    # thread.start()
+
+
+class _SettingsKey(tk.Frame):
+    """
+    设置快捷键
+    """
+    keys_mappings = {
+        '买入/开多': {'ctrl': False, 'shift': False, 'alt': False, 'key': ''},
+        '买入/平多': {'ctrl': False, 'shift': False, 'alt': False, 'key': ''},
+        '卖出/开空': {'ctrl': False, 'shift': False, 'alt': False, 'key': ''},
+        '卖出/平空': {'ctrl': False, 'shift': False, 'alt': False, 'key': ''},
+        "一键撤单": {'ctrl': False, 'shift': False, 'alt': False, 'key': ''},
+        '撤销委托': {'ctrl': False, 'shift': False, 'alt': False, 'key': ''},
+    }
+
+    def __init__(self, master=None, cnf=None, **kw):
+        super().__init__(master, cnf if cnf is not None else {}, **kw)
+
+        self.control = None
+        self.init_ui()
+        self.pack(padx=10, pady=10)
+
+    def init_ui(self):
+        # 快捷键设置
+        fieldset = ttk.LabelFrame(self, text="快捷键设置")
+        fieldset.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+        fieldset.grid_columnconfigure(0, weight=1)
+        fieldset.grid_rowconfigure(1, weight=1)
+
+        # 配置Checkbutton样式
+        style = ttk.Style()
+        style.configure("TCheckbutton", )
+
+        # 循环 keys_mappings
+        for i, (key, value) in enumerate(self.keys_mappings.items()):
+            # 创建一个 frame 来保存当前行的所有控件
+            row_frame = ttk.Frame(fieldset)
+            row_frame.grid(row=i, column=0, sticky="ew", padx=5, pady=5)
+
+            # 创建说明标签
+            label = ttk.Label(row_frame, text=f"{key:<7}:")
+            label.pack(side=tk.LEFT, padx=(0, 10))
+
+            # 为每个按键组合创建变量并保存在字典中
+            key_state = {
+                'name': key,
+                'ctrl_var': tk.BooleanVar(value=value['ctrl']),
+                'alt_var': tk.BooleanVar(value=value['alt']),
+                'shift_var': tk.BooleanVar(value=value['shift']),
+                'key_var': tk.StringVar(value=value['key'])
+            }
+
+            # 创建复选框
+            ttk.Checkbutton(row_frame, text="Ctrl", variable=key_state['ctrl_var'],
+                            command=lambda state=key_state: self._on_checkbox_click(state)).pack(side=tk.LEFT, padx=5)
+            ttk.Checkbutton(row_frame, text="Alt", variable=key_state['alt_var'],
+                            command=lambda state=key_state: self._on_checkbox_click(state)).pack(side=tk.LEFT, padx=5)
+            ttk.Checkbutton(row_frame, text="Shift", variable=key_state['shift_var'],
+                            command=lambda state=key_state: self._on_checkbox_click(state)).pack(side=tk.LEFT, padx=5)
+
+            # 创建下拉框
+            options = [chr(i) for i in range(65, 91)] + [f"F{i}" for i in range(1, 13)]
+            combo = ttk.Combobox(row_frame, textvariable=key_state['key_var'], values=options,
+                                 width=8, state='readonly', )
+            combo.bind('<<ComboboxSelected>>', lambda event, state=key_state: self._on_checkbox_click(state))
+            # 如果key_state中已有值就使用已有值,否则设置为第一个选项
+            if key_state['key_var'].get():
+                combo.set(key_state['key_var'].get())
+            else:
+                combo.current(i)  # 设置第一个选项为默认值
+            combo.pack(side=tk.LEFT, padx=5)
+
+        # idx = len(self.keys_mappings)
+        # save_btn = ttk.Button(self, text="保存",
+        #                       command=lambda: print('保存快捷键设置'))
+        # save_btn.grid(row=idx + 1, column=0, padx=5, pady=5, sticky="nwe")
+
+        # test_btn = ttk.Button(self, text="恢复默认",
+        #                       command=lambda: print('恢复默认'))
+        # test_btn.grid(row=idx + 2, column=0, padx=5, pady=5, sticky="nwe")
+
+    def _on_checkbox_click(self, key_state: dict):
+        """
+        复选框点击事件处理
+        """
+        # 获取当前状态
+        name = key_state['name']
+        ctrl_state = key_state['ctrl_var'].get()
+        alt_state = key_state['alt_var'].get()
+        shift_state = key_state['shift_var'].get()
+        key = key_state['key_var'].get()
+
+        # 更新映射
+        self.keys_mappings[name].update({
+            'ctrl': ctrl_state,
+            'alt': alt_state,
+            'shift': shift_state,
+            'key': key
+        })
+
+        logging.info(f"快捷键 '{name}' 更新: Ctrl={ctrl_state}, Alt={alt_state}, Shift={shift_state}, Key={key}")
+
+        self._key_binds(name)
+
+    def _key_binds(self, name: str):
+
+        """
+        绑定快捷键
+        :param name:
+        :return:
+        """
+
+        key = self.keys_mappings[name]
+        logging.info(f"<UNK> '{key}' <UNK>")
+        key_combo = []
+        if key['ctrl']:
+            key_combo.append('Control')
+        if key['shift']:
+            key_combo.append('Shift')
+        if key['alt']:
+            key_combo.append('Alt')
+
+        # 组合所有激活的修饰键
+        if key_combo:
+            key_bind = f"<{'-'.join(key_combo)}-{key['key'].lower()}>"
+            self.winfo_toplevel().unbind_all(key_bind)
+            self.winfo_toplevel().bind_all(key_bind, lambda event: self._demo_click(f'-->{name}'))
+            logging.info(f'绑定快捷键: {key_bind} --> {name}')
+        else:
+            # 如果没有修饰键,直接绑定键值
+            self.winfo_toplevel().unbind_all(f"<{key['key'].lower()}>")
+            self.winfo_toplevel().bind_all(f"<{key['key'].lower()}>", lambda event: self._demo_click(f'-->{name}'))
+            logging.info(f"<{key['key']}=> {name}  ")
+
+            pass
+
+    def _demo_click(self, event):
+        logging.info(f"<DEMO> {event} <DEMO>")
+
+    def _keys_binds(self, keys: List[dict]):
+        """
+        返回快捷键映射
+        :param keys: [{'ctrl':True,'shift':True,'alt':True,'key':'a'},...]
+        :return:
+        """
+        # 循环 keys 绑定快捷键, self.control
+        for i, key in enumerate(keys):
+            # 绑定快捷键
+            self.init_ui(keys)
+
+
+class _SettingsOther(tk.Frame):
+    """
+    其他设置
+    """
+
+    def __init__(self, master=None, cnf=None, **kw):
+        super().__init__(master, cnf if cnf is not None else {}, **kw)
+
+        self.tel_connect = None
+        self.control = None
+        self.init_ui()
+
+    def init_ui(self):
+        # 创建并配置notebook
+        notebook = ttk.Notebook(self)
+
+        # 设置notebook的填充
+        notebook.pack(anchor="w", fill=tk.BOTH, expand=True)
+
+
+class _SettingsAbout(tk.Frame):
+    """
+    关于
+    """
+
+    def __init__(self, master=None, cnf=None, **kw):
+        super().__init__(master, cnf if cnf is not None else {}, **kw)
+
+        self.init_ui()
+        self.pack(padx=10, pady=10)
+
+    def init_ui(self):
+        # 创建一个 Frame 来模拟 <fieldset>
+        fieldset = ttk.LabelFrame(self, text="群控助手")
+        fieldset.grid(row=0, column=0, padx=10, pady=10, sticky="nw")
+        self.grid_columnconfigure(0, weight=1)
+        self.grid_rowconfigure(0, weight=0)
+        fieldset.grid_columnconfigure(0, weight=1)
+        fieldset.grid_rowconfigure(1, weight=1)
+
+        cmd_label = ttk.Label(fieldset, text=f"版本:{version}")
+        cmd_label.grid(row=0, column=0, padx=5, pady=5, sticky="nw")
+        cmd_label = ttk.Label(fieldset, text=f"作者:强哥")
+        cmd_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
+        cmd_label = ttk.Label(fieldset, text=f"邮箱:fzxs88@yeah.net")
+        cmd_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw")
+
+
+class SettingUI(tk.Frame):
+    """
+    设置页面
+    """
+
+    def __init__(self, master=None, control: BaseControl = None, cnf=None, **kw):
+        super().__init__(master, cnf if cnf is not None else {}, **kw)
+
+        self.about = None
+        self.options = None
+        self.key_mapping = None
+        self.tel_connect = None
+        self.control = control
+        self.init_ui()
+
+    def init_ui(self):
+        # 创建并配置notebook
+        notebook = ttk.Notebook(self)
+        #
+        # 设置notebook的填充
+        notebook.pack(anchor="w", fill=tk.BOTH, expand=True)
+        #
+        # 第一个选项卡的内容
+        self.tel_connect = ttk.Frame(notebook)
+        notebook.add(self.tel_connect, text="连接云手机")
+        _SettingsConnect(master=self.tel_connect, control=self.control)
+        # 第二个选项卡的内容
+        self.key_mapping = ttk.Frame(notebook)
+        notebook.add(self.key_mapping, text="设置快捷键")
+        _SettingsKey(self.key_mapping)
+
+        # 第三个选项卡的内容
+        self.options = ttk.Frame(notebook)
+        notebook.add(self.options, text="其他设置")
+        label3 = ttk.Label(self.options, text="其他设置")
+        label3.pack(padx=10, pady=10)
+
+        # 第四个选项卡的内容
+        self.about = ttk.Frame(notebook)
+        notebook.add(self.about, text="关于")
+        _SettingsAbout(self.about)

+ 14 - 2
main.py

@@ -1,10 +1,22 @@
 import logging
 
-from gui.ui import Application
+from gui.main_ui import Application
 from plat.deepcoin_control import DeepCoinControl
+from plat.settings_control import SettingsControl
+
 logging.basicConfig(level=logging.INFO, force=True)  # 设置日志级别
+
 if __name__ == "__main__":
+    settings = SettingsControl('设置')
     app = Application(
-        controls=[DeepCoinControl('DeepCoin')]
+        controls={
+            SettingsControl.__name__: settings,
+            'deepCoin': DeepCoinControl('DeepCoin', settings)
+        }
+        # controls=[
+        #     adb
+        #     ,
+        #     DeepCoinControl('DeepCoin', adb)
+        # ]
     )
     app.mainloop()

+ 162 - 41
plat/base/base_control.py

@@ -3,22 +3,137 @@ 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 AbsControl(ABC):
+class BaseControl(ABC):
+    # ctx_dict = {
+    #     'connect': 1,
+    #     'platform': 2,
+    # }
+    connect_dict = {
 
-    def __init__(self, name: str):
+    }
+
+    app_pkg = {
+        'com.niocpeed.dna': 'DeepCoin',
+    }
+
+    def __init__(self, name: str, ctx: int = 2):
+        # 名称
+        self.name = name
+        self.ctx = ctx
+
+        pass
+
+    def init_adb(self, commands: List[str], *args, **kwargs):
+        """
+        连接设备
+        使用 python shell执行命令
+        """
+        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 AbsControl(BaseControl):
+
+    def __init__(self, name: str, ctx: int = 2, *args, **kwargs):
         """
         # :param : 设备序列号。 例如 127.0.0.1:6555
         #     可以通过 `adb devices`  获取
         """
         # u2.logger.setLevel(logging.DEBUG)
-        # 名称
-        self.name = name
 
-        self.d = None
+        super().__init__(name)
+
+        self.ctx = ctx
+
+        self.info = {}
+        #
+        self._d = None
+
         # 屏幕高度
         self.height = 0
         # 屏幕宽度
@@ -32,15 +147,36 @@ class AbsControl(ABC):
 
         self._log_func = None
 
-    def connect_adb(self, serial: str):
+    def d(self, serial: str = ''):
         """
         连接设备
         """
-        self.d = u2.connect(serial)
+        if self._d is not None:
+            return self._d
 
-        self.enable_click_monitor()
+        if serial in self.connect_dict:
+            self.info = self.connect_dict[serial]
+            self._d = self.info['d']
+        else:
+            self._d = u2.connect(serial)
 
-        #     如果连接成功,返回截图,否则返回None
+        return self._d
+
+    def re_connect(self):
+
+        pass
+
+    # @abstractmethod
+    # def get_app_package_name(self):
+    #     return ""
+
+    def prevent_sleep(self):
+        """
+        防止设备休眠
+        """
+        self.d().wake()
+        self.d().screen_on()
+        self.d().unlock()
 
     def screenshot(self):
         """
@@ -48,22 +184,7 @@ class AbsControl(ABC):
         """
         # 获取系统/tmp路径
         tmp = tempfile.gettempdir()
-        return self.d.screenshot(f"{tmp}/{self.name}.png")
-
-    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')
+        return self.d().screenshot(f"{tmp}/{self.name}.png")
 
     def to_top_swipe(self, sleep=0.1, times=2):
         """
@@ -75,9 +196,9 @@ class AbsControl(ABC):
         width, height = self.get_screen_size()
         # 循环滑动直到无法继续滑动
         for _ in range(times):
-            self.d.swipe(width // 2, height * 0.8, width // 2, height)  # 向上滑动
+            self.d().swipe(width // 2, height * 0.8, width // 2, height)  # 向上滑动
             # 短暂等待确保滑动完成
-            self.d.sleep(sleep)
+            self.d().sleep(sleep)
 
     def click_point(self, x: int, y: int):
         """
@@ -85,13 +206,13 @@ class AbsControl(ABC):
         :param x: x坐标
         :param y: y坐标
         """
-        self.d.click(x, y)
+        self.d().click(x, y)
 
     def click_xpath(self, xpath: str):
         """
         点击指定xpath
         """
-        el = self.d.xpath(xpath).get()
+        el = self.d().xpath(xpath).get()
         if el:
             el.click()
         else:
@@ -103,16 +224,16 @@ class AbsControl(ABC):
         """
         width, height = self.get_screen_size()
         # 从屏幕下方向上滑动到顶部
-        self.d.swipe(width // 2, height * 0.8, width // 2, height * 0.2)  # 向上滑动
+        self.d().swipe(width // 2, height * 0.8, width // 2, height * 0.2)  # 向上滑动
 
         # 短暂等待确保滑动完成
-        self.d.sleep(sleep)
+        self.d().sleep(sleep)
 
     def get_screen_size(self):
         """
         获取屏幕尺寸,宽度和高度
         """
-        self.width, self.height = self.d.window_size()
+        self.width, self.height = self.d().window_size()
         logging.info(f"屏幕尺寸: {self.width}x{self.height}")
         return self.width, self.height
 
@@ -125,7 +246,7 @@ class AbsControl(ABC):
         :param end_y: 结束y坐标
         :param steps: 步数,值越大滑动越平滑
         """
-        self.d.swipe_ext(start_x, start_y, end_x, end_y, steps)
+        self.d().swipe_ext(start_x, start_y, end_x, end_y, steps)
 
     def add_point(self, point=None):
         """
@@ -167,7 +288,7 @@ class AbsControl(ABC):
         保存坐标
         """
         self.to_top_swipe()
-        self.d.sleep(2)
+        self.d().sleep(2)
 
         for func in self._func:
             func()
@@ -208,7 +329,7 @@ class AbsControl(ABC):
         info 数据结构
         :return: x, y, el
         """
-        el = self.d.xpath(xpath).get()
+        el = self.d().xpath(xpath).get()
         x, y = el.center()
 
         return x, y, el
@@ -218,7 +339,7 @@ class AbsControl(ABC):
         获取所有坐标点
         info 数据结构
         """
-        els = self.d.xpath(xpath).all()
+        els = self.d().xpath(xpath).all()
 
         items = []
 
@@ -236,8 +357,8 @@ class AbsControl(ABC):
         :param timeout: 超时时间(秒)
         :return: 是否存在
         """
-        self.d.sleep(timeout)
-        return self.d.xpath(xpath).exists
+        self.d().sleep(timeout)
+        return self.d().xpath(xpath).exists
 
     def input_xpath(self, xpath: str, text: str, clear: bool = True):
         """
@@ -247,7 +368,7 @@ class AbsControl(ABC):
         :param text: 要输入的文本
         :param clear: 输入前是否清空原有内容
         """
-        element = self.d.xpath(xpath).get()
+        element = self.d().xpath(xpath).get()
         if element:
             if clear:
                 element.text.set_text("")  # 清空原有内容
@@ -266,8 +387,8 @@ class AbsControl(ABC):
         """
         self.click_point(x, y)  # 先点击获取焦点
         if clear:
-            self.d.clear_text()  # 清空原有内容
-        self.d.send_keys(text)  # 输入新文本
+            self.d().clear_text()  # 清空原有内容
+        self.d().send_keys(text)  # 输入新文本
 
     @abstractmethod
     def event_f1(self):

+ 19 - 8
plat/deepcoin_control.py

@@ -6,8 +6,14 @@ from plat.base.base_control import AbsControl
 
 class DeepCoinControl(AbsControl):
 
-    def __init__(self, serial: str):
+    def get_app_package_name(self):
+        return 'com.niocpeed.dna'
+
+    def __init__(self, serial: str, *args, **kwargs):
         super().__init__(serial)
+
+        self.package_name = 'com.niocpeed.dna'
+
         self.point_path = '../../assets/point/deepcoin_point.json'
         self.add_func(self.btn_jiao_yi)
         self.add_func(self.btn_mairu_kaiduo)
@@ -96,7 +102,7 @@ class DeepCoinControl(AbsControl):
         x, y, btn = self.btn_mairu_kaiduo()
 
         x1 = x + offset * 69
-        y1 = y - 140
+        y1 = y - 155
         logging.info(f"开多坐标: ({x},{y})")
         logging.info(f"保证金50%坐标: ({x1},{y1})")
 
@@ -146,12 +152,12 @@ class DeepCoinControl(AbsControl):
         """
         x, y, _ = self.btn_kaichang()
         logging.info(f"开仓按钮坐标: ({x},{y})")
-        x1 = x + 50
-        y1 = y + 55
+        x1 = x + 100
+        y1 = y + 70
         logging.info(f"选择委托框: ({x1},{y1})")
         self.click_point(x1, y1)
         x2 = x1
-        y2 = y1 + 100
+        y2 = y1 + 150
         self.d.sleep(0.2)
         logging.info(f"选择限价委托: ({x2},{y2})")
         self.click_point(x2, y2)
@@ -164,7 +170,7 @@ class DeepCoinControl(AbsControl):
         """
 
         x1 = 50
-        y1 = 330 + offset * 20
+        y1 = 450 + offset * 20
         logging.info(f"红色部分选择价格: ({x1},{y1})")
         self.click_point(x1, y1)
 
@@ -270,6 +276,7 @@ class DeepCoinControl(AbsControl):
         pass
 
     def event_f2(self):
+        self.to_top_swipe(sleep=0.2)
         self.click_kaicang()
         self.select_kaicang_weituo_xianjia()
         self.d.sleep(0.1)
@@ -277,6 +284,7 @@ class DeepCoinControl(AbsControl):
         self.click_price_red()
 
         self.text_xiadan_shuliang(3)
+        # self.slider_baozhengjin_kaicang()
 
         self.click_mairu_kaiduo()
         pass
@@ -292,7 +300,8 @@ class DeepCoinControl(AbsControl):
         self.click_pingchang()
         self.select_kaicang_weituo_xianjia()
         self.click_price_green()
-        self.text_xiadan_shuliang(3)
+        # self.text_xiadan_shuliang(3)
+        self.slider_baozhengjin_pingcang(offset=1)
         self.click_maichu_pingduo()
         pass
 
@@ -307,6 +316,7 @@ class DeepCoinControl(AbsControl):
         self.click_kaicang()
         self.select_kaicang_weituo_xianjia()
         self.click_price_red()
+        self.slider_baozhengjin_kaicang()
         self.click_maichu_kaikong()
         pass
 
@@ -323,7 +333,8 @@ class DeepCoinControl(AbsControl):
 
         self.click_price_red()
 
-        self.text_xiadan_shuliang(3)
+        # self.text_xiadan_shuliang(3)
+        self.slider_baozhengjin_pingcang(offset=2)
 
         self.click_mairu_pingkong()
         pass

+ 79 - 0
plat/settings_control.py

@@ -0,0 +1,79 @@
+import logging
+import subprocess
+from typing import List
+
+import tkinter as tk
+from adbutils import adb_path
+
+from plat.base.base_control import BaseControl
+
+
+class SettingsControl(BaseControl):
+
+    def __init__(self, name: str, ctx: int = 1):
+
+        """
+        设置相关的控制器
+        :param name:
+        """
+        super().__init__(name)
+
+        self.ctx = ctx
+
+    def init_adb(self, commands: List[str], *args, **kwargs):
+
+        def func():
+            adb = adb_path()
+            logging.info(f'adb_path:{adb}')
+            log_widget = args[0] if args else None
+            _commons = list(commands)
+            _commons.append(f'adb devices')
+            for command in _commons:
+                command = command.strip()
+                if command == '':
+                    continue
+                logging.info(f'初始化[{self.name}]:{command}')
+                try:
+                    cmd = command.split(' ')
+                    if cmd[1] == 'connect':
+                        self._init_connect(cmd)
+                    cmd[0] = adb
+                    logging.info(f"执行命令:{cmd}")
+                    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
+                    logging.info(f"命令执行结果:\n{result.stdout}")
+                    if log_widget:
+                        log_widget.insert(tk.END, f"命令【{command}】执行成功:\n{result.stdout}")
+
+
+
+                except subprocess.CalledProcessError as cpe:
+                    logging.info(f"命令 '{command}' 执行失败: {cpe}")
+                    if log_widget:
+                        log_widget.insert(tk.END, f"{cpe}\n")
+
+        top = kwargs['ui']
+
+        if top:
+            self.loadding(top, func)
+        else:
+            func()
+
+    def _init_connect(self, cmds: List[str]):
+        """
+        初始化一些连接,将信息保存到 connect_dict
+        :param cmds: 要执行的命令行 ['adb', 'connect', '127.0.0.1:64079']
+        :return:
+        """
+        cmd = list(cmds).pop()
+        d = self.connect_adb(cmd)
+        info = d.info
+        logging.info(f'设备信息{info}')
+        self.connect_dict[cmd] = {
+            'd': d,
+            'info': info,
+            'device': cmd,
+            'status': 'online',
+            'pkg': self.app_pkg[info['currentPackageName']],
+            'name': str(d.shell('settings get global device_name').output).strip(),
+        }
+        pass

+ 3 - 1
pyproject.toml

@@ -4,15 +4,17 @@ version = "0.1.0"
 description = ""
 authors = ["fzxs88 <fzxs88@yeah.net>"]
 readme = "README.md"
+package-mode = false
 
 [tool.poetry.dependencies]
 python = "~3.10"
 fastapi = "^0.115.8"
-uiautomator2 = "^3.2.8"
+uiautomator2 = "3.2.9"
 uvicorn = "^0.34.0"
 numpy = "^2.2.4"
 pyinstaller = "^6.12.0"
 opencv-python = "^4.11.0.86"
+schedule = "^1.2.2"
 
 [[tool.poetry.source]]
 name = "tsinghua"

+ 83 - 83
test/deepcoin/deepcoin_test.py

@@ -1,87 +1,87 @@
-import time
-
-import uiautomator2 as u2
-
-# 连接到设备,默认连接第一个设备,如果有多个设备可以指定序列号
-d = u2.connect('127.0.0.1:6555')
-
-# 获取所有运行的APP
-running_apps = d.app_list_running()
-print("安装列表:", running_apps)
-# 如果app没有启动, 启动APP
-if "com.niocpeed.dna" not in running_apps:
-    print("com niocpeed.dna 重新启动")
-    d.app_start("com.niocpeed.dna")
-    # tab 交易按钮
-btn_jy = d.xpath('//*[@content-desc="交易"]')
-# btn_jy.click()
-# 交易方式选择
-btn_sel = d.xpath('//*[@content-desc="USDT合约"]')
-if btn_sel.exists:
-    btn_sel.click()
-
-# 设置委托
-input_weituo = d.xpath('//*[@content-desc="最优"]')
-# 设置数量
-input_num = d.xpath('//android.widget.ScrollView/android.view.View[18]/android.widget.EditText[1]')
-btn_buy = d(description="买入/开多")
-
-buy_x, buy_y = btn_buy.center()
-
-start1 = time.time()
-input_weituo.click()
-end1 = time.time()
-print("点击:最优委托 操作耗时:", end1 - start1)
-
+# import time
+#
+# import uiautomator2 as u2
+#
+# # 连接到设备,默认连接第一个设备,如果有多个设备可以指定序列号
+# d = u2.connect('127.0.0.1:6555')
+#
+# # 获取所有运行的APP
+# running_apps = d.app_list_running()
+# print("安装列表:", running_apps)
+# # 如果app没有启动, 启动APP
+# if "com.niocpeed.dna" not in running_apps:
+#     print("com niocpeed.dna 重新启动")
+#     d.app_start("com.niocpeed.dna")
+#     # tab 交易按钮
+# btn_jy = d.xpath('//*[@content-desc="交易"]')
+# # btn_jy.click()
+# # 交易方式选择
+# btn_sel = d.xpath('//*[@content-desc="USDT合约"]')
+# if btn_sel.exists:
+#     btn_sel.click()
+#
+# # 设置委托
+# input_weituo = d.xpath('//*[@content-desc="最优"]')
+# # 设置数量
+# input_num = d.xpath('//android.widget.ScrollView/android.view.View[18]/android.widget.EditText[1]')
+# btn_buy = d(description="买入/开多")
+#
+# buy_x, buy_y = btn_buy.center()
+#
 # start1 = time.time()
-# input_weituo.set_text("0.114")
+# input_weituo.click()
 # end1 = time.time()
-# print("设置:操作耗时:", end1 - start1)
-start1 = time.time()
-input_num.click()
-end1 = time.time()
-print("点击数量:操作耗时:", end1 - start1)
+# print("点击:最优委托 操作耗时:", end1 - start1)
+#
+# # start1 = time.time()
+# # input_weituo.set_text("0.114")
+# # end1 = time.time()
+# # print("设置:操作耗时:", end1 - start1)
 # start1 = time.time()
-# input_num.set_text("40")
+# input_num.click()
 # end1 = time.time()
-# print("设置数量:操作耗时:", end1 - start1)
-
-# for i in range(10):
-#     # 初始化点击按钮
-#     start = time.time()
-#     # start1 = time.time()
-#     # input_weituo.click()
-#     # end1 = time.time()
-#     # print("点击:最优委托 操作耗时:", end1 - start1)
-#     #
-#     # # start1 = time.time()
-#     # # input_weituo.set_text("0.114")
-#     # # end1 = time.time()
-#     # # print("设置:操作耗时:", end1 - start1)
-#     # start1 = time.time()
-#     # input_num.click()
-#     # end1 = time.time()
-#     # print("点击:操作耗时:", end1 - start1)
-#     # start1 = time.time()
-#     # input_num.set_text("40")
-#     # end1 = time.time()
-#     # print("设置:操作耗时:", end1 - start1)
-#     start1 = time.time()
-#     #  直接点击   xpath直接点击
-#     # btn_buy.click()
-#     # 比上面的快50毫秒
-#     # d.click(1078, 1094)  # ui点击 指定坐标
-#     # 比上面的快100毫秒  直接调用shell 指定坐标
-#     d.shell(f"input tap {buy_x} {buy_y}")
-#     end1 = time.time()
-#     print("点击下单:操作耗时:", end1 - start1)
-#     end = time.time()
-#     # print("点击整体:操作耗时:", end - start)
-#     time.sleep(5)
-
-# 给开仓确认的弹框 选中 下次不在提醒,能减少时间
-# d(description="确认").click()
-# d.xpath('//*[@content-desc="确认"]').click()
-
-
-# button = d.xpath("//android.widget.Button[@text='Button']")
+# print("点击数量:操作耗时:", end1 - start1)
+# # start1 = time.time()
+# # input_num.set_text("40")
+# # end1 = time.time()
+# # print("设置数量:操作耗时:", end1 - start1)
+#
+# # for i in range(10):
+# #     # 初始化点击按钮
+# #     start = time.time()
+# #     # start1 = time.time()
+# #     # input_weituo.click()
+# #     # end1 = time.time()
+# #     # print("点击:最优委托 操作耗时:", end1 - start1)
+# #     #
+# #     # # start1 = time.time()
+# #     # # input_weituo.set_text("0.114")
+# #     # # end1 = time.time()
+# #     # # print("设置:操作耗时:", end1 - start1)
+# #     # start1 = time.time()
+# #     # input_num.click()
+# #     # end1 = time.time()
+# #     # print("点击:操作耗时:", end1 - start1)
+# #     # start1 = time.time()
+# #     # input_num.set_text("40")
+# #     # end1 = time.time()
+# #     # print("设置:操作耗时:", end1 - start1)
+# #     start1 = time.time()
+# #     #  直接点击   xpath直接点击
+# #     # btn_buy.click()
+# #     # 比上面的快50毫秒
+# #     # d.click(1078, 1094)  # ui点击 指定坐标
+# #     # 比上面的快100毫秒  直接调用shell 指定坐标
+# #     d.shell(f"input tap {buy_x} {buy_y}")
+# #     end1 = time.time()
+# #     print("点击下单:操作耗时:", end1 - start1)
+# #     end = time.time()
+# #     # print("点击整体:操作耗时:", end - start)
+# #     time.sleep(5)
+#
+# # 给开仓确认的弹框 选中 下次不在提醒,能减少时间
+# # d(description="确认").click()
+# # d.xpath('//*[@content-desc="确认"]').click()
+#
+#
+# # button = d.xpath("//android.widget.Button[@text='Button']")

+ 17 - 6
test/deepcoin/test01.py

@@ -2,6 +2,13 @@
 
 import uiautomator2 as u2
 
+
+import os
+
+command = "ls -l"  # 你想要执行的 shell 命令
+return_code = os.system(command)
+print(f"命令 '{command}' 的返回码是: {return_code}")
+
 # 连接设备
 d = u2.connect()
 # 获取屏幕的尺寸
@@ -39,16 +46,19 @@ print(f"屏幕尺寸: {width}x{height}")
 # 每一步大约需要 5 毫秒
 # 例如:设置 steps=200,整个滑动过程将持续约 1 秒(200 * 5ms = 1000ms)
 # 当设置了 steps 时,会忽略 duration 参数
-# d.swipe(width // 2, height * 0.8, width // 2, height * 0.1)
+d.swipe(width // 2, height * 0.8, width // 2, height * 0.1)
 
 # buttons = d.xpath('//*[starts-with(@content-desc, "持仓")]').all()
 # button = d.xpath('//*[@content-desc="买入/开多"]').get()
-button = d.xpath('//*[@content-desc="市价全平"]').get()
-print(f"第一个 坐标(left, top, right, bottom): {button.bounds}")
-buttons = d.xpath('//*[@content-desc="市价全平"]').all()
+# button = d.xpath('//*[@content-desc="市价全平"]').get()
+# print(f"第一个 坐标(left, top, right, bottom): {button.bounds}")
+# buttons = d.xpath('//*[@content-desc="市价全平"]').all()
+#
+button = d.xpath('//*[@content-desc="手机"]').get()
 
-for idx, button in enumerate(buttons):
-    print(f"按钮 {idx + 1} 坐标(left, top, right, bottom): {button.bounds}")
+button.click()
+# for idx, button in enumerate(buttons):
+#     print(f"按钮 {idx + 1} 坐标(left, top, right, bottom): {button.bounds}")
 
 # if button:
 #     # bounds返回(left, top, right, bottom)坐标
@@ -56,3 +66,4 @@ for idx, button in enumerate(buttons):
 #     top_distance = button.bounds[1]  # top坐标即是距离顶部的距离
 #     print(f"按钮距离顶部: {top_distance}像素")
 # d.swipe(width // 2, height * 0.8, width // 2, height * 0.3, duration=0.5)
+

+ 58 - 45
test/deepcoin/test_deepcoin_control.py

@@ -10,7 +10,7 @@ class MyTestCase(unittest.TestCase):
     def setUp(self):
         self.pc = DeepCoinControl('Deepcoin')
 
-        self.pc.connect_adb('127.0.0.1:6555')
+        self.pc.connect_adb('127.0.0.1:64079')
 
     def test_to_top_swipe(self):
         """
@@ -64,88 +64,99 @@ class MyTestCase(unittest.TestCase):
         """
         f1 开仓界面,仓位滑竿百分比(30-60)
         """
-        self.pc.click_kaicang()
-        self.pc.slider_baozhengjin_kaicang()
+        # self.pc.click_kaicang()
+        # self.pc.slider_baozhengjin_kaicang()
+        self.pc.event_f1()
 
     def test_f2(self):
         """
         f2 确认开仓,开多 限价职中间数值
         检查撤销限价委托单
         """
-        self.pc.click_kaicang()
-
-        self.pc.select_kaicang_weituo_xianjia()
-        self.pc.d.sleep(0.1)
-
-        self.pc.click_price_red()
-
-        self.pc.text_xiadan_shuliang(3)
-
-        self.pc.click_mairu_kaiduo()
+        # self.pc.click_kaicang()
+        #
+        # self.pc.select_kaicang_weituo_xianjia()
+        # self.pc.d.sleep(0.1)
+        #
+        # self.pc.click_price_red()
+        #
+        # # self.pc.text_xiadan_shuliang(3)
+        # self.pc.slider_baozhengjin_kaicang()
+        #
+        # self.pc.click_mairu_kaiduo()
+        self.pc.event_f2()
 
     def test_f3(self):
         """
         F3 平仓界面 平空,仓位滑竿百分比(90-100)确认开仓,限价职中间数值
         """
-        self.pc.to_top_swipe(sleep=0.2)
-        self.pc.click_pingchang()
-        self.pc.slider_baozhengjin_pingcang(offset=1)
+        # self.pc.to_top_swipe(sleep=0.2)
+        # self.pc.click_pingchang()
+        # self.pc.slider_baozhengjin_pingcang(offset=1)
+        self.pc.event_f3()
 
     def test_f4(self):
         """
         F4   确认平仓 平多 限价取中间数值 检查撤销限价委托单
         """
-        self.pc.to_top_swipe(sleep=0.2)
-        self.pc.click_pingchang()
-        self.pc.select_kaicang_weituo_xianjia()
-        self.pc.click_price_green()
-        self.pc.text_xiadan_shuliang(3)
-        self.pc.click_maichu_pingduo()
+        # self.pc.to_top_swipe(sleep=0.2)
+        # self.pc.click_pingchang()
+        # self.pc.select_kaicang_weituo_xianjia()
+        # self.pc.click_price_green()
+        # # self.pc.text_xiadan_shuliang(3)
+        # self.pc.slider_baozhengjin_pingcang(offset=1)
+        # self.pc.click_maichu_pingduo()
+        self.pc.event_f4()
 
     def test_f5(self):
         """
         F5 开仓界面胂仓位滑竿百分比(30-60)
         """
-        self.pc.to_top_swipe(sleep=0.2)
-        self.pc.click_kaicang()
-        self.pc.slider_baozhengjin_kaicang()
+        # self.pc.to_top_swipe(sleep=0.2)
+        # self.pc.click_kaicang()
+        # self.pc.slider_baozhengjin_kaicang()
+        self.pc.event_f5()
 
     def test_f6(self):
         """
         F6 确认开仓, 开空 限价值中间数值
         检查撤销限价委托单
         """
-        self.pc.to_top_swipe(sleep=0.1)
-        self.pc.click_kaicang()
-        self.pc.select_kaicang_weituo_xianjia()
-        self.pc.click_price_red()
-        # self.pc.text_xiadan_shuliang(3)
-        self.pc.click_maichu_kaikong()
+        # self.pc.to_top_swipe(sleep=0.1)
+        # self.pc.click_kaicang()
+        # self.pc.select_kaicang_weituo_xianjia()
+        # self.pc.click_price_red()
+        # # self.pc.text_xiadan_shuliang(3)
+        # self.pc.click_maichu_kaikong()
+        self.pc.event_f6()
 
     def test_f7(self):
         """
         F7    平仓界面二仓位滑竿百分比(90-100)
         """
-        self.pc.to_top_swipe(sleep=0.2)
-        self.pc.click_pingchang()
-        self.pc.slider_baozhengjin_pingcang(offset=2)
+        # self.pc.to_top_swipe(sleep=0.2)
+        # self.pc.click_pingchang()
+        # self.pc.slider_baozhengjin_pingcang(offset=2)
+        self.pc.event_f7()
 
     def test_f8(self):
         """
         F8 确认平仓, 平空 限价职中间数值
         检查撤销限价委托单
         """
-        self.pc.to_top_swipe()
-
-        self.pc.d.sleep(0.1)
-
-        self.pc.click_pingchang()
-
-        self.pc.click_price_red()
-
-        self.pc.text_xiadan_shuliang(3)
-
-        self.pc.click_mairu_pingkong()
+        # self.pc.to_top_swipe()
+        #
+        # self.pc.d.sleep(0.1)
+        #
+        # self.pc.click_pingchang()
+        #
+        # self.pc.click_price_red()
+        #
+        # # self.pc.text_xiadan_shuliang(3)
+        # self.pc.click_maichu_kaikong()
+        #
+        # self.pc.click_mairu_pingkong()
+        self.pc.event_f8()
 
     def test_jiao_yi(self):
         x, y = self.pc.btn_jiao_yi()
@@ -231,6 +242,8 @@ class MyTestCase(unittest.TestCase):
         if self.pc.check_element_exists('//*[@content-desc="开仓确认"]'):
             self.pc.click_xpath('//*[@content-desc="确认"]')
 
+    def test_get_appinfo(self):
+        logging.info(f'>>>{self.pc.d.info}',)
 
 if __name__ == '__main__':
     unittest.main()

+ 158 - 0
test/test-adb.py

@@ -0,0 +1,158 @@
+# import uiautomator2 as u2
+# d = u2.connect('127.0.0.1:52377') # 例如:"127.0.0.1:61468"
+#
+# print('>>>>',d.info)
+import subprocess
+import os
+
+import subprocess
+import os
+
+
+def run_adb_command(command, adb_path=None):
+    """
+    执行 ADB 命令并返回输出,可以通过环境变量指定 ADB 路径。
+
+    Args:
+        command (list): 包含 ADB 命令及其参数的列表。
+        adb_path (str, optional): ADB 可执行文件的完整路径。
+                                   如果为 None,则使用系统环境变量中的 ADB,
+                                   或者在代码中设置的自定义环境变量。
+    Returns:
+        str: 命令的标准输出,如果执行失败则返回 None。
+    """
+    env = os.environ.copy()  # 复制当前的环境变量
+    adb_executable = adb_path + "\\adb.exe"
+    if adb_path:
+        # 在子进程的环境变量中设置 ADB 的路径
+        # adb_executable = adb_path
+        env["PATH"] = os.pathsep.join([os.path.dirname(adb_path), env.get("PATH", "")])
+    # else:
+
+    try:
+        full_command = [adb_executable] + ["shell"] + command
+        result = subprocess.run(
+            full_command,
+            capture_output=True,
+            text=True,
+            check=True,
+            env=env  # 将设置的环境变量传递给子进程
+        )
+        return result.stdout.strip()
+    except subprocess.CalledProcessError as e:
+        print(f"ADB 命令 '{' '.join(command)}' 执行失败:")
+        print(f"错误代码: {e.returncode}")
+        print(f"错误输出:\n{e.stderr}")
+        return None
+    except FileNotFoundError:
+        print(f"错误: ADB 可执行文件 '{adb_executable}' 未找到。请检查路径是否正确。")
+        return None
+
+
+def list_devices(adb_path=None):
+    """
+    列出已连接的 Android 设备。
+
+    Args:
+        adb_path (str, optional): ADB 可执行文件的完整路径。 Defaults to None.
+    """
+    command = ["devices"]
+    output = run_adb_command(command, adb_path)
+    if output:
+        print("已连接的设备:")
+        for line in output.splitlines()[1:]:
+            if line.strip():
+                device_info = line.split('\t')
+                print(f"- {device_info[0]}")
+    else:
+        print("未找到已连接的 Android 设备。")
+
+
+def get_screen_size(device_serial=None, adb_path=None):
+    """
+    获取指定设备的屏幕尺寸。
+
+    Args:
+        device_serial (str, optional): 设备的序列号。 Defaults to None.
+        adb_path (str, optional): ADB 可执行文件的完整路径。 Defaults to None.
+    """
+    command_base = []
+    if device_serial:
+        command_base.extend(["-s", device_serial])
+    command_base.extend(["wm", "size"])
+    output = run_adb_command(command_base, adb_path)
+    if output:
+        if "Physical size" in output:
+            size_str = output.split(":")[1].strip()
+            print(f"屏幕尺寸: {size_str}")
+            return size_str
+        else:
+            print(f"无法获取屏幕尺寸: {output}")
+            return None
+    return None
+
+
+def tap_screen(x, y, device_serial=None, adb_path=None):
+    """
+    模拟点击屏幕指定坐标。
+
+    Args:
+        x (int): 点击的 X 坐标。
+        y (int): 点击的 Y 坐标。
+        device_serial (str, optional): 设备的序列号。 Defaults to None.
+        adb_path (str, optional): ADB 可执行文件的完整路径。 Defaults to None.
+    """
+    command_base = []
+    if device_serial:
+        command_base.extend(["-s", device_serial])
+    command_base.extend(["input", "tap", str(x), str(y)])
+    print(f"模拟点击坐标 ({x}, {y})")
+    run_adb_command(command_base, adb_path)
+
+
+def swipe_screen(start_x, start_y, end_x, end_y, duration_ms=None, adb_path=None):
+    """
+    模拟屏幕滑动操作。
+
+    Args:
+        start_x (int): 滑动起始 X 坐标。
+        start_y (int): 滑动起始 Y 坐标。
+        end_x (int): 滑动结束 X 坐标。
+        end_y (int): 滑动结束 Y 坐标。
+        duration_ms (int, optional): 滑动持续时间 (毫秒). Defaults to None.
+        adb_path (str, optional): ADB 可执行文件的完整路径。 Defaults to None.
+    """
+    command_base = ["input", "swipe", str(start_x), str(start_y), str(end_x), str(end_y)]
+    if duration_ms is not None:
+        command_base.append(str(duration_ms))
+    run_adb_command(command_base, adb_path)
+
+
+if __name__ == "__main__":
+    # 假设你的 ADB 路径是:
+    # adb_custom_path = "/path/to/your/android-sdk/platform-tools/adb"  # 请替换为你的实际路径
+    adb_custom_path = "../bin/windows/platform-tools"  # 请替换为你的实际路径
+    print("--- 列出已连接的设备 (通过设置环境变量) ---")
+    list_devices(adb_path=adb_custom_path)
+
+    # 尝试获取第一个连接设备的屏幕尺寸 (通过设置环境变量)
+    print("\n--- 获取屏幕尺寸 (通过设置环境变量) ---")
+    screen_size_str = get_screen_size(adb_path=adb_custom_path)
+    screen_width = None
+    screen_height = None
+    if screen_size_str:
+        try:
+            width, height = map(int, screen_size_str.split("x"))
+            screen_width = width
+            screen_height = height
+            print(f"屏幕宽度: {screen_width}, 屏幕高度: {screen_height}")
+        except ValueError:
+            print("无法解析屏幕尺寸字符串。")
+
+    # 模拟点击屏幕中间 (如果获取到屏幕尺寸,并设置环境变量)
+    if screen_width and screen_height:
+        center_x = screen_width // 2
+        center_y = screen_height // 2
+        print("\n--- 模拟点击屏幕中间 (通过设置环境变量) ---")
+        tap_screen(center_x, center_y, adb_path=adb_custom_path)
+        swipe_screen(center_x, center_y, center_x, center_y * 1.5, adb_path=adb_custom_path)

+ 66 - 0
test/test-adb1.py

@@ -0,0 +1,66 @@
+import uiautomator2 as u2
+import time
+
+from adbutils import adb
+from uiautomator2 import Direction
+
+d = u2.connect('127.0.0.1:6555')  # 例如:"127.0.0.1:61468"
+# d = u2.connect_usb()
+print('>0>>>', d.info)
+
+res = d.shell('settings get global device_name')
+
+print('>>1>>', str(res.output).strip())
+#
+# el=d.xpath('//*[@content-desc="开仓"]').scroll_to(xpath='//*[@content-desc="市价全平"]', direction=Direction.FORWARD,
+#                                                max_swipes=100)
+#
+# print('>>>>', el)
+#
+# while True:
+#     time.sleep(9999)
+# d.xpath("//*[@text='私人FM']/../android.widget.ImageView")
+
+# d.screen_off();
+# d.sleep(4)
+# d.screen_on();
+# # d.keyevent('24')
+#
+# urls = ['https://www.baidu.com', 'https://www.qq.com']
+# idx = 0
+
+# for d in adb.device_list():
+#     print('--->', d.serial)  # print device serial
+#     print('>>>', d.info)  # print device
+#     # d.open_browser(urls[idx])
+#     idx += 1
+# d.switch_wifi(False)
+# ctx = d.dump_hierarchy()
+# print(f'>>{idx}>>', ctx)
+# pck = d.list_packages()
+# print(pck)
+# # d.switch_wifi(True)
+# # d.app_start("com.android.chrome")
+# img= d.screenshot()
+# img.save(f'screenshot{idx}.png')
+# print(img)
+# time.sleep(10)
+
+# d = adb.device(serial="127.0.0.1:50269")
+# print('>>1>', d.info)  # print device
+#
+# # d.sleep = lambda seconds: d.shell(f'sleep {seconds}')
+# # d.sleep(3)
+# # print('>>2>', d.info)  # print device
+#
+# w, h = d.window_size()
+# print('>>3>', w, h)  # print device
+
+
+# # or
+# d = adb.device(transport_id=24) # transport_id can be found in: adb devices -l
+
+# You do not need to offer serial if only one device connected
+# RuntimeError will be raised if multi device connected
+# d = adb.device()
+# print('>>>',d.info)

+ 16 - 0
test/test-ui-1.py

@@ -0,0 +1,16 @@
+import tkinter as tk
+from tkinter import ttk
+
+from gui.phone_list import LeftPanel, RightPanel
+
+if __name__ == '__main__':
+    root = tk.Tk()
+    root.title("操作页面布局")
+    left_panel = LeftPanel(root)
+    left_panel.pack(side="left", fill="y")
+
+    # 为了演示,我们创建一个简单的右侧区域
+    right_panel = RightPanel(root, width=400, height=400)
+    right_panel.pack(side="left", fill="both", expand=True)
+
+    root.mainloop()

+ 49 - 0
test/test-ui-2.py

@@ -0,0 +1,49 @@
+import tkinter as tk
+
+def show_card(card_number):
+    card1_frame.grid_forget()
+    card2_frame.grid_forget()
+    card3_frame.grid_forget()
+
+    if card_number == 1:
+        card1_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+    elif card_number == 2:
+        card2_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+    elif card_number == 3:
+        card3_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
+
+root = tk.Tk()
+root.title("模拟 CardLayout")
+
+# 创建一个容器 Frame
+container = tk.Frame(root)
+container.pack(fill="both", expand=True)
+
+# 创建三个 "卡片" Frame
+card1_frame = tk.Frame(container, bg="lightblue", padx=50, pady=50)
+card2_frame = tk.Frame(container, bg="lightgreen", padx=50, pady=50)
+card3_frame = tk.Frame(container, bg="lightyellow", padx=50, pady=50)
+
+card1_label = tk.Label(card1_frame, text="卡片 1")
+card1_label.pack()
+
+card2_label = tk.Label(card2_frame, text="卡片 2")
+card2_label.pack()
+
+card3_label = tk.Label(card3_frame, text="卡片 3")
+card3_label.pack()
+
+# 初始显示第一个卡片
+show_card(1)
+
+# 创建切换按钮
+button1 = tk.Button(root, text="显示卡片 1", command=lambda: show_card(1))
+button1.pack(pady=5)
+
+button2 = tk.Button(root, text="显示卡片 2", command=lambda: show_card(2))
+button2.pack(pady=5)
+
+button3 = tk.Button(root, text="显示卡片 3", command=lambda: show_card(3))
+button3.pack(pady=5)
+
+root.mainloop()

+ 87 - 0
test/test-ui-3.py

@@ -0,0 +1,87 @@
+import tkinter as tk
+from tkinter import ttk
+
+
+def show_tab(index):
+    notebook.select(index)  # 使用 notebook 的 select 方法切换到指定索引的 tab
+
+
+root = tk.Tk()
+root.title("自定义按钮切换 Notebook 内容")
+
+# 创建一个 Style 实例
+style = ttk.Style()
+
+# 为我们的 Notebook 创建一个新的样式名称
+notebook_style_name = "NoTabs.TNotebook"
+style.configure(notebook_style_name)  # 可以进行其他配置,如果需要
+
+# 移除这个新样式的 Tab 布局
+style.layout(notebook_style_name + ".Tab", [])
+
+notebook = ttk.Notebook(root, style=notebook_style_name)  # 将自定义样式应用到这个 Notebook
+notebook.pack(fill="both", expand=True)
+
+# 创建不同的内容 Frame
+tab1_content = ttk.Frame(notebook)
+label1 = tk.Label(tab1_content, text="这是选项卡 1 的内容", padx=50, pady=50)
+label1.pack(expand=True, fill="both")
+notebook.add(tab1_content, text="标签一")  # 虽然设置了 text,但我们会隐藏标签
+
+tab2_content = ttk.Frame(notebook)
+label2 = tk.Label(tab2_content, text="这是选项卡 2 的内容", padx=50, pady=50)
+label2.pack(expand=True, fill="both")
+notebook.add(tab2_content, text="标签二")  # 同样设置了 text
+
+tab3_content = ttk.Frame(notebook)
+label3 = tk.Label(tab3_content, text="这是选项卡 3 的内容", padx=50, pady=50)
+label3.pack(expand=True, fill="both")
+notebook.add(tab3_content, text="标签三")  # 同样设置了 text
+
+# 创建自定义按钮
+button1 = tk.Button(root, text="显示内容 1", command=lambda: show_tab(0))
+button1.pack(side="left", padx=5)
+
+button2 = tk.Button(root, text="显示内容 2", command=lambda: show_tab(1))
+button2.pack(side="left", padx=5)
+
+button3 = tk.Button(root, text="显示内容 3", command=lambda: show_tab(2))
+button3.pack(side="left", padx=5)
+
+root.mainloop()
+
+# import tkinter as tk
+# from tkinter import ttk
+#
+# def add_new_tab():
+#     global tab_count
+#     tab_count += 1
+#     new_tab = ttk.Frame(notebook)
+#     label = tk.Label(new_tab, text=f"这是动态添加的选项卡 {tab_count}", padx=50, pady=50)
+#     label.pack(expand=True, fill="both")
+#     notebook.add(new_tab, text=f"动态标签 {tab_count}")
+#
+# root = tk.Tk()
+# root.title("动态添加 Notebook 标签")
+#
+# notebook = ttk.Notebook(root)
+# notebook.pack(fill="both", expand=True)
+#
+# # 初始添加一些标签
+# tab1 = ttk.Frame(notebook)
+# label1 = tk.Label(tab1, text="初始选项卡 1", padx=50, pady=50)
+# label1.pack(expand=True, fill="both")
+# notebook.add(tab1, text="标签一")
+#
+# tab2 = ttk.Frame(notebook)
+# label2 = tk.Label(tab2, text="初始选项卡 2", padx=50, pady=50)
+# label2.pack(expand=True, fill="both")
+# notebook.add(tab2, text="标签二")
+#
+# tab_count = 2  # 记录已经添加的标签数量
+#
+# # 添加一个按钮,点击时动态添加新标签
+# add_button = tk.Button(root, text="添加新标签", command=add_new_tab)
+# add_button.pack(pady=10)
+#
+# root.mainloop()