天龙八部拍卖行.py 24 KB

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