|
@@ -0,0 +1,677 @@
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+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")
|
|
|
|
|
+
|
|
|
|
|
+# ========== 幽灵键鼠加载 ==========
|
|
|
|
|
+def load_ghost_key_mouse():
|
|
|
|
|
+ if platform.architecture()[0] == "64bit":
|
|
|
|
|
+ dll_path = os.path.join(APP_DIR, "gbild64.dll")
|
|
|
|
|
+ else:
|
|
|
|
|
+ dll_path = os.path.join(APP_DIR, "gbild32.dll")
|
|
|
|
|
+
|
|
|
|
|
+ if not os.path.exists(dll_path):
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ dll = ctypes.windll.LoadLibrary(dll_path)
|
|
|
|
|
+ return dll
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ghost_dll = load_ghost_key_mouse()
|
|
|
|
|
+
|
|
|
|
|
+if ghost_dll:
|
|
|
|
|
+ ghost_dll.getmodel.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.getserialnumber.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.getproductiondate.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.getfirmwareversion.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.getclientscreenresolution.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.readstring.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.encryptstring.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.decryptstring.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.getproductname.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.sdktype.restype = ctypes.c_char_p
|
|
|
|
|
+ ghost_dll.sdkversion.restype = ctypes.c_char_p
|
|
|
|
|
+
|
|
|
|
|
+ def opendevice(index=0):
|
|
|
|
|
+ return ghost_dll.opendevice(index)
|
|
|
|
|
+
|
|
|
|
|
+ def closedevice():
|
|
|
|
|
+ return ghost_dll.closedevice()
|
|
|
|
|
+
|
|
|
|
|
+ def pressandreleasemousebutton(mbtn):
|
|
|
|
|
+ return ghost_dll.pressandreleasemousebutton(mbtn)
|
|
|
|
|
+
|
|
|
|
|
+ def combinationkey(key_sequence):
|
|
|
|
|
+ return ghost_dll.combinationkey(key_sequence)
|
|
|
|
|
+
|
|
|
|
|
+ def pressandreleasekeybyname(key_name):
|
|
|
|
|
+ return ghost_dll.pressandreleasekeybyname(key_name)
|
|
|
|
|
+
|
|
|
|
|
+ device_id = opendevice(0)
|
|
|
|
|
+ if device_id == 0:
|
|
|
|
|
+ ghost_available = False
|
|
|
|
|
+ else:
|
|
|
|
|
+ ghost_available = True
|
|
|
|
|
+else:
|
|
|
|
|
+ ghost_available = False
|
|
|
|
|
+
|
|
|
|
|
+# ========== 核心功能类 ==========
|
|
|
|
|
+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, hwnd, x1, y1, x2, y2):
|
|
|
|
|
+ try:
|
|
|
|
|
+ client_rect = self.get_client_rect(hwnd)
|
|
|
|
|
+ left, top, right, bottom = client_rect
|
|
|
|
|
+
|
|
|
|
|
+ screen_x1 = left + x1
|
|
|
|
|
+ screen_y1 = top + y1
|
|
|
|
|
+ screen_x2 = left + x2
|
|
|
|
|
+ screen_y2 = top + 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:
|
|
|
|
|
+ # url = "http://111.229.45.38:19100/ocr"
|
|
|
|
|
+ url = "http://192.168.1.101:10001/ocr"
|
|
|
|
|
+ files = {'image': ('screenshot.png', image_bytes, 'image/png')}
|
|
|
|
|
+
|
|
|
|
|
+ self.log("正在识别价格...")
|
|
|
|
|
+ response = requests.post(url, files=files, timeout=30)
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ self.log(f"OCR请求失败: HTTP {response.status_code}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ result = response.json()
|
|
|
|
|
+
|
|
|
|
|
+ if not result.get('success', False):
|
|
|
|
|
+ self.log(f"OCR识别失败: {result}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ texts = result.get('texts', [])
|
|
|
|
|
+ if not texts:
|
|
|
|
|
+ self.log("未识别到任何文字")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ all_texts = []
|
|
|
|
|
+ for item in texts:
|
|
|
|
|
+ rec_texts = item.get('rec_texts', [])
|
|
|
|
|
+ all_texts.extend(rec_texts)
|
|
|
|
|
+
|
|
|
|
|
+ for text in all_texts:
|
|
|
|
|
+ try:
|
|
|
|
|
+ price = float(text)
|
|
|
|
|
+ self.log(f"识别到价格: {price}")
|
|
|
|
|
+ return price
|
|
|
|
|
+ except ValueError:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ self.log(f"未识别到价格,识别到的文字: {all_texts}")
|
|
|
|
|
+ return None
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ self.log(f"OCR识别异常: {e}")
|
|
|
|
|
+ return 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:
|
|
|
|
|
+ if ghost_available:
|
|
|
|
|
+ return combinationkey(b"ctrl+v") == 1
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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):
|
|
|
|
|
+ global lastKeyWord
|
|
|
|
|
+ """处理单个关键词"""
|
|
|
|
|
+ if stop_flag:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ self.log(f"========== 处理: {keyword} (目标价格: {target_price}) ==========")
|
|
|
|
|
+ if (lastKeyWord != keyword):
|
|
|
|
|
+ # 点击重置
|
|
|
|
|
+ click_x = self.left + 728
|
|
|
|
|
+ click_y = self.top + 152
|
|
|
|
|
+ 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 = self.left + 568
|
|
|
|
|
+ click_y = self.top + 152
|
|
|
|
|
+ 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 = self.left + 664
|
|
|
|
|
+ click_y = self.top + 152
|
|
|
|
|
+ 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(self.target_hwnd, 580, 190 - 30, 634, 211 - 30)
|
|
|
|
|
+
|
|
|
|
|
+ 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 = self.left + 506
|
|
|
|
|
+ click_y = self.top + 202
|
|
|
|
|
+ self.log(f"选择购买项: ({click_x}, {click_y})")
|
|
|
|
|
+ self.mouse_click_at(click_x, click_y, 0)
|
|
|
|
|
+ time.sleep(0.5)
|
|
|
|
|
+
|
|
|
|
|
+ # 购买
|
|
|
|
|
+ click_x = self.left + 729
|
|
|
|
|
+ click_y = self.top + 545
|
|
|
|
|
+ self.log(f"点击购买: ({click_x}, {click_y})")
|
|
|
|
|
+ self.mouse_click_at(click_x, click_y, 0)
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # 确认购买
|
|
|
|
|
+ click_x = self.left + 516
|
|
|
|
|
+ click_y = self.top + 286
|
|
|
|
|
+ self.log(f"确认购买: ({click_x}, {click_y})")
|
|
|
|
|
+ self.mouse_click_at(click_x, click_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
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ if not ghost_available:
|
|
|
|
|
+ result = messagebox.askyesno("警告", "幽灵键鼠未连接,点击将使用系统API,是否继续?")
|
|
|
|
|
+ if not result:
|
|
|
|
|
+ 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()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ if ghost_available:
|
|
|
|
|
+ try:
|
|
|
|
|
+ closedevice()
|
|
|
|
|
+ except:
|
|
|
|
|
+ pass
|