commit cd6acdea610785546d242d243d1cf85b1ceca490 Author: youngkingdom <990708896@qq.com> Date: Wed Jun 25 18:18:27 2025 +0800 feat: 添加 POE-AutoFlask 项目 - 新增主程序文件 main.py - 添加项目图标和资源文件 - 实现自动喝药和自定义按键功能 - 添加异常处理和程序打包配置 - 编写项目 README 和依赖列表 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8edb971 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +dist +.venv +PyQt-Fluent-Widgets +.idea diff --git a/POE-AutoFlask.spec b/POE-AutoFlask.spec new file mode 100644 index 0000000..a30eb47 --- /dev/null +++ b/POE-AutoFlask.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[('icons/icon.png', 'icons')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='POE-AutoFlask', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['icons\\icon.ico'], +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..29caeb3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +pip install -r requirements.txt + +pyinstaller --name=POE-AutoFlask --onefile main.py --noconsole --add-data "icons/icon.png;icons" --icon=icons/icon.ico --clean \ No newline at end of file diff --git a/icons/icon.ico b/icons/icon.ico new file mode 100644 index 0000000..18fbeeb Binary files /dev/null and b/icons/icon.ico differ diff --git a/icons/icon.png b/icons/icon.png new file mode 100644 index 0000000..03543c1 Binary files /dev/null and b/icons/icon.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..82546c7 --- /dev/null +++ b/main.py @@ -0,0 +1,409 @@ +import os +import subprocess +import sys +import time +import traceback + +import keyboard +from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtGui import QKeySequence, QIcon, QIntValidator +from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, + QDialog, QMessageBox) +from qfluentwidgets import (ComboBox, LineEdit, CheckBox, PushButton, BodyLabel, StrongBodyLabel, + HeaderCardWidget) +from qfluentwidgets.common import setTheme, Theme + + +# 获取资源文件路径的函数 +def resource_path(relative_path): + """返回打包或开发环境下的资源路径""" + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + return os.path.join(os.path.abspath("."), relative_path) + +# KeyListenerOne: 负责功能区1(按“`”键模拟勾选的按键) +class KeyListenerOne(QThread): + def __init__(self, keys_to_send, delay): + super().__init__() + self.running = True # 控制线程运行状态 + self.is_active = True # 由App类控制的全局激活状态 + self.keys_to_send = keys_to_send # 要模拟的按键列表 + self.delay = delay / 1000.0 # 按键之间的延迟(秒) + + def run(self): + # 此线程不再注册F2热键,F2由App类全局管理 + while self.running: + # 只有当全局激活状态为True且“`”键被按下时才执行 + if self.is_active and keyboard.is_pressed('`'): + # 模拟勾选的按键 + for key in self.keys_to_send: + keyboard.write(key) + time.sleep(self.delay) # 按键之间的延迟 + # 功能区1不再需要每轮循环延迟 + time.sleep(0.01) # 避免CPU占用过高 + + def set_active(self, state): + """设置功能区1的激活状态。""" + self.is_active = state + + def stop(self): + """停止线程运行。""" + self.running = False + + +# KeyListenerTwo: 负责功能区2(启停循环自动输入) +class KeyListenerTwo(QThread): + # 添加一个信号,用于在状态改变时通知主UI + status_changed = pyqtSignal(bool) + + def __init__(self, auto_keys, auto_delay, auto_loop_delay, auto_key_trigger): + super().__init__() + self.running = True # 控制线程运行状态 + self.is_active = True # 由App类控制的全局激活状态 + self.individual_auto_toggle = False # 功能区2独立的启停状态 + self.auto_keys = auto_keys # 自动输入的按键列表 + self.auto_delay = auto_delay / 1000.0 # 自动输入按键之间的延迟(秒) + self.auto_loop_delay = auto_loop_delay / 1000.0 # 每轮自动输入循环之间的延迟(秒) + self.auto_key_trigger = auto_key_trigger # 启停自动输入的触发按键 + + def run(self): + # 注册功能区2独立的触发按键 + if self.auto_key_trigger: + # 使用 try-except 避免重复注册热键时出错 + try: + keyboard.add_hotkey(self.auto_key_trigger, self.toggle_individual_auto_typing) + except Exception as e: + print(f"Failed to register hotkey {self.auto_key_trigger}: {e}") + + while self.running: + # 只有当全局激活状态为True且功能区2被独立触发时才执行 + if self.is_active and self.individual_auto_toggle: + # 执行自动输入 + for key in self.auto_keys: + keyboard.write(key) + time.sleep(self.auto_delay) # 自动输入按键之间的延迟 + time.sleep(self.auto_loop_delay) # 每轮自动输入循环后的延迟 + time.sleep(0.01) # 避免CPU占用过高 + + def set_active(self, state): + """设置功能区2的全局激活状态。""" + self.is_active = state + + def toggle_individual_auto_typing(self): + """切换功能区2的独立自动输入状态。""" + self.individual_auto_toggle = not self.individual_auto_toggle + # 发射信号,将新的状态(True为启动,False为停止)传递出去 + self.status_changed.emit(self.individual_auto_toggle) + + def stop(self): + """停止线程运行并取消注册功能区2的触发热键。""" + self.running = False + if self.auto_key_trigger: + try: + # 尝试解绑热键 + keyboard.remove_hotkey(self.auto_key_trigger) + except KeyError: + pass # 忽略热键不存在的错误 + + +# KeyCaptureDialog: 用于捕获用户按键的对话框 +class KeyCaptureDialog(QDialog): + def __init__(self, prompt, max_length=5): + super().__init__() + self.setWindowTitle(prompt) + self.setModal(True) + self.resize(300, 100) + self.keys = [] # 捕获到的按键列表 + self.max_length = max_length # 最大捕获按键数量 + self.label = BodyLabel("请依次按下按键(最多{}个),按 Enter 完成".format(max_length), self) + layout = QVBoxLayout() + layout.addWidget(self.label) + self.setLayout(layout) + self.start_listening() + + def start_listening(self): + self.show() + self.grabKeyboard() # 捕获键盘输入 + + def keyPressEvent(self, event): + key = event.key() + if key in (Qt.Key_Return, Qt.Key_Enter): + self.done(0) # 完成对话框 + return + + if len(self.keys) < self.max_length: + key_seq = event.text() # 获取按键字符 + if key_seq: + self.keys.append(key_seq) + self.keys = list(set(self.keys)) + self.label.setText("已捕获: " + ', '.join(self.keys)) + + def closeEvent(self, event): + self.releaseKeyboard() # 释放键盘捕获 + event.accept() + + +# TriggerCaptureDialog: 用于捕获触发按键的对话框 +class TriggerCaptureDialog(QDialog): + def __init__(self): + super().__init__() + self.setWindowTitle("设置启停按键") + self.setModal(True) + self.resize(300, 100) + self.key = '' + self.label = BodyLabel("请按下用于启停自动输入的按键组合(按 Enter 完成)...", self) + layout = QVBoxLayout() + layout.addWidget(self.label) + self.setLayout(layout) + + def keyPressEvent(self, event): + if event.key() in (Qt.Key_Return, Qt.Key_Enter): + self.done(0) # 完成对话框 + return + + key_seq_str = QKeySequence(event.modifiers() | event.key()).toString(QKeySequence.NativeText) + if key_seq_str and key_seq_str.lower() not in ('enter', 'return'): + self.key = key_seq_str.lower() + self.label.setText(f"已捕获: {self.key}") + + def closeEvent(self, event): + self.releaseKeyboard() # 释放键盘捕获 + event.accept() + + +# App: 主应用程序窗口 +class App(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("POE-AutoFlask") + flags = Qt.Window | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint + self.setWindowFlags(flags) + self.setFixedSize(391,543) + self.setWindowIcon(QIcon(resource_path("icons/icon.png"))) + + # UI 元素初始化 + self._global_active = True + self.label = StrongBodyLabel("状态:监听中 (F2 切换)", self) + self.button = PushButton("停止监听 (F2) 切换", self) + self.button.setEnabled(True) + self.button.clicked.connect(self._toggle_global_active) + + # 功能区1的复选框布局 + self.checkbox_layout = QHBoxLayout() + self.checkboxes = [] + self.key_labels = ['1', '2', '3', '4', '5'] + for key in self.key_labels: + checkbox = CheckBox(key) + checkbox.setChecked(True) + self.checkbox_layout.addWidget(checkbox) + self.checkboxes.append(checkbox) + + # 功能区1的按键间延迟设置 + self.delay_combobox = ComboBox() + self.delay_options = ["50 ms", "100 ms", "200 ms", "300 ms", "500 ms", "1000 ms", "2000 ms", "5000 ms"] + self.delay_combobox.addItems(self.delay_options) + self.delay_combobox.setCurrentText("50 ms") + self.delay_combobox.setToolTip("按键之间的延迟") + + # 功能区1的布局 (移除每轮循环延迟) + feature1_text = BodyLabel("按键间延迟(ms):", self) + feature1_form = QHBoxLayout() + feature1_form.addWidget(feature1_text,1) + feature1_form.addWidget(self.delay_combobox,9) + + + feature1_group = HeaderCardWidget() + feature1_group.setBorderRadius(8) + feature1_group.setTitle("按`键自动喝药,可勾选取消") + feature1_layout = QVBoxLayout() + feature1_layout.addLayout(self.checkbox_layout) + feature1_layout.addLayout(feature1_form) + feature1_group.viewLayout.addLayout(feature1_layout) + + # 功能区2的初始化 + self.auto_keys = [] + self.auto_key_trigger = '' + + self.auto_delay_combobox = ComboBox() + self.auto_delay_combobox.addItems(self.delay_options) + self.auto_delay_combobox.setCurrentText("50 ms") + self.auto_delay_combobox.setToolTip("自动输入按键之间的延迟") + + # --- 修改:将每轮循环延迟的 QComboBox 改为 QLineEdit --- + self.auto_loop_delay_input = LineEdit() # 默认1000ms + self.auto_loop_delay_input.setText("1000") + self.auto_loop_delay_input.setValidator(QIntValidator(0, 999999)) # 限制只能输入0到999999的整数 + self.auto_loop_delay_input.setToolTip("每轮自动输入循环之间的延迟 (ms)") + + self.auto_keys_button = PushButton("设置自动输入按键") + self.auto_trigger_button = PushButton("设置启停按键") + + self.auto_keys_button.clicked.connect(self.set_auto_keys) + self.auto_trigger_button.clicked.connect(self.set_auto_trigger) + + self.auto_keys_display = BodyLabel("当前自动输入按键: 无") + self.auto_trigger_display = BodyLabel("当前启停按键: 无") + + self.feature2_widgets_to_disable = [ + self.auto_delay_combobox, + self.auto_loop_delay_input, + self.auto_keys_button, + self.auto_trigger_button, + ] + + # 功能区2的布局 + feature2_form = QFormLayout() + feature2_hLayout = QHBoxLayout() + feature2_hLayout.addWidget(BodyLabel("按键间延迟(ms):", self),1) + feature2_hLayout.addWidget(self.auto_delay_combobox,9) + feature2_form.addRow(feature2_hLayout) + feature2_hLayout2 = QHBoxLayout() + feature2_hLayout2.addWidget(BodyLabel("每轮循环延迟(ms):", self),1) + feature2_hLayout2.addWidget(self.auto_loop_delay_input,9) + feature2_form.addRow(feature2_hLayout2) + feature2_form.addRow(self.auto_keys_button) + feature2_form.addRow(self.auto_keys_display) + feature2_form.addRow(self.auto_trigger_button) + feature2_form.addRow(self.auto_trigger_display) + + feature2_group = HeaderCardWidget() + feature2_group.setBorderRadius(8) + feature2_group.setTitle("设置自定义启停,循环自动输入自定义") + # feature2_group = QGroupBox("功能区2:启停循环自动输入") + feature2_group.viewLayout.addLayout(feature2_form) + self.feature2_group = feature2_group # 保存对group的引用 + + # 主布局 + layout = QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(feature1_group) + layout.addWidget(feature2_group) + layout.addWidget(self.button) + self.setLayout(layout) + + # 初始化两个独立的监听线程 + self.listener_one = KeyListenerOne(self.get_enabled_keys(), + int(self.delay_combobox.currentText().replace(" ms", ""))) + self.listener_one.start() + + loop_delay_val = int(self.auto_loop_delay_input.text()) if self.auto_loop_delay_input.text() else 1000 + self.listener_two = KeyListenerTwo(self.auto_keys, + int(self.auto_delay_combobox.currentText().replace(" ms", "")), + loop_delay_val, + self.auto_key_trigger) + self.listener_two.status_changed.connect(self._update_feature2_ui_state) + self.listener_two.start() + + keyboard.add_hotkey('f2', self._toggle_global_active) + self._update_status_label() # 初始化状态标签 + + for cb in self.checkboxes: + cb.stateChanged.connect(self.update_keys) + self.delay_combobox.currentTextChanged.connect(self.update_delay) + self.auto_delay_combobox.currentTextChanged.connect(self.update_auto_delay) + self.auto_loop_delay_input.textChanged.connect(self.update_auto_loop_delay) + + def _toggle_global_active(self): + self._global_active = not self._global_active + self.listener_one.set_active(self._global_active) + self.listener_two.set_active(self._global_active) + self._update_status_label() + + def _update_status_label(self): + status_text = "状态:监听中 (F2 切换)" if self._global_active else "状态:已暂停监听 (F2 切换)" + button_text = "暂停所有功能 (F2 切换)" if self._global_active else "启动所有功能 (F2 切换)" + + self.label.setText(status_text) + self.button.setText(button_text) + + def _update_feature2_ui_state(self, is_running): + for widget in self.feature2_widgets_to_disable: + widget.setEnabled(not is_running) + + def get_enabled_keys(self): + return [cb.text() for cb in self.checkboxes if cb.isChecked()] + + def update_keys(self): + self.listener_one.keys_to_send = self.get_enabled_keys() + + def update_delay(self): + self.listener_one.delay = int(self.delay_combobox.currentText().replace(" ms", "")) / 1000.0 + + def update_auto_delay(self): + self.listener_two.auto_delay = int(self.auto_delay_combobox.currentText().replace(" ms", "")) / 1000.0 + + def update_auto_loop_delay(self): + try: + delay_ms = int(self.auto_loop_delay_input.text()) + self.listener_two.auto_loop_delay = delay_ms / 1000.0 + except (ValueError, AttributeError): + self.listener_two.auto_loop_delay = 1.0 + + def set_auto_keys(self): + dialog = KeyCaptureDialog("设置自动输入按键") + dialog.exec_() + self.auto_keys = dialog.keys + self.listener_two.auto_keys = self.auto_keys # 更新功能区2监听线程的按键 + self.auto_keys_display.setText("当前自动输入按键: " + ', '.join(self.auto_keys) if self.auto_keys else "无") + + def set_auto_trigger(self): + dialog = TriggerCaptureDialog() + dialog.exec_() + new_trigger_key = dialog.key + + if new_trigger_key and new_trigger_key != self.auto_key_trigger: + self.auto_key_trigger = new_trigger_key + self.auto_trigger_display.setText( + "当前启停按键: " + self.auto_key_trigger if self.auto_key_trigger else "无") + + self.listener_two.stop() + self.listener_two.wait() + + loop_delay_val = int(self.auto_loop_delay_input.text()) if self.auto_loop_delay_input.text() else 1000 + self.listener_two = KeyListenerTwo(self.auto_keys, + int(self.auto_delay_combobox.currentText().replace(" ms", "")), + loop_delay_val, + self.auto_key_trigger) + self.listener_two.status_changed.connect(self._update_feature2_ui_state) + self.listener_two.start() + elif not new_trigger_key: + self.auto_trigger_display.setText("当前启停按键: 无") + + def closeEvent(self, event): + self.listener_one.stop() + self.listener_one.wait() # 等待线程结束 + self.listener_two.stop() + self.listener_two.wait() # 等待线程结束 + # 在程序退出时全局解绑所有热键,确保清理 + keyboard.unhook_all() + event.accept() + + +# show_exception_dialog: 全局异常处理函数 +def show_exception_dialog(exc_type, exc_value, exc_traceback): + error_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback)) + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Critical) + msg_box.setWindowTitle("程序异常") + msg_box.setText("程序发生异常:\n" + str(exc_value)) + msg_box.setDetailedText(error_msg) + msg_box.setStandardButtons(QMessageBox.Retry | QMessageBox.Close) + result = msg_box.exec_() + if result == QMessageBox.Retry: + python = sys.executable + args = sys.argv[:] + args.insert(0, python) + sys.exit(subprocess.call(args)) + else: + sys.exit(1) + + +# 主程序入口 +if __name__ == '__main__': + sys.excepthook = show_exception_dialog # 设置全局异常处理 + + app = QApplication(sys.argv) + setTheme(Theme.LIGHT) # 设置主题为自动模式 + window = App() + window.show() + sys.exit(app.exec_()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a019b18 Binary files /dev/null and b/requirements.txt differ