import ctypes import time import os import sys import platform import win32gui import win32con import win32api import win32clipboard as clipboard from PIL import ImageGrab import requests import io import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import threading import json import pytesseract from PIL import Image import re import base64 import cv2 import numpy as np lastKeyWord = "" # ========== 获取程序目录 ========== def get_app_dir(): if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) else: return os.path.dirname(os.path.abspath(__file__)) APP_DIR = get_app_dir() # ========== 全局变量 ========== is_running = False stop_flag = False config_file = os.path.join(APP_DIR, "config.json") # ========== 核心功能类 ========== class AuctionBot: def __init__(self, log_callback): self.log_callback = log_callback self.target_hwnd = None self.left = 0 self.top = 0 self.is_running = False def log(self, message): if self.log_callback: self.log_callback(message) def get_hwnds_by_title_contains(self, title_contains): hwnds = [] def enum_callback(hwnd, _): if win32gui.IsWindowVisible(hwnd): window_title = win32gui.GetWindowText(hwnd) if title_contains in window_title: hwnds.append(hwnd) return True win32gui.EnumWindows(enum_callback, None) return hwnds def get_client_rect(self, hwnd): rect = win32gui.GetClientRect(hwnd) point = win32gui.ClientToScreen(hwnd, (rect[0], rect[1])) return (point[0], point[1], point[0] + rect[2], point[1] + rect[3]) def activate_window(self, hwnd): try: if win32gui.IsIconic(hwnd): win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) win32gui.SetForegroundWindow(hwnd) win32gui.BringWindowToTop(hwnd) time.sleep(0.2) return True except Exception as e: self.log(f"激活窗口失败: {e}") return False def capture_window_area_to_bytes(self, x1, y1, x2, y2): try: screen_x1 = x1 screen_y1 = y1 screen_x2 = x2 screen_y2 = y2 screenshot = ImageGrab.grab(bbox=(screen_x1, screen_y1, screen_x2, screen_y2)) img_bytes = io.BytesIO() screenshot.save(img_bytes, format='PNG') img_bytes.seek(0) return img_bytes.getvalue() except Exception as e: self.log(f"截图失败: {e}") return None def ocr_price(self, image_bytes): try: from PIL import Image img = Image.open(io.BytesIO(image_bytes)) # 图片预处理:灰度化 + 放大(提升识别率) img = img.convert('L') # 转灰度 # 放大2倍,小字识别更准 width, height = img.size img = img.resize((width * 2, height * 2), Image.Resampling.LANCZOS) self.log("正在识别价格(本地Tesseract)...") # 只识别数字和小数点 custom_config = r'--psm 6 -c tessedit_char_whitelist=0123456789.' text = pytesseract.image_to_string(img, config=custom_config) text = text.strip() if not text: self.log("未识别到任何文字") return None self.log(f"识别到原始文字: '{text}'") # 提取数字 numbers = re.findall(r'(\d+\.?\d*)', text) if numbers: price = float(numbers[0]) self.log(f"解析到价格: {price}") return price self.log(f"未识别到价格") return None except Exception as e: self.log(f"OCR识别异常: {e}") return None def find_image_on_screen(self, base64_str, threshold=0.8, region=None): """ 在屏幕上查找base64编码的图片,返回中心坐标 Args: base64_str: base64编码的图片字符串 threshold: 匹配阈值 (0-1),默认0.8 region: 搜索区域 (x1, y1, x2, y2),None表示全屏 Returns: (x, y) 中心坐标,未找到返回 (None, None) """ try: # 解码base64为图片 img_data = base64.b64decode(base64_str) img_array = np.frombuffer(img_data, dtype=np.uint8) template = cv2.imdecode(img_array, cv2.IMREAD_COLOR) if template is None: self.log("base64图片解码失败") return None, None # 截图 if region: screenshot = ImageGrab.grab(bbox=region) else: screenshot = ImageGrab.grab() # 转为OpenCV格式 screenshot_np = np.array(screenshot) screenshot_cv = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR) # 模板匹配 result = cv2.matchTemplate(screenshot_cv, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) self.log(f"匹配度: {max_val:.3f}") if max_val >= threshold: h, w = template.shape[:2] center_x = max_loc[0] + w // 2 center_y = max_loc[1] + h // 2 # 如果指定了region,加上偏移 if region: center_x += region[0] center_y += region[1] return center_x, center_y return None, None except Exception as e: self.log(f"查找图片异常: {e}") return None, None def mouse_click_at(self, x, y, button=0): try: # 1. 移动物理光标(部分游戏会校验系统光标位置) win32api.SetCursorPos((x, y)) time.sleep(0.1) # 2. 准备 SendInput 结构体(64/32位兼容) ULONG_PTR = ctypes.c_uint64 if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_uint32 class MOUSEINPUT(ctypes.Structure): _fields_ = [ ("dx", ctypes.c_long), ("dy", ctypes.c_long), ("mouseData", ctypes.c_ulong), ("dwFlags", ctypes.c_ulong), ("time", ctypes.c_ulong), ("dwExtraInfo", ULONG_PTR), ] class KEYBDINPUT(ctypes.Structure): _fields_ = [ ("wVk", ctypes.c_ushort), ("wScan", ctypes.c_ushort), ("dwFlags", ctypes.c_ulong), ("time", ctypes.c_ulong), ("dwExtraInfo", ULONG_PTR), ] class HARDWAREINPUT(ctypes.Structure): _fields_ = [ ("uMsg", ctypes.c_ulong), ("wParamL", ctypes.c_ushort), ("wParamH", ctypes.c_ushort), ] class DUMMYUNIONNAME(ctypes.Union): _fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT), ("hi", HARDWAREINPUT)] class INPUT(ctypes.Structure): _fields_ = [("type", ctypes.c_ulong), ("DUMMYUNIONNAME", DUMMYUNIONNAME)] INPUT_MOUSE = 0 screen_w = win32api.GetSystemMetrics(win32con.SM_CXSCREEN) screen_h = win32api.GetSystemMetrics(win32con.SM_CYSCREEN) # 3. 转换为绝对坐标(0-65535) abs_x = int(x * 65535 / (screen_w - 1)) if screen_w > 1 else 0 abs_y = int(y * 65535 / (screen_h - 1)) if screen_h > 1 else 0 # 4. 选择按键 if button == 0: down_flag = win32con.MOUSEEVENTF_LEFTDOWN up_flag = win32con.MOUSEEVENTF_LEFTUP else: down_flag = win32con.MOUSEEVENTF_RIGHTDOWN up_flag = win32con.MOUSEEVENTF_RIGHTUP move_abs = win32con.MOUSEEVENTF_ABSOLUTE | win32con.MOUSEEVENTF_MOVE # 5. 按下(带绝对坐标移动) inp_down = INPUT() inp_down.type = INPUT_MOUSE inp_down.DUMMYUNIONNAME.mi.dx = abs_x inp_down.DUMMYUNIONNAME.mi.dy = abs_y inp_down.DUMMYUNIONNAME.mi.mouseData = 0 inp_down.DUMMYUNIONNAME.mi.dwFlags = move_abs | down_flag inp_down.DUMMYUNIONNAME.mi.time = 0 inp_down.DUMMYUNIONNAME.mi.dwExtraInfo = 0 ctypes.windll.user32.SendInput(1, ctypes.byref(inp_down), ctypes.sizeof(INPUT)) # 关键:保持按下状态至少100ms,确保游戏能轮询到 time.sleep(0.1) # 6. 释放 inp_up = INPUT() inp_up.type = INPUT_MOUSE inp_up.DUMMYUNIONNAME.mi.dx = abs_x inp_up.DUMMYUNIONNAME.mi.dy = abs_y inp_up.DUMMYUNIONNAME.mi.mouseData = 0 inp_up.DUMMYUNIONNAME.mi.dwFlags = move_abs | up_flag inp_up.DUMMYUNIONNAME.mi.time = 0 inp_up.DUMMYUNIONNAME.mi.dwExtraInfo = 0 ctypes.windll.user32.SendInput(1, ctypes.byref(inp_up), ctypes.sizeof(INPUT)) time.sleep(0.1) return True except Exception as e: self.log(f"鼠标点击失败: {e}") return False def keyboard_ctrl_v(self): try: win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0) time.sleep(0.05) win32api.keybd_event(ord('V'), 0, 0, 0) time.sleep(0.05) win32api.keybd_event(ord('V'), 0, win32con.KEYEVENTF_KEYUP, 0) time.sleep(0.05) win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) return True except Exception as e: self.log(f"Ctrl+V失败: {e}") return False def clipboard_set_text(self, text): try: clipboard.OpenClipboard() clipboard.EmptyClipboard() clipboard.SetClipboardText(text, clipboard.CF_TEXT) clipboard.CloseClipboard() return True except Exception as e: self.log(f"设置剪贴板失败: {e}") return False def process_item(self, keyword, target_price, baseX, baseY): global lastKeyWord """处理单个关键词""" if stop_flag: return False self.log(f"========== 处理: {keyword} (目标价格: {target_price}) ==========") if (lastKeyWord != keyword): # 点击重置 click_x = baseX + 64 click_y = baseY + 1 self.log(f"点击重置: ({click_x}, {click_y})") self.mouse_click_at(click_x, click_y, 0) time.sleep(1) self.log(f"点击重置: ({click_x}, {click_y})") self.mouse_click_at(click_x, click_y, 0) time.sleep(1) # 设置剪贴板 self.log(f"设置剪贴板: '{keyword}'") self.clipboard_set_text(keyword) # 点击输入框粘贴 click_x = baseX -103 click_y = baseY + 1 self.log(f"点击输入框: ({click_x}, {click_y})") self.mouse_click_at(click_x, click_y, 0) time.sleep(1) # Ctrl+V self.log("执行 Ctrl+V") self.keyboard_ctrl_v() time.sleep(1) else: self.log(f"搜索关键词一样,自动忽略重置") lastKeyWord = keyword # 点击搜索按钮 click_x = baseX click_y = baseY self.log(f"点击搜索按钮: ({click_x}, {click_y})") self.mouse_click_at(click_x, click_y, 0) time.sleep(2) # 截图识别价格 self.log("截图识别价格...") img_bytes = self.capture_window_area_to_bytes(baseX - 86, baseY + 42, baseX - 31, baseY + 61) if not img_bytes: self.log("截图失败") return False price = self.ocr_price(img_bytes) if price is None: self.log("价格识别失败") return False self.log(f"识别到价格: {price}, 目标价格: {target_price}") if price <= target_price and price > 1: self.log(f"价格 {price} <= {target_price},执行购买!") # 选择购买项 click_x = baseX - 86 click_y = baseY + 42 self.log(f"选择购买项: ({click_x}, {click_y})") self.mouse_click_at(click_x, click_y, 0) time.sleep(0.5) # 购买 click_x = baseX + 62 click_y = baseY + 395 self.log(f"点击购买: ({click_x}, {click_y})") self.mouse_click_at(click_x, click_y, 0) time.sleep(0.5) # 确认购买 x, y = self.find_image_on_screen("iVBORw0KGgoAAAANSUhEUgAAAB0AAAAOCAYAAADT0Rc6AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAMUSURBVDhPfZRbTxNRFIUHeVEQJFGeTbzhBZUCwfhmlJaZdooU/DsGL/FCCDTWCsjFCghiYuKv8YVEeplWYvwFauhyrT2dCmp4+LLPmbP3WWfvfc449845GD/7h8w+xrV2CBPnD9Jc4zij+QVaMvYXTpqLPh2bcD522UGKi35PaDO9DhDM2zx90cHIGQejtCn6G4wRtn6JMB7lKaQ4TtLP4FqEk6RzhMcNfnxeBkp5oPKCgYuGL1HNvy9RfIGswWXwiOJotTnKa/R5B3zb5DjHsaBvucD9+L34nvMtJK/sE1VGQsJ3JUKxpE6qDUtZc3aVQW0Wo9foy7FEE/Svf3lLn3X4/KZD6Put07QaC+4rUP6EJH1MNBLM9LZiVCLFZSuJy2/1HZ62xiyDp+QBsPuM8yxPvWTrEog21QHivAtClYhzPtwgTr+If8qrkyLIU2wKqL62kyKYQeqqsnwI/zp7x0xRzYZijJP9tc3SV9YJ21OdCQk4VskrG+RDM+umqAnyw972CgNeWvlQzMFlxgimUS8xy9okuR/a6ixcxrmMM8vYNNvisVKeYsuPMcJxE+7zf1HalDLbLYSiwSI8zcvP4Vumk9Zb9QWVeYvxWEqzjEWpEGZPsJO1A4umaGPtQHl13X1lxn753BzFuYboNMePmN0TrpEyS1dZMf9IVFVCddXidKFQU2l5wazkvNnlrfAO0NfRCfejC6Seqo8o5pFWhs0y58Ly2cFW7cIkJMrNVFpUJZQLxzt5S8TnWvSOdThZR+rWEzLMRx8+kQW4siyv+qAbnFAmzE5v1i4XM4gevcRR2rTYlCpUesXMWC0dkEJ3+HxkVUVVxElQzOBHt6eFpWnBbYprc+sdfwTqg/0Agq0wC/YOwYaJmuDOx/CNalMJ0Ybii9grzQFf36BeKeAn33OC644Xa4cXOw6vr4OcQDLWBX+gC3G+Wa+PZRsULaS1gcbh97Gho5i42YnRwTZkhjpIJ/z+NiT7SOwI3H4mMkDom7pBhkQrnJNtDpq0N2jMT3HcvY9T+hZx7HC6Bf0OYN8d/AaXfRtF9M3LCQAAAABJRU5ErkJggg==", threshold=0.85) if x and y: self.log(f"找到购买位置: ({x}, {y})") self.mouse_click_at(x, y, 0) self.log(f"✅ {keyword} 购买成功! 价格: {price}") return True else: self.log(f"价格 {price} > {target_price},不购买") return False def run(self, keywords_prices, interval): """主循环""" global stop_flag stop_flag = False self.log("="*60) self.log("开始自动拍卖操作") self.log("="*60) # 查找窗口 self.log("查找新天龙八部窗口...") window_list = self.get_hwnds_by_title_contains("新天龙八部") if not window_list: self.log("未找到新天龙八部窗口!") return self.target_hwnd = window_list[0] window_title = win32gui.GetWindowText(self.target_hwnd) self.log(f"找到窗口: {window_title}") # 获取窗口位置 client_rect = self.get_client_rect(self.target_hwnd) self.left, self.top, right, bottom = client_rect self.top = self.top - 30 self.log(f"窗口位置: left={self.left}, top={self.top}") # 激活窗口 self.log("激活窗口...") self.activate_window(self.target_hwnd) time.sleep(1) round_count = 0 # 检测基准坐标 x, y = self.find_image_on_screen("iVBORw0KGgoAAAANSUhEUgAAADQAAAAMCAYAAAAtUZ4rAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAVUSURBVEhLTZbpblNXFIVNGKJS9SU6JCR0pFV/VhAP1zMEStVnqdQyBEgRCZkTEkhIoEWqKlV9njaJr+1rhydoiYe7+6197ZIfS+fcc/a819l2qnIhZTfGB9AeXGcvfHcxWXV2cu8yAwy/T55Pj6Xs2ofoTLI/oeP3A9tD6Exy33/89l7rt+CmgK0b2Jp+n7sPOP/oLW4iKwy/r4FUgEKJjYO97a9YGQcVEhDKE5wdLFhFZ6CMkypnVa1geO5A3mq3rUJwLoucUEJOKGpF5iTKn6BTn7MKq1CWrsBdFRvX2ZdJqEJ804qlvgEem4VDzPP9xEokG4BUhgxzCDuUUH3Rihjuh/ctrj+0AoZjFIV+bcHi2poVcaQAPYFwB7lV9FbMmnNm0Y/gJ/YLZo1n1tvfsTyyBZAnoDjcRn4lQYOAIvw0Z9g/AovoLaP3BHsvrYSfgGQKxJZnLRGrHS7a9OeJPd0XBkkWuQvGRyyVZpNGoV/bxfg6xnEQ3TJr37PCpxgiubixZFkFRLW6tSXLk4gMVbnPyehnGJcTvqWbZ80io2KpC3mxQAWYHPHul6WvgLAXh7eshA8hDu94xxSoAiyiJ+SJT0mpw9Zctav4q+BDhVXBjYIGyE+RtCeURSmHcMBl3LxtAUatdRs8sACn1lJFV+kanWivW4ChQMajP3Gwa/0G3Ymo7tE8+vdYH3G+hKM1KLhm8f62VShAceyUB2r1X8Azqo1M+JCO3Ad3wQzfs6ybNi2qjZ91nQI6Dvmke90DmBBuWv8Q+h1twponNsUbu6KElExBlUA4UcBw9AOB3/GqFVWBaM4r7ffRhhWUkFrd/MNppG9VzOiuqq53EB9seaVVzd7+HgGdstLEiFfV6tt+V3K67FoV+asqIpQry07Im+KuPPYusZ2lw6fBGcuTlJikuNIEL1vqjIWvLBAtJ0YHCfFRVeDN5xidTYJrUzW4bQ069VoVpzuNp1R8y+mmDlUJ1t+FKtWUDDotqldf8OSLA7kM9rN6C+xFkf7helKcw189SMkJ/h7aD7Cz4BQtX3iPQox60HqHKqIoLsrrmXgxB7pKSCzzoaB2dv96kdAIY6KUHBepnAaERTPJN1AgCiI7BN85DKpq0lNABfSSYbLkNhVQVgGwl01/9OFe0q2IIRBBu9bP4KHpCdjRjgdamjyPDp1hryHSY1D1KLi9XnafVt9ElyZEO0D7PUtNYUBOc6zKUJ1Ia21QdYaERTiKZIR943kixzTJQSEVo9/YI4lt7uByY5k3NOtTS9OrHy7Y8d8vfXoqIflRAaz11Mp0V4lba95yrIK1Z7wDmo7qanFyFD/QjTPZzSJzGf24dc+yXox1q36R2FHhe4zz1BUU5ETBZVj7zTWfUJpaFr1KnCEcR1tWhIqSy46fQWck6Q6ymmrd2p4PkH5zwx3HdPr610peyZwHo97hLJXt1DYswyrdfosCIp+Wj6M7PlU7+0sJrYktGDvnHRKVA2IqfSUdpuGX8rVqlUv4R18xdBoPLHWZSZK/eNodpEGvuW5pss8M0AmZYtGuj2YfxcgocR/3rDo7rtE5jGoft6AuQXmXm78xQU/9n5AoqkkqalQujZrY0cN2t71infZj67UfeZLG1PQOTUC3C+ecOT3OOnS+C/31pgt0pg/N4hY/N60taDpr/aNF3tDkO27kuLZjx+GadaFanuA79SX2W95aJfEvD/mfg2US3EuS8mRfJN/D5GsvPTl1Sp1QIMf7vydv7cRPQ7f2wt+AoCJeQfcbMKXONvTLv+j/GkTVPL9doqHsZtDNqRP8hGh9U3tqGXWXc7HiTf2Z/QdO9jewUA8g2gAAAABJRU5ErkJggg==", threshold=0.85) if x and y: self.log(f"找到图片位置: ({x}, {y})") self.mouse_click_at(x, y, 0) else: self.log("未找到图片") while not stop_flag: round_count += 1 self.log(f"\n========== 第 {round_count} 轮 ==========") for keyword, target_price in keywords_prices: if stop_flag: break try: self.process_item(keyword, target_price, x, y) time.sleep(1) except Exception as e: self.log(f"处理 {keyword} 时出错: {e}") if not stop_flag: self.log(f"等待 {interval} 秒后继续...") # 分段等待,便于检测停止信号 for _ in range(interval): if stop_flag: break time.sleep(1) self.log("程序已停止") # ========== GUI程序 ========== class AuctionGUI: def __init__(self, root): self.root = root self.root.title("天龙八部自动拍卖机器人") self.root.geometry("800x700") self.root.resizable(True, True) self.bot = AuctionBot(self.log_message) self.keywords_list = [] self.running = False self.setup_ui() self.load_config() def setup_ui(self): # 主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 配置区域 config_frame = ttk.LabelFrame(main_frame, text="配置", padding="10") config_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) # 关键词列表 ttk.Label(config_frame, text="关键词和价格:").grid(row=0, column=0, sticky=tk.W) # 关键词表格 columns = ('关键词', '目标价格') self.tree = ttk.Treeview(config_frame, columns=columns, show='headings', height=6) self.tree.heading('关键词', text='关键词') self.tree.heading('目标价格', text='目标价格') self.tree.column('关键词', width=150) self.tree.column('目标价格', width=100) self.tree.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(5, 5)) # 滚动条 scrollbar = ttk.Scrollbar(config_frame, orient=tk.VERTICAL, command=self.tree.yview) scrollbar.grid(row=1, column=4, sticky=(tk.N, tk.S)) self.tree.configure(yscrollcommand=scrollbar.set) # 添加关键词 ttk.Label(config_frame, text="关键词:").grid(row=2, column=0, sticky=tk.W, pady=(5, 0)) self.keyword_entry = ttk.Entry(config_frame, width=15) self.keyword_entry.grid(row=3, column=0, sticky=tk.W, pady=(0, 5)) ttk.Label(config_frame, text="目标价格:").grid(row=2, column=1, sticky=tk.W, pady=(5, 0)) self.price_entry = ttk.Entry(config_frame, width=10) self.price_entry.grid(row=3, column=1, sticky=tk.W, pady=(0, 5)) ttk.Button(config_frame, text="添加", command=self.add_keyword).grid(row=3, column=2, padx=(5, 0)) ttk.Button(config_frame, text="删除选中", command=self.delete_keyword).grid(row=3, column=3, padx=(5, 0)) # 循环间隔 ttk.Label(config_frame, text="循环间隔(秒):").grid(row=4, column=0, sticky=tk.W, pady=(5, 0)) self.interval_var = tk.StringVar(value="10") self.interval_entry = ttk.Entry(config_frame, textvariable=self.interval_var, width=10) self.interval_entry.grid(row=5, column=0, sticky=tk.W, pady=(0, 5)) # 控制按钮 control_frame = ttk.Frame(config_frame) control_frame.grid(row=5, column=1, columnspan=3, sticky=tk.E, pady=(0, 5)) self.start_btn = ttk.Button(control_frame, text="开始", command=self.start_bot) self.start_btn.grid(row=0, column=0, padx=(0, 5)) self.stop_btn = ttk.Button(control_frame, text="停止", command=self.stop_bot, state=tk.DISABLED) self.stop_btn.grid(row=0, column=1, padx=(0, 5)) ttk.Button(control_frame, text="保存配置", command=self.save_config).grid(row=0, column=2, padx=(0, 5)) ttk.Button(control_frame, text="加载配置", command=self.load_config).grid(row=0, column=3) # 日志区域 log_frame = ttk.LabelFrame(main_frame, text="日志", padding="10") log_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80) self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 配置网格权重 self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(1, weight=1) log_frame.columnconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1) def add_keyword(self): keyword = self.keyword_entry.get().strip() price_str = self.price_entry.get().strip() if not keyword: messagebox.showwarning("警告", "请输入关键词") return try: price = float(price_str) except ValueError: messagebox.showwarning("警告", "请输入有效的价格") return self.tree.insert('', 'end', values=(keyword, price)) self.keyword_entry.delete(0, tk.END) self.price_entry.delete(0, tk.END) def delete_keyword(self): selected = self.tree.selection() if not selected: messagebox.showwarning("警告", "请先选中要删除的项目") return for item in selected: self.tree.delete(item) def get_keywords_list(self): items = self.tree.get_children() result = [] for item in items: values = self.tree.item(item)['values'] if values: result.append((values[0], float(values[1]))) return result def log_message(self, message): self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n") self.log_text.see(tk.END) self.root.update_idletasks() def start_bot(self): keywords = self.get_keywords_list() if not keywords: messagebox.showwarning("警告", "请至少添加一个关键词") return try: interval = int(self.interval_var.get()) if interval < 1: raise ValueError except ValueError: messagebox.showwarning("警告", "请输入有效的间隔秒数(大于0)") return self.running = True self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) # 在新线程中运行 self.bot_thread = threading.Thread( target=self.bot.run, args=(keywords, interval) ) self.bot_thread.daemon = True self.bot_thread.start() def stop_bot(self): global stop_flag stop_flag = True self.log_message("正在停止...") self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.running = False def save_config(self): keywords = self.get_keywords_list() config = { 'keywords': keywords, 'interval': self.interval_var.get() } try: with open(config_file, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2) self.log_message(f"配置已保存到: {config_file}") messagebox.showinfo("成功", "配置已保存") except Exception as e: messagebox.showerror("错误", f"保存配置失败: {e}") def load_config(self): try: if not os.path.exists(config_file): return with open(config_file, 'r', encoding='utf-8') as f: config = json.load(f) # 清空当前列表 for item in self.tree.get_children(): self.tree.delete(item) # 加载关键词 for keyword, price in config.get('keywords', []): self.tree.insert('', 'end', values=(keyword, price)) # 加载间隔 if 'interval' in config: self.interval_var.set(config['interval']) self.log_message("配置已加载") except Exception as e: messagebox.showerror("错误", f"加载配置失败: {e}") def main(): root = tk.Tk() app = AuctionGUI(root) root.mainloop() if __name__ == "__main__": try: main() except Exception as e: print(f"程序出错: {e}") import traceback traceback.print_exc()