feat: 添加 POE-AutoFlask 项目

- 新增主程序文件 main.py
- 添加项目图标和资源文件
- 实现自动喝药和自定义按键功能
- 添加异常处理和程序打包配置
- 编写项目 README 和依赖列表
This commit is contained in:
2025-06-25 18:18:27 +08:00
commit cd6acdea61
7 changed files with 456 additions and 0 deletions

409
main.py Normal file
View File

@ -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_())