| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- 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()
|