天龙八部拍卖行.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import ctypes
  2. import time
  3. import os
  4. import sys
  5. import platform
  6. import win32gui
  7. import win32con
  8. import win32api
  9. import win32clipboard as clipboard
  10. from PIL import ImageGrab
  11. import requests
  12. import io
  13. import tkinter as tk
  14. from tkinter import ttk, scrolledtext, messagebox
  15. import threading
  16. import json
  17. import pytesseract
  18. from PIL import Image
  19. import re
  20. import base64
  21. import cv2
  22. import numpy as np
  23. lastKeyWord = ""
  24. # ========== 获取程序目录 ==========
  25. def get_app_dir():
  26. if getattr(sys, 'frozen', False):
  27. return os.path.dirname(sys.executable)
  28. else:
  29. return os.path.dirname(os.path.abspath(__file__))
  30. APP_DIR = get_app_dir()
  31. # ========== 全局变量 ==========
  32. is_running = False
  33. stop_flag = False
  34. config_file = os.path.join(APP_DIR, "config.json")
  35. # ========== 核心功能类 ==========
  36. class AuctionBot:
  37. def __init__(self, log_callback):
  38. self.log_callback = log_callback
  39. self.target_hwnd = None
  40. self.left = 0
  41. self.top = 0
  42. self.is_running = False
  43. def log(self, message):
  44. if self.log_callback:
  45. self.log_callback(message)
  46. def get_hwnds_by_title_contains(self, title_contains):
  47. hwnds = []
  48. def enum_callback(hwnd, _):
  49. if win32gui.IsWindowVisible(hwnd):
  50. window_title = win32gui.GetWindowText(hwnd)
  51. if title_contains in window_title:
  52. hwnds.append(hwnd)
  53. return True
  54. win32gui.EnumWindows(enum_callback, None)
  55. return hwnds
  56. def get_client_rect(self, hwnd):
  57. rect = win32gui.GetClientRect(hwnd)
  58. point = win32gui.ClientToScreen(hwnd, (rect[0], rect[1]))
  59. return (point[0], point[1], point[0] + rect[2], point[1] + rect[3])
  60. def activate_window(self, hwnd):
  61. try:
  62. if win32gui.IsIconic(hwnd):
  63. win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
  64. win32gui.SetForegroundWindow(hwnd)
  65. win32gui.BringWindowToTop(hwnd)
  66. time.sleep(0.2)
  67. return True
  68. except Exception as e:
  69. self.log(f"激活窗口失败: {e}")
  70. return False
  71. def capture_window_area_to_bytes(self, x1, y1, x2, y2):
  72. try:
  73. screen_x1 = x1
  74. screen_y1 = y1
  75. screen_x2 = x2
  76. screen_y2 = y2
  77. screenshot = ImageGrab.grab(bbox=(screen_x1, screen_y1, screen_x2, screen_y2))
  78. img_bytes = io.BytesIO()
  79. screenshot.save(img_bytes, format='PNG')
  80. img_bytes.seek(0)
  81. return img_bytes.getvalue()
  82. except Exception as e:
  83. self.log(f"截图失败: {e}")
  84. return None
  85. def ocr_price(self, image_bytes):
  86. try:
  87. from PIL import Image
  88. img = Image.open(io.BytesIO(image_bytes))
  89. # 图片预处理:灰度化 + 放大(提升识别率)
  90. img = img.convert('L') # 转灰度
  91. # 放大2倍,小字识别更准
  92. width, height = img.size
  93. img = img.resize((width * 2, height * 2), Image.Resampling.LANCZOS)
  94. self.log("正在识别价格(本地Tesseract)...")
  95. # 只识别数字和小数点
  96. custom_config = r'--psm 8 -c tessedit_char_whitelist=0123456789.'
  97. text = pytesseract.image_to_string(img, config=custom_config)
  98. text = text.strip()
  99. if not text:
  100. self.log("未识别到任何文字")
  101. return None
  102. self.log(f"识别到原始文字: '{text}'")
  103. # 提取数字
  104. numbers = re.findall(r'(\d+\.?\d*)', text)
  105. if numbers:
  106. price = float(numbers[0])
  107. self.log(f"解析到价格: {price}")
  108. return price
  109. self.log(f"未识别到价格")
  110. return None
  111. except Exception as e:
  112. self.log(f"OCR识别异常: {e}")
  113. return None
  114. def find_image_on_screen(self, base64_str, threshold=0.8, region=None):
  115. """
  116. 在屏幕上查找base64编码的图片,返回中心坐标
  117. Args:
  118. base64_str: base64编码的图片字符串
  119. threshold: 匹配阈值 (0-1),默认0.8
  120. region: 搜索区域 (x1, y1, x2, y2),None表示全屏
  121. Returns:
  122. (x, y) 中心坐标,未找到返回 (None, None)
  123. """
  124. try:
  125. # 解码base64为图片
  126. img_data = base64.b64decode(base64_str)
  127. img_array = np.frombuffer(img_data, dtype=np.uint8)
  128. template = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
  129. if template is None:
  130. self.log("base64图片解码失败")
  131. return None, None
  132. # 截图
  133. if region:
  134. screenshot = ImageGrab.grab(bbox=region)
  135. else:
  136. screenshot = ImageGrab.grab()
  137. # 转为OpenCV格式
  138. screenshot_np = np.array(screenshot)
  139. screenshot_cv = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
  140. # 模板匹配
  141. result = cv2.matchTemplate(screenshot_cv, template, cv2.TM_CCOEFF_NORMED)
  142. min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
  143. self.log(f"匹配度: {max_val:.3f}")
  144. if max_val >= threshold:
  145. h, w = template.shape[:2]
  146. center_x = max_loc[0] + w // 2
  147. center_y = max_loc[1] + h // 2
  148. # 如果指定了region,加上偏移
  149. if region:
  150. center_x += region[0]
  151. center_y += region[1]
  152. return center_x, center_y
  153. return None, None
  154. except Exception as e:
  155. self.log(f"查找图片异常: {e}")
  156. return None, None
  157. def mouse_click_at(self, x, y, button=0):
  158. try:
  159. # 1. 移动物理光标(部分游戏会校验系统光标位置)
  160. win32api.SetCursorPos((x, y))
  161. time.sleep(0.1)
  162. # 2. 准备 SendInput 结构体(64/32位兼容)
  163. ULONG_PTR = ctypes.c_uint64 if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_uint32
  164. class MOUSEINPUT(ctypes.Structure):
  165. _fields_ = [
  166. ("dx", ctypes.c_long),
  167. ("dy", ctypes.c_long),
  168. ("mouseData", ctypes.c_ulong),
  169. ("dwFlags", ctypes.c_ulong),
  170. ("time", ctypes.c_ulong),
  171. ("dwExtraInfo", ULONG_PTR),
  172. ]
  173. class KEYBDINPUT(ctypes.Structure):
  174. _fields_ = [
  175. ("wVk", ctypes.c_ushort),
  176. ("wScan", ctypes.c_ushort),
  177. ("dwFlags", ctypes.c_ulong),
  178. ("time", ctypes.c_ulong),
  179. ("dwExtraInfo", ULONG_PTR),
  180. ]
  181. class HARDWAREINPUT(ctypes.Structure):
  182. _fields_ = [
  183. ("uMsg", ctypes.c_ulong),
  184. ("wParamL", ctypes.c_ushort),
  185. ("wParamH", ctypes.c_ushort),
  186. ]
  187. class DUMMYUNIONNAME(ctypes.Union):
  188. _fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT), ("hi", HARDWAREINPUT)]
  189. class INPUT(ctypes.Structure):
  190. _fields_ = [("type", ctypes.c_ulong), ("DUMMYUNIONNAME", DUMMYUNIONNAME)]
  191. INPUT_MOUSE = 0
  192. screen_w = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
  193. screen_h = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
  194. # 3. 转换为绝对坐标(0-65535)
  195. abs_x = int(x * 65535 / (screen_w - 1)) if screen_w > 1 else 0
  196. abs_y = int(y * 65535 / (screen_h - 1)) if screen_h > 1 else 0
  197. # 4. 选择按键
  198. if button == 0:
  199. down_flag = win32con.MOUSEEVENTF_LEFTDOWN
  200. up_flag = win32con.MOUSEEVENTF_LEFTUP
  201. else:
  202. down_flag = win32con.MOUSEEVENTF_RIGHTDOWN
  203. up_flag = win32con.MOUSEEVENTF_RIGHTUP
  204. move_abs = win32con.MOUSEEVENTF_ABSOLUTE | win32con.MOUSEEVENTF_MOVE
  205. # 5. 按下(带绝对坐标移动)
  206. inp_down = INPUT()
  207. inp_down.type = INPUT_MOUSE
  208. inp_down.DUMMYUNIONNAME.mi.dx = abs_x
  209. inp_down.DUMMYUNIONNAME.mi.dy = abs_y
  210. inp_down.DUMMYUNIONNAME.mi.mouseData = 0
  211. inp_down.DUMMYUNIONNAME.mi.dwFlags = move_abs | down_flag
  212. inp_down.DUMMYUNIONNAME.mi.time = 0
  213. inp_down.DUMMYUNIONNAME.mi.dwExtraInfo = 0
  214. ctypes.windll.user32.SendInput(1, ctypes.byref(inp_down), ctypes.sizeof(INPUT))
  215. # 关键:保持按下状态至少100ms,确保游戏能轮询到
  216. time.sleep(0.1)
  217. # 6. 释放
  218. inp_up = INPUT()
  219. inp_up.type = INPUT_MOUSE
  220. inp_up.DUMMYUNIONNAME.mi.dx = abs_x
  221. inp_up.DUMMYUNIONNAME.mi.dy = abs_y
  222. inp_up.DUMMYUNIONNAME.mi.mouseData = 0
  223. inp_up.DUMMYUNIONNAME.mi.dwFlags = move_abs | up_flag
  224. inp_up.DUMMYUNIONNAME.mi.time = 0
  225. inp_up.DUMMYUNIONNAME.mi.dwExtraInfo = 0
  226. ctypes.windll.user32.SendInput(1, ctypes.byref(inp_up), ctypes.sizeof(INPUT))
  227. time.sleep(0.1)
  228. return True
  229. except Exception as e:
  230. self.log(f"鼠标点击失败: {e}")
  231. return False
  232. def keyboard_ctrl_v(self):
  233. try:
  234. win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
  235. time.sleep(0.05)
  236. win32api.keybd_event(ord('V'), 0, 0, 0)
  237. time.sleep(0.05)
  238. win32api.keybd_event(ord('V'), 0, win32con.KEYEVENTF_KEYUP, 0)
  239. time.sleep(0.05)
  240. win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0)
  241. return True
  242. except Exception as e:
  243. self.log(f"Ctrl+V失败: {e}")
  244. return False
  245. def clipboard_set_text(self, text):
  246. try:
  247. clipboard.OpenClipboard()
  248. clipboard.EmptyClipboard()
  249. clipboard.SetClipboardText(text, clipboard.CF_TEXT)
  250. clipboard.CloseClipboard()
  251. return True
  252. except Exception as e:
  253. self.log(f"设置剪贴板失败: {e}")
  254. return False
  255. def process_item(self, keyword, target_price, baseX, baseY):
  256. global lastKeyWord
  257. """处理单个关键词"""
  258. if stop_flag:
  259. return False
  260. self.log(f"========== 处理: {keyword} (目标价格: {target_price}) ==========")
  261. if (lastKeyWord != keyword):
  262. # 点击重置
  263. click_x = baseX + 64
  264. click_y = baseY + 1
  265. self.log(f"点击重置: ({click_x}, {click_y})")
  266. self.mouse_click_at(click_x, click_y, 0)
  267. time.sleep(1)
  268. self.log(f"点击重置: ({click_x}, {click_y})")
  269. self.mouse_click_at(click_x, click_y, 0)
  270. time.sleep(1)
  271. # 设置剪贴板
  272. self.log(f"设置剪贴板: '{keyword}'")
  273. self.clipboard_set_text(keyword)
  274. # 点击输入框粘贴
  275. click_x = baseX -103
  276. click_y = baseY + 1
  277. self.log(f"点击输入框: ({click_x}, {click_y})")
  278. self.mouse_click_at(click_x, click_y, 0)
  279. time.sleep(1)
  280. # Ctrl+V
  281. self.log("执行 Ctrl+V")
  282. self.keyboard_ctrl_v()
  283. time.sleep(1)
  284. else:
  285. self.log(f"搜索关键词一样,自动忽略重置")
  286. lastKeyWord = keyword
  287. # 点击搜索按钮
  288. click_x = baseX
  289. click_y = baseY
  290. self.log(f"点击搜索按钮: ({click_x}, {click_y})")
  291. self.mouse_click_at(click_x, click_y, 0)
  292. time.sleep(2)
  293. # 截图识别价格
  294. self.log("截图识别价格...")
  295. img_bytes = self.capture_window_area_to_bytes(baseX - 86, baseY + 42, baseX - 31, baseY + 61)
  296. if not img_bytes:
  297. self.log("截图失败")
  298. return False
  299. price = self.ocr_price(img_bytes)
  300. if price is None:
  301. self.log("价格识别失败")
  302. return False
  303. self.log(f"识别到价格: {price}, 目标价格: {target_price}")
  304. if price <= target_price and price > 1:
  305. self.log(f"价格 {price} <= {target_price},执行购买!")
  306. # 选择购买项
  307. click_x = baseX - 86
  308. click_y = baseY + 42
  309. self.log(f"选择购买项: ({click_x}, {click_y})")
  310. self.mouse_click_at(click_x, click_y, 0)
  311. time.sleep(0.5)
  312. # 购买
  313. click_x = baseX + 62
  314. click_y = baseY + 395
  315. self.log(f"点击购买: ({click_x}, {click_y})")
  316. self.mouse_click_at(click_x, click_y, 0)
  317. time.sleep(0.5)
  318. # 确认购买
  319. 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)
  320. if x and y:
  321. self.log(f"找到购买位置: ({x}, {y})")
  322. self.mouse_click_at(x, y, 0)
  323. self.log(f"✅ {keyword} 购买成功! 价格: {price}")
  324. return True
  325. else:
  326. self.log(f"价格 {price} > {target_price},不购买")
  327. return False
  328. def run(self, keywords_prices, interval):
  329. """主循环"""
  330. global stop_flag
  331. stop_flag = False
  332. self.log("="*60)
  333. self.log("开始自动拍卖操作")
  334. self.log("="*60)
  335. # 查找窗口
  336. self.log("查找新天龙八部窗口...")
  337. window_list = self.get_hwnds_by_title_contains("新天龙八部")
  338. if not window_list:
  339. self.log("未找到新天龙八部窗口!")
  340. return
  341. self.target_hwnd = window_list[0]
  342. window_title = win32gui.GetWindowText(self.target_hwnd)
  343. self.log(f"找到窗口: {window_title}")
  344. # 获取窗口位置
  345. client_rect = self.get_client_rect(self.target_hwnd)
  346. self.left, self.top, right, bottom = client_rect
  347. self.top = self.top - 30
  348. self.log(f"窗口位置: left={self.left}, top={self.top}")
  349. # 激活窗口
  350. self.log("激活窗口...")
  351. self.activate_window(self.target_hwnd)
  352. time.sleep(1)
  353. round_count = 0
  354. # 检测基准坐标
  355. 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)
  356. if x and y:
  357. self.log(f"找到图片位置: ({x}, {y})")
  358. self.mouse_click_at(x, y, 0)
  359. else:
  360. self.log("未找到图片")
  361. while not stop_flag:
  362. round_count += 1
  363. self.log(f"\n========== 第 {round_count} 轮 ==========")
  364. for keyword, target_price in keywords_prices:
  365. if stop_flag:
  366. break
  367. try:
  368. self.process_item(keyword, target_price, x, y)
  369. time.sleep(1)
  370. except Exception as e:
  371. self.log(f"处理 {keyword} 时出错: {e}")
  372. if not stop_flag:
  373. self.log(f"等待 {interval} 秒后继续...")
  374. # 分段等待,便于检测停止信号
  375. for _ in range(interval):
  376. if stop_flag:
  377. break
  378. time.sleep(1)
  379. self.log("程序已停止")
  380. # ========== GUI程序 ==========
  381. class AuctionGUI:
  382. def __init__(self, root):
  383. self.root = root
  384. self.root.title("天龙八部自动拍卖机器人")
  385. self.root.geometry("800x700")
  386. self.root.resizable(True, True)
  387. self.bot = AuctionBot(self.log_message)
  388. self.keywords_list = []
  389. self.running = False
  390. self.setup_ui()
  391. self.load_config()
  392. def setup_ui(self):
  393. # 主框架
  394. main_frame = ttk.Frame(self.root, padding="10")
  395. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  396. # 配置区域
  397. config_frame = ttk.LabelFrame(main_frame, text="配置", padding="10")
  398. config_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
  399. # 关键词列表
  400. ttk.Label(config_frame, text="关键词和价格:").grid(row=0, column=0, sticky=tk.W)
  401. # 关键词表格
  402. columns = ('关键词', '目标价格')
  403. self.tree = ttk.Treeview(config_frame, columns=columns, show='headings', height=6)
  404. self.tree.heading('关键词', text='关键词')
  405. self.tree.heading('目标价格', text='目标价格')
  406. self.tree.column('关键词', width=150)
  407. self.tree.column('目标价格', width=100)
  408. self.tree.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(5, 5))
  409. # 滚动条
  410. scrollbar = ttk.Scrollbar(config_frame, orient=tk.VERTICAL, command=self.tree.yview)
  411. scrollbar.grid(row=1, column=4, sticky=(tk.N, tk.S))
  412. self.tree.configure(yscrollcommand=scrollbar.set)
  413. # 添加关键词
  414. ttk.Label(config_frame, text="关键词:").grid(row=2, column=0, sticky=tk.W, pady=(5, 0))
  415. self.keyword_entry = ttk.Entry(config_frame, width=15)
  416. self.keyword_entry.grid(row=3, column=0, sticky=tk.W, pady=(0, 5))
  417. ttk.Label(config_frame, text="目标价格:").grid(row=2, column=1, sticky=tk.W, pady=(5, 0))
  418. self.price_entry = ttk.Entry(config_frame, width=10)
  419. self.price_entry.grid(row=3, column=1, sticky=tk.W, pady=(0, 5))
  420. ttk.Button(config_frame, text="添加", command=self.add_keyword).grid(row=3, column=2, padx=(5, 0))
  421. ttk.Button(config_frame, text="删除选中", command=self.delete_keyword).grid(row=3, column=3, padx=(5, 0))
  422. # 循环间隔
  423. ttk.Label(config_frame, text="循环间隔(秒):").grid(row=4, column=0, sticky=tk.W, pady=(5, 0))
  424. self.interval_var = tk.StringVar(value="10")
  425. self.interval_entry = ttk.Entry(config_frame, textvariable=self.interval_var, width=10)
  426. self.interval_entry.grid(row=5, column=0, sticky=tk.W, pady=(0, 5))
  427. # 控制按钮
  428. control_frame = ttk.Frame(config_frame)
  429. control_frame.grid(row=5, column=1, columnspan=3, sticky=tk.E, pady=(0, 5))
  430. self.start_btn = ttk.Button(control_frame, text="开始", command=self.start_bot)
  431. self.start_btn.grid(row=0, column=0, padx=(0, 5))
  432. self.stop_btn = ttk.Button(control_frame, text="停止", command=self.stop_bot, state=tk.DISABLED)
  433. self.stop_btn.grid(row=0, column=1, padx=(0, 5))
  434. ttk.Button(control_frame, text="保存配置", command=self.save_config).grid(row=0, column=2, padx=(0, 5))
  435. ttk.Button(control_frame, text="加载配置", command=self.load_config).grid(row=0, column=3)
  436. # 日志区域
  437. log_frame = ttk.LabelFrame(main_frame, text="日志", padding="10")
  438. log_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  439. self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80)
  440. self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  441. # 配置网格权重
  442. self.root.columnconfigure(0, weight=1)
  443. self.root.rowconfigure(0, weight=1)
  444. main_frame.columnconfigure(0, weight=1)
  445. main_frame.rowconfigure(1, weight=1)
  446. log_frame.columnconfigure(0, weight=1)
  447. log_frame.rowconfigure(0, weight=1)
  448. def add_keyword(self):
  449. keyword = self.keyword_entry.get().strip()
  450. price_str = self.price_entry.get().strip()
  451. if not keyword:
  452. messagebox.showwarning("警告", "请输入关键词")
  453. return
  454. try:
  455. price = float(price_str)
  456. except ValueError:
  457. messagebox.showwarning("警告", "请输入有效的价格")
  458. return
  459. self.tree.insert('', 'end', values=(keyword, price))
  460. self.keyword_entry.delete(0, tk.END)
  461. self.price_entry.delete(0, tk.END)
  462. def delete_keyword(self):
  463. selected = self.tree.selection()
  464. if not selected:
  465. messagebox.showwarning("警告", "请先选中要删除的项目")
  466. return
  467. for item in selected:
  468. self.tree.delete(item)
  469. def get_keywords_list(self):
  470. items = self.tree.get_children()
  471. result = []
  472. for item in items:
  473. values = self.tree.item(item)['values']
  474. if values:
  475. result.append((values[0], float(values[1])))
  476. return result
  477. def log_message(self, message):
  478. self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n")
  479. self.log_text.see(tk.END)
  480. self.root.update_idletasks()
  481. def start_bot(self):
  482. keywords = self.get_keywords_list()
  483. if not keywords:
  484. messagebox.showwarning("警告", "请至少添加一个关键词")
  485. return
  486. try:
  487. interval = int(self.interval_var.get())
  488. if interval < 1:
  489. raise ValueError
  490. except ValueError:
  491. messagebox.showwarning("警告", "请输入有效的间隔秒数(大于0)")
  492. return
  493. self.running = True
  494. self.start_btn.config(state=tk.DISABLED)
  495. self.stop_btn.config(state=tk.NORMAL)
  496. # 在新线程中运行
  497. self.bot_thread = threading.Thread(
  498. target=self.bot.run,
  499. args=(keywords, interval)
  500. )
  501. self.bot_thread.daemon = True
  502. self.bot_thread.start()
  503. def stop_bot(self):
  504. global stop_flag
  505. stop_flag = True
  506. self.log_message("正在停止...")
  507. self.start_btn.config(state=tk.NORMAL)
  508. self.stop_btn.config(state=tk.DISABLED)
  509. self.running = False
  510. def save_config(self):
  511. keywords = self.get_keywords_list()
  512. config = {
  513. 'keywords': keywords,
  514. 'interval': self.interval_var.get()
  515. }
  516. try:
  517. with open(config_file, 'w', encoding='utf-8') as f:
  518. json.dump(config, f, ensure_ascii=False, indent=2)
  519. self.log_message(f"配置已保存到: {config_file}")
  520. messagebox.showinfo("成功", "配置已保存")
  521. except Exception as e:
  522. messagebox.showerror("错误", f"保存配置失败: {e}")
  523. def load_config(self):
  524. try:
  525. if not os.path.exists(config_file):
  526. return
  527. with open(config_file, 'r', encoding='utf-8') as f:
  528. config = json.load(f)
  529. # 清空当前列表
  530. for item in self.tree.get_children():
  531. self.tree.delete(item)
  532. # 加载关键词
  533. for keyword, price in config.get('keywords', []):
  534. self.tree.insert('', 'end', values=(keyword, price))
  535. # 加载间隔
  536. if 'interval' in config:
  537. self.interval_var.set(config['interval'])
  538. self.log_message("配置已加载")
  539. except Exception as e:
  540. messagebox.showerror("错误", f"加载配置失败: {e}")
  541. def main():
  542. root = tk.Tk()
  543. app = AuctionGUI(root)
  544. root.mainloop()
  545. if __name__ == "__main__":
  546. try:
  547. main()
  548. except Exception as e:
  549. print(f"程序出错: {e}")
  550. import traceback
  551. traceback.print_exc()