import json import queue import threading import tkinter as tk from dataclasses import asdict from pathlib import Path from tkinter import filedialog, messagebox, ttk from PIL import Image, ImageTk from .engine import Desktop, Matcher, Region, Runner, load_flow ROOT = Path(__file__).resolve().parent.parent LOCAL = ROOT / 'local' class Selector(tk.Toplevel): def __init__(self, parent, screenshot, on_select): super().__init__(parent) self.title('拖动框选游戏内容:不包含微信标题栏和窗口边框') self.original = screenshot self.on_select = on_select ratio = min(1000 / screenshot.width, 700 / screenshot.height, 1) self.preview_size = (round(screenshot.width * ratio), round(screenshot.height * ratio)) self.photo = ImageTk.PhotoImage(screenshot.resize(self.preview_size)) self.canvas = tk.Canvas(self, width=self.preview_size[0], height=self.preview_size[1], highlightthickness=0) self.canvas.pack() self.canvas.create_image(0, 0, image=self.photo, anchor='nw') self.start = None self.rect = None self.canvas.bind('', self.press) self.canvas.bind('', self.drag) self.canvas.bind('', self.release) def press(self, event): self.start = (event.x, event.y) if self.rect: self.canvas.delete(self.rect) self.rect = self.canvas.create_rectangle(event.x, event.y, event.x, event.y, outline='#ff3333', width=3) def drag(self, event): if self.start: self.canvas.coords(self.rect, *self.start, event.x, event.y) def release(self, event): if not self.start: return x1, x2 = sorted((self.start[0], event.x)) y1, y2 = sorted((self.start[1], event.y)) sx = self.original.width / self.preview_size[0] sy = self.original.height / self.preview_size[1] region = Region(round(x1 * sx), round(y1 * sy), round((x2 - x1) * sx), round((y2 - y1) * sy)) try: region.validate(self.original.size) except Exception as exc: messagebox.showerror('重新框选', str(exc), parent=self) return self.on_select(region) self.destroy() class App(tk.Tk): def __init__(self): super().__init__() self.title('蔚蓝星球 · 本地自动化') self.geometry('720x520') self.region = None self.events = queue.Queue() self.stop = threading.Event() self.busy = False self.flow = load_flow(ROOT / 'workflows' / 'pig_claim.json') self.matcher = Matcher(ROOT / 'assets') self.status = tk.StringVar(value='请先将游戏放在主屏幕,打开首页并校准游戏内容区域。') ttk.Label(self, text='第一条流程:小金猪挂机奖励领取', font=('', 18)).pack(pady=16) ttk.Label(self, text='开始前请保持游戏窗口可见且位置不变。鼠标移到主屏幕左上角可紧急停止。').pack() bar = ttk.Frame(self) bar.pack(pady=14) self.buttons = [] for name, cmd in [('校准游戏区域', self.capture), ('导入截图校准', self.import_image), ('预览流程', lambda: self.launch(True)), ('执行领取', lambda: self.launch(False))]: button = ttk.Button(bar, text=name, command=cmd) button.pack(side='left', padx=4) self.buttons.append(button) ttk.Button(self, text='停止', command=self.stop.set).pack() ttk.Label(self, textvariable=self.status, wraplength=680).pack(pady=10) self.output = tk.Text(self, height=16, wrap='word', state='disabled') self.output.pack(fill='both', expand=True, padx=16, pady=12) self.protocol('WM_DELETE_WINDOW', self.close) config = LOCAL / 'region.json' if config.exists(): try: self.region = Region(**json.loads(config.read_text())) self.status.set('已加载游戏区域;移动游戏窗口或更换屏幕后请重新校准。') except (ValueError, TypeError): pass self.after(100, self.poll) def log(self, text): self.events.put(('log', text)) def set_region(self, region): LOCAL.mkdir(exist_ok=True) (LOCAL / 'region.json').write_text(json.dumps(asdict(region)), encoding='utf-8') self.region = region self.status.set('校准完成。先预览,确认首页识别通过后再执行。') def capture(self): self.iconify() self.after(1500, self.take_capture) def take_capture(self): try: screenshot = Desktop().screenshot() except Exception as exc: self.deiconify() messagebox.showerror('截图失败', '请检查系统屏幕录制权限。\n' + str(exc)) return self.deiconify() Selector(self, screenshot, self.set_region) def import_image(self): name = filedialog.askopenfilename(filetypes=[('主屏幕完整截图', '*.png *.jpg *.jpeg')]) if name: with Image.open(name) as image: Selector(self, image.convert('RGB').copy(), self.set_region) def launch(self, dry_run): if self.busy: return if self.region is None: messagebox.showinfo('需要校准', '请先校准游戏内容区域。') return self.busy = True self.stop.clear() for button in self.buttons: button.configure(state='disabled') self.status.set('正在预览…' if dry_run else '3 秒后执行,请将游戏放到前台。') region = self.region def work(): try: if self.stop.wait(3): return Runner(Desktop(), self.matcher, region, self.log, self.stop).run(self.flow, dry_run) except Exception as exc: self.log('流程停止:' + str(exc)) finally: self.events.put(('done', None)) threading.Thread(target=work, daemon=True).start() def poll(self): while not self.events.empty(): kind, value = self.events.get_nowait() if kind == 'log': self.output.configure(state='normal') self.output.insert('end', value + '\n') self.output.see('end') self.output.configure(state='disabled') elif kind == 'done': self.busy = False for button in self.buttons: button.configure(state='normal') self.status.set('本轮已结束,请查看日志中的实际结果。') self.after(100, self.poll) def close(self): self.stop.set() self.destroy() def main(): App().mainloop()