- 新增主程序文件 main.py - 添加项目图标和资源文件 - 实现自动喝药和自定义按键功能 - 添加异常处理和程序打包配置 - 编写项目 README 和依赖列表
410 lines
17 KiB
Python
410 lines
17 KiB
Python
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_())
|