项目重构
This commit is contained in:
parent
9a76798602
commit
b9c2a31a5a
|
|
@ -0,0 +1,592 @@
|
|||
# 开发者指南
|
||||
|
||||
本文档为 KeyPressRemark 项目的开发者提供详细的技术指导和代码贡献指南。
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [开发环境设置](#开发环境设置)
|
||||
- [项目架构](#项目架构)
|
||||
- [核心技术](#核心技术)
|
||||
- [代码规范](#代码规范)
|
||||
- [开发流程](#开发流程)
|
||||
- [测试指南](#测试指南)
|
||||
- [调试技巧](#调试技巧)
|
||||
- [性能优化](#性能优化)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
## 🛠️ 开发环境设置
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **操作系统**: Windows 10/11 (开发和测试)
|
||||
- **Python**: 3.11+ (推荐 3.11.x)
|
||||
- **IDE**: PyCharm / VSCode (推荐)
|
||||
- **版本控制**: Git
|
||||
|
||||
### 开发环境配置
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd KeyPressRemark
|
||||
```
|
||||
|
||||
2. **创建虚拟环境**
|
||||
```bash
|
||||
# 使用 uv (推荐)
|
||||
uv venv --python 3.11
|
||||
.venv\Scripts\activate
|
||||
|
||||
# 或使用 venv
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. **安装依赖**
|
||||
```bash
|
||||
# 开发依赖(包含测试工具)
|
||||
uv pip install -r requirements.txt
|
||||
uv add --dev pytest pytest-qt black isort flake8
|
||||
```
|
||||
|
||||
4. **IDE 配置**
|
||||
- 设置 Python 解释器为虚拟环境中的 Python
|
||||
- 配置代码格式化工具(Black)
|
||||
- 启用类型检查(mypy)
|
||||
|
||||
### 调试配置
|
||||
|
||||
**VSCode launch.json**:
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "KeyPressRemark",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/main.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ 项目架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
```
|
||||
KeyPressRemark/
|
||||
├── src/ # 源代码
|
||||
│ ├── core/ # 核心功能层
|
||||
│ ├── gui/ # 用户界面层
|
||||
│ ├── utils/ # 工具库层
|
||||
│ └── main.py # 应用入口
|
||||
├── resources/ # 资源文件
|
||||
├── tests/ # 测试代码
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
### 架构原则
|
||||
|
||||
1. **分层架构**: 核心功能、界面、工具明确分离
|
||||
2. **依赖注入**: 通过构造函数传递依赖
|
||||
3. **单一职责**: 每个模块只负责一个功能域
|
||||
4. **开闭原则**: 对扩展开放,对修改封闭
|
||||
|
||||
### 模块依赖关系
|
||||
|
||||
```
|
||||
main.py
|
||||
├── gui.main_window (界面层)
|
||||
│ ├── core.window_selector (窗口选择)
|
||||
│ ├── core.key_sender (按键发送)
|
||||
│ └── utils.settings (设置管理)
|
||||
├── core.admin_check (权限检查)
|
||||
└── resources (资源文件)
|
||||
```
|
||||
|
||||
## 🔧 核心技术
|
||||
|
||||
### Windows API 技术
|
||||
|
||||
本项目大量使用 Windows API,主要包括:
|
||||
|
||||
1. **用户交互 API**
|
||||
- `win32gui`: 窗口操作、消息发送
|
||||
- `win32api`: 系统信息获取
|
||||
- `win32con`: 常量定义
|
||||
|
||||
2. **底层输入 API**
|
||||
- `SendMessage`: 同步消息发送
|
||||
- `PostMessage`: 异步消息发送
|
||||
- `SendInput`: 系统级输入模拟
|
||||
|
||||
3. **系统权限 API**
|
||||
- UAC 提升检查
|
||||
- 进程权限管理
|
||||
|
||||
### PyQt5 技术
|
||||
|
||||
1. **界面架构**
|
||||
- QWidget: 基础窗口组件
|
||||
- QLayout: 布局管理
|
||||
- QTimer: 定时器功能
|
||||
|
||||
2. **事件处理**
|
||||
- 信号槽机制
|
||||
- 事件过滤器
|
||||
- 自定义事件
|
||||
|
||||
### 按键发送原理
|
||||
|
||||
#### 1. PostMessage 模式
|
||||
```python
|
||||
# 异步发送,消息进入目标窗口队列
|
||||
win32gui.PostMessage(hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
win32gui.PostMessage(hwnd, WM_KEYUP, key_code, lparam)
|
||||
```
|
||||
|
||||
#### 2. SendMessage 模式
|
||||
```python
|
||||
# 同步发送,等待处理完成
|
||||
win32gui.SendMessage(hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
win32gui.SendMessage(hwnd, WM_KEYUP, key_code, lparam)
|
||||
```
|
||||
|
||||
#### 3. SendInput 模式
|
||||
```python
|
||||
# 系统级模拟,最高兼容性
|
||||
inputs = (INPUT * 2)()
|
||||
# 设置输入结构...
|
||||
ctypes.windll.user32.SendInput(2, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
||||
```
|
||||
|
||||
## 📝 代码规范
|
||||
|
||||
### Python 编码规范
|
||||
|
||||
1. **PEP 8 标准**
|
||||
- 使用 4 个空格缩进
|
||||
- 行长度不超过 88 字符(Black 标准)
|
||||
- 函数和类之间用两个空行分隔
|
||||
|
||||
2. **命名规范**
|
||||
- 类名:`PascalCase`
|
||||
- 函数/变量:`snake_case`
|
||||
- 常量:`UPPER_CASE`
|
||||
- 私有成员:`_underscore_prefix`
|
||||
|
||||
3. **类型注解**
|
||||
```python
|
||||
def send_key(self, key_code: int, mode: str = "post") -> bool:
|
||||
"""发送按键"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 文档规范
|
||||
|
||||
1. **模块文档**
|
||||
```python
|
||||
"""
|
||||
模块名 - 简短描述
|
||||
|
||||
详细说明模块的功能、用途和技术原理。
|
||||
|
||||
作者: xiao liu
|
||||
版本: v0.1.0
|
||||
"""
|
||||
```
|
||||
|
||||
2. **函数文档**
|
||||
```python
|
||||
def function_name(param1: type, param2: type) -> return_type:
|
||||
"""
|
||||
函数简短描述
|
||||
|
||||
详细说明函数的功能、算法或注意事项。
|
||||
|
||||
Args:
|
||||
param1: 参数1说明
|
||||
param2: 参数2说明
|
||||
|
||||
Returns:
|
||||
返回值说明
|
||||
|
||||
Raises:
|
||||
Exception: 异常情况说明
|
||||
"""
|
||||
```
|
||||
|
||||
3. **行内注释**
|
||||
```python
|
||||
# 检查目标窗口是否有效
|
||||
if not self.target_hwnd or not win32gui.IsWindow(self.target_hwnd):
|
||||
return False
|
||||
```
|
||||
|
||||
### Git 提交规范
|
||||
|
||||
使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
**类型说明**:
|
||||
- `feat`: 新功能
|
||||
- `fix`: 修复 bug
|
||||
- `docs`: 文档更新
|
||||
- `style`: 代码格式化
|
||||
- `refactor`: 代码重构
|
||||
- `test`: 测试相关
|
||||
- `chore`: 构建过程或辅助工具的变动
|
||||
|
||||
**示例**:
|
||||
```
|
||||
feat(core): 添加新的按键发送模式
|
||||
|
||||
- 实现了 SendInput 模式
|
||||
- 提高了兼容性
|
||||
- 添加了相关测试
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
## 🔄 开发流程
|
||||
|
||||
### 功能开发流程
|
||||
|
||||
1. **需求分析**
|
||||
- 在 GitHub Issues 中创建需求
|
||||
- 分析技术可行性
|
||||
- 设计 API 接口
|
||||
|
||||
2. **创建分支**
|
||||
```bash
|
||||
git checkout -b feature/new-feature-name
|
||||
```
|
||||
|
||||
3. **开发实现**
|
||||
- 编写核心逻辑
|
||||
- 添加单元测试
|
||||
- 更新文档
|
||||
|
||||
4. **代码检查**
|
||||
```bash
|
||||
# 格式化代码
|
||||
black src/
|
||||
isort src/
|
||||
|
||||
# 静态检查
|
||||
flake8 src/
|
||||
mypy src/
|
||||
|
||||
# 运行测试
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
5. **提交代码**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: 添加新功能"
|
||||
git push origin feature/new-feature-name
|
||||
```
|
||||
|
||||
6. **创建 Pull Request**
|
||||
- 填写详细的变更说明
|
||||
- 添加测试截图(如适用)
|
||||
- 等待代码审查
|
||||
|
||||
### Bug 修复流程
|
||||
|
||||
1. **复现问题**
|
||||
- 记录复现步骤
|
||||
- 确认影响范围
|
||||
- 添加回归测试
|
||||
|
||||
2. **定位问题**
|
||||
- 使用调试器定位
|
||||
- 查看日志信息
|
||||
- 分析堆栈跟踪
|
||||
|
||||
3. **修复实现**
|
||||
- 最小化修改范围
|
||||
- 确保不引入新问题
|
||||
- 验证修复效果
|
||||
|
||||
## 🧪 测试指南
|
||||
|
||||
### 测试框架
|
||||
|
||||
项目使用 `pytest` 和 `pytest-qt` 进行测试:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from src.gui.main_window import KeyPresser
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""创建 QApplication 实例"""
|
||||
app = QApplication([])
|
||||
yield app
|
||||
app.quit()
|
||||
|
||||
def test_window_creation(app):
|
||||
"""测试窗口创建"""
|
||||
window = KeyPresser()
|
||||
assert window is not None
|
||||
assert window.windowTitle() == "KeyPresser(无钩子版)"
|
||||
```
|
||||
|
||||
### 测试分类
|
||||
|
||||
1. **单元测试** (`tests/unit/`)
|
||||
- 测试单个函数/方法
|
||||
- 使用 mock 隔离依赖
|
||||
- 覆盖边界条件
|
||||
|
||||
2. **集成测试** (`tests/integration/`)
|
||||
- 测试模块间交互
|
||||
- 验证 API 契约
|
||||
- 测试数据流
|
||||
|
||||
3. **界面测试** (`tests/gui/`)
|
||||
- 使用 pytest-qt
|
||||
- 模拟用户操作
|
||||
- 验证界面响应
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
pytest
|
||||
|
||||
# 运行特定测试文件
|
||||
pytest tests/test_key_sender.py
|
||||
|
||||
# 运行特定测试函数
|
||||
pytest tests/test_key_sender.py::test_post_message
|
||||
|
||||
# 生成覆盖率报告
|
||||
pytest --cov=src --cov-report=html
|
||||
```
|
||||
|
||||
## 🐛 调试技巧
|
||||
|
||||
### 日志记录
|
||||
|
||||
使用 Python 标准库 `logging`:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('keypress.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def send_key(self, key_code: int) -> bool:
|
||||
logger.debug(f"发送按键: {key_code}")
|
||||
try:
|
||||
# 发送逻辑
|
||||
result = self._do_send_key(key_code)
|
||||
logger.info(f"按键发送成功: {key_code}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"按键发送失败: {key_code}, 错误: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
### 调试 Windows API
|
||||
|
||||
1. **检查窗口句柄**
|
||||
```python
|
||||
import win32gui
|
||||
|
||||
def debug_window_info(hwnd):
|
||||
if win32gui.IsWindow(hwnd):
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
class_name = win32gui.GetClassName(hwnd)
|
||||
print(f"窗口: {title}, 类名: {class_name}")
|
||||
else:
|
||||
print("无效的窗口句柄")
|
||||
```
|
||||
|
||||
2. **监控消息发送**
|
||||
```python
|
||||
def send_key_with_debug(self, key_code: int):
|
||||
print(f"发送按键: {hex(key_code)} 到窗口: {hex(self.target_hwnd)}")
|
||||
result = win32gui.PostMessage(self.target_hwnd, WM_KEYDOWN, key_code, 0)
|
||||
print(f"PostMessage 返回值: {result}")
|
||||
```
|
||||
|
||||
### 性能分析
|
||||
|
||||
使用 `cProfile` 进行性能分析:
|
||||
|
||||
```bash
|
||||
python -m cProfile -o profile.stats src/main.py
|
||||
```
|
||||
|
||||
查看结果:
|
||||
```python
|
||||
import pstats
|
||||
p = pstats.Stats('profile.stats')
|
||||
p.sort_stats('cumulative').print_stats(10)
|
||||
```
|
||||
|
||||
## ⚡ 性能优化
|
||||
|
||||
### 按键发送优化
|
||||
|
||||
1. **批量发送**
|
||||
```python
|
||||
def send_keys_batch(self, keys: List[int], mode: str = "post"):
|
||||
"""批量发送按键,减少 API 调用次数"""
|
||||
for key_code in keys:
|
||||
self.send_key(key_code, mode)
|
||||
```
|
||||
|
||||
2. **缓存窗口信息**
|
||||
```python
|
||||
class KeySender:
|
||||
def __init__(self):
|
||||
self._window_cache = {}
|
||||
|
||||
def get_window_info(self, hwnd):
|
||||
if hwnd not in self._window_cache:
|
||||
self._window_cache[hwnd] = {
|
||||
'title': win32gui.GetWindowText(hwnd),
|
||||
'class': win32gui.GetClassName(hwnd)
|
||||
}
|
||||
return self._window_cache[hwnd]
|
||||
```
|
||||
|
||||
### 界面响应优化
|
||||
|
||||
1. **异步操作**
|
||||
```python
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
|
||||
class KeySendThread(QThread):
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, key_sender, key_code):
|
||||
super().__init__()
|
||||
self.key_sender = key_sender
|
||||
self.key_code = key_code
|
||||
|
||||
def run(self):
|
||||
self.key_sender.send_key(self.key_code)
|
||||
self.finished.emit()
|
||||
```
|
||||
|
||||
2. **减少重绘**
|
||||
```python
|
||||
# 批量更新界面
|
||||
self.setUpdatesEnabled(False)
|
||||
# 进行多个界面操作...
|
||||
self.setUpdatesEnabled(True)
|
||||
```
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q1: 按键发送不生效
|
||||
|
||||
**可能原因**:
|
||||
1. 目标应用需要管理员权限
|
||||
2. 应用使用了低级键盘钩子
|
||||
3. 窗口句柄已失效
|
||||
|
||||
**解决方案**:
|
||||
```python
|
||||
# 检查窗口是否有效
|
||||
if not win32gui.IsWindow(self.target_hwnd):
|
||||
print("窗口句柄已失效,需要重新选择")
|
||||
|
||||
# 尝试不同的发送模式
|
||||
for mode in ["post", "send_message", "send"]:
|
||||
if self.send_key(key_code, mode):
|
||||
print(f"模式 {mode} 发送成功")
|
||||
break
|
||||
```
|
||||
|
||||
### Q2: 界面冻结
|
||||
|
||||
**原因**: 在主线程执行耗时操作
|
||||
|
||||
**解决方案**:
|
||||
```python
|
||||
# 使用 QTimer 分解操作
|
||||
def process_large_task(self):
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self.process_chunk)
|
||||
self.timer.start(10) # 每10ms处理一小块
|
||||
|
||||
def process_chunk(self):
|
||||
# 处理一小部分数据
|
||||
if self.is_finished():
|
||||
self.timer.stop()
|
||||
```
|
||||
|
||||
### Q3: 内存泄漏
|
||||
|
||||
**检查点**:
|
||||
1. Qt 对象是否正确释放
|
||||
2. 定时器是否正确停止
|
||||
3. 事件监听器是否移除
|
||||
|
||||
**解决方案**:
|
||||
```python
|
||||
def cleanup(self):
|
||||
# 停止所有定时器
|
||||
for timer in self.timers:
|
||||
timer.stop()
|
||||
|
||||
# 断开信号连接
|
||||
self.some_signal.disconnect()
|
||||
|
||||
# 释放资源
|
||||
if hasattr(self, 'key_sender'):
|
||||
del self.key_sender
|
||||
```
|
||||
|
||||
### Q4: 依赖安装问题
|
||||
|
||||
**pyqt5-qt5 版本问题**:
|
||||
```bash
|
||||
# 强制安装特定版本
|
||||
uv pip install PyQt5==5.15.11 pyqt5-qt5==5.15.2 --force-reinstall
|
||||
```
|
||||
|
||||
**权限问题**:
|
||||
```bash
|
||||
# 以管理员身份运行命令提示符
|
||||
# 或者使用 --user 参数
|
||||
pip install --user PyQt5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- **GitHub Issues**: 报告 bug 和功能请求
|
||||
- **讨论区**: 技术讨论和经验分享
|
||||
- **Wiki**: 更多技术文档和示例
|
||||
|
||||
欢迎贡献代码和文档!🎉
|
||||
|
|
@ -0,0 +1,538 @@
|
|||
# 环境配置与依赖说明
|
||||
|
||||
本文档详细说明了 KeyPressRemark 项目的环境配置要求、依赖管理和常见问题解决方案。
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [系统要求](#系统要求)
|
||||
- [Python 环境](#python-环境)
|
||||
- [依赖包详解](#依赖包详解)
|
||||
- [环境配置步骤](#环境配置步骤)
|
||||
- [常见问题解决](#常见问题解决)
|
||||
- [依赖管理最佳实践](#依赖管理最佳实践)
|
||||
|
||||
## 🖥️ 系统要求
|
||||
|
||||
### 操作系统支持
|
||||
|
||||
| 操作系统 | 版本要求 | 支持状态 | 说明 |
|
||||
|---------|---------|---------|------|
|
||||
| Windows 10 | 1909+ | ✅ 完全支持 | 推荐版本 |
|
||||
| Windows 11 | 所有版本 | ✅ 完全支持 | 推荐版本 |
|
||||
| Windows 8.1 | 所有版本 | ⚠️ 基本支持 | 部分功能可能受限 |
|
||||
| Windows 7 | SP1+ | ❌ 不支持 | PyQt5 不再支持 |
|
||||
| Linux | 任何发行版 | ❌ 不支持 | Windows API 依赖 |
|
||||
| macOS | 任何版本 | ❌ 不支持 | Windows API 依赖 |
|
||||
|
||||
### 硬件要求
|
||||
|
||||
| 组件 | 最低要求 | 推荐配置 |
|
||||
|------|---------|---------|
|
||||
| CPU | 1GHz 单核 | 2GHz 双核+ |
|
||||
| 内存 | 512MB | 2GB+ |
|
||||
| 硬盘 | 100MB 可用空间 | 500MB+ |
|
||||
| 显示器 | 800x600 | 1920x1080+ |
|
||||
|
||||
### 权限要求
|
||||
|
||||
- **管理员权限**: 必需(用于按键模拟功能)
|
||||
- **UAC 设置**: 建议设置为最低级别,减少弹窗干扰
|
||||
- **防火墙**: 无特殊要求
|
||||
- **杀毒软件**: 可能需要将程序添加到白名单
|
||||
|
||||
## 🐍 Python 环境
|
||||
|
||||
### Python 版本要求
|
||||
|
||||
```yaml
|
||||
最低版本: Python 3.11.0
|
||||
推荐版本: Python 3.11.7+
|
||||
测试版本:
|
||||
- Python 3.11.7 ✅
|
||||
- Python 3.12.x ✅
|
||||
- Python 3.10.x ⚠️ (可能存在兼容性问题)
|
||||
```
|
||||
|
||||
### Python 安装建议
|
||||
|
||||
1. **官方安装器** (推荐)
|
||||
```bash
|
||||
# 下载 Python 3.11.7 from python.org
|
||||
# 安装时勾选 "Add Python to PATH"
|
||||
# 安装时勾选 "Install for all users"
|
||||
```
|
||||
|
||||
2. **通过 Microsoft Store** (简单)
|
||||
```bash
|
||||
# 搜索 "Python 3.11" 并安装
|
||||
# 自动配置环境变量
|
||||
```
|
||||
|
||||
3. **通过 pyenv-win** (高级用户)
|
||||
```bash
|
||||
# 安装 pyenv-win
|
||||
git clone https://github.com/pyenv-win/pyenv-win.git %USERPROFILE%\.pyenv
|
||||
|
||||
# 安装指定版本
|
||||
pyenv install 3.11.7
|
||||
pyenv global 3.11.7
|
||||
```
|
||||
|
||||
### 验证 Python 安装
|
||||
|
||||
```bash
|
||||
# 检查 Python 版本
|
||||
python --version
|
||||
# 输出: Python 3.11.7
|
||||
|
||||
# 检查 pip 版本
|
||||
pip --version
|
||||
# 输出: pip 23.3.1 from ...
|
||||
|
||||
# 检查环境变量
|
||||
echo %PATH%
|
||||
# 应该包含 Python 安装路径
|
||||
```
|
||||
|
||||
## 📦 依赖包详解
|
||||
|
||||
### 核心依赖
|
||||
|
||||
#### PyQt5 (5.15.11)
|
||||
```yaml
|
||||
用途: 图形用户界面框架
|
||||
重要性: 核心依赖
|
||||
安装大小: ~50MB
|
||||
许可证: GPL v3 / Commercial
|
||||
```
|
||||
|
||||
**功能说明**:
|
||||
- 提供完整的 GUI 组件库
|
||||
- 支持信号槽机制
|
||||
- 跨平台支持(本项目仅用于 Windows)
|
||||
- 事件循环和定时器功能
|
||||
|
||||
**安装命令**:
|
||||
```bash
|
||||
# 标准安装
|
||||
pip install PyQt5==5.15.11
|
||||
|
||||
# 使用 uv (推荐)
|
||||
uv pip install PyQt5==5.15.11
|
||||
```
|
||||
|
||||
#### pyqt5-qt5 (5.15.2)
|
||||
```yaml
|
||||
用途: Qt5 运行时库
|
||||
重要性: 核心依赖
|
||||
安装大小: ~100MB
|
||||
许可证: LGPL v3
|
||||
```
|
||||
|
||||
**重要说明**:
|
||||
⚠️ **版本锁定原因**:
|
||||
- Windows 平台必须使用 `5.15.2` 版本
|
||||
- 更新的 `5.15.17` 版本不提供 Windows wheel 文件
|
||||
- 这是 PyQt5 在 Windows 平台的已知限制
|
||||
|
||||
**安装命令**:
|
||||
```bash
|
||||
# 必须指定确切版本
|
||||
pip install pyqt5-qt5==5.15.2
|
||||
|
||||
# 使用 uv
|
||||
uv pip install pyqt5-qt5==5.15.2
|
||||
```
|
||||
|
||||
#### pywin32 (311)
|
||||
```yaml
|
||||
用途: Windows API 访问库
|
||||
重要性: 核心依赖
|
||||
安装大小: ~10MB
|
||||
许可证: PSF License
|
||||
```
|
||||
|
||||
**功能说明**:
|
||||
- 提供 Python 访问 Windows API 的能力
|
||||
- 包含 win32gui, win32api, win32con 等模块
|
||||
- 支持窗口操作、消息发送、系统信息获取
|
||||
- 版本号与 Windows 构建号相关
|
||||
|
||||
**安装命令**:
|
||||
```bash
|
||||
pip install pywin32==311
|
||||
```
|
||||
|
||||
### 开发依赖
|
||||
|
||||
#### PyInstaller (6.16.0+)
|
||||
```yaml
|
||||
用途: Python 程序打包工具
|
||||
重要性: 打包依赖
|
||||
安装大小: ~15MB
|
||||
许可证: GPL v2
|
||||
```
|
||||
|
||||
**功能说明**:
|
||||
- 将 Python 程序打包为独立的可执行文件
|
||||
- 支持单文件和目录两种打包方式
|
||||
- 自动处理依赖关系
|
||||
- 支持自定义图标和版本信息
|
||||
|
||||
**安装命令**:
|
||||
```bash
|
||||
# 开发环境安装
|
||||
uv add --dev pyinstaller>=6.0.0
|
||||
```
|
||||
|
||||
### 依赖关系图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[KeyPressRemark] --> B[PyQt5]
|
||||
A --> C[pywin32]
|
||||
B --> D[pyqt5-qt5]
|
||||
B --> E[pyqt5-sip]
|
||||
|
||||
A --> F[PyInstaller]
|
||||
F --> G[altgraph]
|
||||
F --> H[pefile]
|
||||
F --> I[pyinstaller-hooks-contrib]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#f3e5f5
|
||||
style C fill:#f3e5f5
|
||||
style D fill:#fff3e0
|
||||
style F fill:#e8f5e8
|
||||
```
|
||||
|
||||
## 🔧 环境配置步骤
|
||||
|
||||
### 方案一:使用 uv (推荐)
|
||||
|
||||
uv 是新一代 Python 包管理器,速度比 pip 快 10-100 倍。
|
||||
|
||||
```bash
|
||||
# 1. 安装 uv
|
||||
pip install uv
|
||||
|
||||
# 2. 克隆项目
|
||||
git clone <repository-url>
|
||||
cd KeyPressRemark
|
||||
|
||||
# 3. 创建虚拟环境
|
||||
uv venv --python 3.11
|
||||
|
||||
# 4. 激活虚拟环境
|
||||
.venv\Scripts\activate
|
||||
|
||||
# 5. 安装依赖
|
||||
uv pip install PyQt5==5.15.11 pyqt5-qt5==5.15.2 pywin32==311
|
||||
|
||||
# 6. 安装开发依赖(可选)
|
||||
uv add --dev pyinstaller pytest pytest-qt black isort flake8
|
||||
|
||||
# 7. 验证安装
|
||||
python -c "import PyQt5; print('PyQt5 安装成功')"
|
||||
python -c "import win32gui; print('pywin32 安装成功')"
|
||||
```
|
||||
|
||||
### 方案二:使用传统 pip
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone <repository-url>
|
||||
cd KeyPressRemark
|
||||
|
||||
# 2. 创建虚拟环境
|
||||
python -m venv .venv
|
||||
|
||||
# 3. 激活虚拟环境
|
||||
.venv\Scripts\activate
|
||||
|
||||
# 4. 升级 pip
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
# 5. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 6. 验证安装
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
### 方案三:使用 conda/mamba
|
||||
|
||||
```bash
|
||||
# 1. 创建 conda 环境
|
||||
conda create -n keypressremark python=3.11
|
||||
|
||||
# 2. 激活环境
|
||||
conda activate keypressremark
|
||||
|
||||
# 3. 安装依赖
|
||||
# 注意:PyQt5 通过 conda-forge 安装可能版本不匹配
|
||||
pip install PyQt5==5.15.11 pyqt5-qt5==5.15.2 pywin32==311
|
||||
|
||||
# 4. 验证安装
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
### Docker 环境 (实验性)
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile (仅用于开发,不能运行GUI)
|
||||
FROM python:3.11-windowsservercore
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY src/ ./src/
|
||||
CMD ["python", "src/main.py"]
|
||||
```
|
||||
|
||||
注意:由于 Windows API 依赖,Docker 环境主要用于代码检查,无法运行完整功能。
|
||||
|
||||
## 🔍 常见问题解决
|
||||
|
||||
### 问题 1: PyQt5 安装失败
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
ERROR: Could not find a version that satisfies the requirement PyQt5
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 方案 A: 使用官方索引
|
||||
pip install -i https://pypi.org/simple/ PyQt5==5.15.11
|
||||
|
||||
# 方案 B: 手动下载 wheel 文件
|
||||
# 从 https://pypi.org/project/PyQt5/#files 下载对应版本
|
||||
pip install PyQt5-5.15.11-cp311-cp311-win_amd64.whl
|
||||
|
||||
# 方案 C: 清理缓存重试
|
||||
pip cache purge
|
||||
pip install PyQt5==5.15.11
|
||||
```
|
||||
|
||||
### 问题 2: pyqt5-qt5 版本冲突
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
Distribution pyqt5-qt5==5.15.17 can't be installed because it doesn't have a wheel for the current platform
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 强制安装兼容版本
|
||||
uv pip install pyqt5-qt5==5.15.2 --force-reinstall
|
||||
|
||||
# 或者手动指定约束
|
||||
echo "pyqt5-qt5==5.15.2" > constraints.txt
|
||||
pip install PyQt5==5.15.11 -c constraints.txt
|
||||
```
|
||||
|
||||
### 问题 3: pywin32 权限问题
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
Access is denied
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 方案 A: 管理员权限安装
|
||||
# 以管理员身份运行命令提示符
|
||||
pip install pywin32==311
|
||||
|
||||
# 方案 B: 用户级安装
|
||||
pip install --user pywin32==311
|
||||
|
||||
# 方案 C: 安装后手动注册
|
||||
python Scripts/pywin32_postinstall.py -install
|
||||
```
|
||||
|
||||
### 问题 4: 虚拟环境激活失败
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
cannot be loaded because running scripts is disabled on this system
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```powershell
|
||||
# 在 PowerShell 中执行(管理员权限)
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
# 然后重新激活虚拟环境
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
### 问题 5: 中文路径问题
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
UnicodeDecodeError: 'gbk' codec can't decode byte
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 方案 A: 设置环境变量
|
||||
set PYTHONIOENCODING=utf-8
|
||||
|
||||
# 方案 B: 避免中文路径
|
||||
# 将项目移动到英文路径下
|
||||
|
||||
# 方案 C: 修改系统区域设置
|
||||
# 控制面板 -> 区域 -> 管理 -> 更改系统区域设置 -> Beta: 使用 Unicode UTF-8
|
||||
```
|
||||
|
||||
### 问题 6: 防火墙/杀毒软件拦截
|
||||
|
||||
**现象**:
|
||||
- 程序无法启动
|
||||
- 按键发送功能异常
|
||||
- 弹出安全警告
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 1. 添加到防火墙例外
|
||||
# Windows Defender 防火墙 -> 允许应用通过防火墙
|
||||
|
||||
# 2. 添加到杀毒软件白名单
|
||||
# 各杀毒软件设置方法不同,需要将以下路径添加到白名单:
|
||||
# - Python.exe 路径
|
||||
# - 项目文件夹路径
|
||||
# - 生成的 exe 文件路径
|
||||
|
||||
# 3. 临时关闭实时保护(仅测试用)
|
||||
# Windows 安全中心 -> 病毒和威胁防护 -> 实时保护
|
||||
```
|
||||
|
||||
## 📚 依赖管理最佳实践
|
||||
|
||||
### 版本锁定策略
|
||||
|
||||
1. **精确版本锁定** (推荐用于生产)
|
||||
```
|
||||
PyQt5==5.15.11
|
||||
pyqt5-qt5==5.15.2
|
||||
pywin32==311
|
||||
```
|
||||
|
||||
2. **兼容版本范围** (用于开发)
|
||||
```
|
||||
PyQt5>=5.15.10,<5.16.0
|
||||
pyqt5-qt5>=5.15.2,<5.16.0
|
||||
pywin32>=311
|
||||
```
|
||||
|
||||
3. **主版本锁定** (用于扩展性)
|
||||
```
|
||||
PyQt5~=5.15.11 # 等同于 >=5.15.11, <5.16.0
|
||||
```
|
||||
|
||||
### requirements.txt 管理
|
||||
|
||||
创建多个需求文件:
|
||||
|
||||
```bash
|
||||
# requirements/base.txt - 基础依赖
|
||||
PyQt5==5.15.11
|
||||
pyqt5-qt5==5.15.2
|
||||
pywin32==311
|
||||
|
||||
# requirements/dev.txt - 开发依赖
|
||||
-r base.txt
|
||||
pytest>=7.0.0
|
||||
pytest-qt>=4.0.0
|
||||
black>=22.0.0
|
||||
isort>=5.0.0
|
||||
flake8>=4.0.0
|
||||
|
||||
# requirements/build.txt - 打包依赖
|
||||
-r base.txt
|
||||
pyinstaller>=6.0.0
|
||||
```
|
||||
|
||||
### 依赖安全检查
|
||||
|
||||
```bash
|
||||
# 使用 pip-audit 检查安全漏洞
|
||||
pip install pip-audit
|
||||
pip-audit
|
||||
|
||||
# 使用 safety 检查
|
||||
pip install safety
|
||||
safety check
|
||||
|
||||
# 检查依赖许可证
|
||||
pip install pip-licenses
|
||||
pip-licenses
|
||||
```
|
||||
|
||||
### 依赖更新策略
|
||||
|
||||
```bash
|
||||
# 1. 检查过时的包
|
||||
pip list --outdated
|
||||
|
||||
# 2. 生成当前环境的精确依赖
|
||||
pip freeze > requirements-lock.txt
|
||||
|
||||
# 3. 测试更新(在独立环境中)
|
||||
python -m venv test-env
|
||||
test-env\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
python src/main.py # 测试功能
|
||||
|
||||
# 4. 逐步更新(推荐)
|
||||
# 一次只更新一个包,测试后再更新下一个
|
||||
```
|
||||
|
||||
### 环境隔离最佳实践
|
||||
|
||||
1. **项目级隔离**
|
||||
```bash
|
||||
# 每个项目使用独立的虚拟环境
|
||||
python -m venv .venv-keypressremark
|
||||
```
|
||||
|
||||
2. **功能级隔离**
|
||||
```bash
|
||||
# 开发环境
|
||||
python -m venv .venv-dev
|
||||
|
||||
# 测试环境
|
||||
python -m venv .venv-test
|
||||
|
||||
# 打包环境
|
||||
python -m venv .venv-build
|
||||
```
|
||||
|
||||
3. **版本级隔离**
|
||||
```bash
|
||||
# Python 3.11 环境
|
||||
python3.11 -m venv .venv-py311
|
||||
|
||||
# Python 3.12 测试环境
|
||||
python3.12 -m venv .venv-py312
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 获取支持
|
||||
|
||||
如果遇到环境配置问题,可以通过以下方式获取帮助:
|
||||
|
||||
1. **查看 Issues**: [GitHub Issues](https://github.com/your-repo/issues)
|
||||
2. **提交问题**: 使用问题模板提供详细信息
|
||||
3. **社区讨论**: [GitHub Discussions](https://github.com/your-repo/discussions)
|
||||
|
||||
提交问题时,请包含以下信息:
|
||||
- 操作系统版本
|
||||
- Python 版本
|
||||
- 完整的错误信息
|
||||
- 尝试的解决方案
|
||||
- 环境信息 (`pip list` 输出)
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# 项目文档索引
|
||||
|
||||
本目录包含 KeyPressRemark 项目的完整技术文档,为开发者、用户和维护者提供全面的参考资料。
|
||||
|
||||
## 📚 文档导航
|
||||
|
||||
### 用户文档
|
||||
|
||||
- **[README.md](../README.md)** - 项目概述和快速入门指南
|
||||
- 功能特性介绍
|
||||
- 安装和使用说明
|
||||
- 基本操作指南
|
||||
- 打包部署说明
|
||||
|
||||
### 开发者文档
|
||||
|
||||
- **[开发者指南 (DEVELOPER_GUIDE.md)](./DEVELOPER_GUIDE.md)** - 完整的开发指导
|
||||
- 开发环境设置
|
||||
- 项目架构详解
|
||||
- 代码规范和最佳实践
|
||||
- 测试和调试指南
|
||||
- 性能优化建议
|
||||
|
||||
- **[环境配置指南 (ENVIRONMENT_SETUP.md)](./ENVIRONMENT_SETUP.md)** - 详细的环境配置说明
|
||||
- 系统要求详解
|
||||
- Python 环境配置
|
||||
- 依赖包详细说明
|
||||
- 常见问题解决方案
|
||||
- 依赖管理最佳实践
|
||||
|
||||
### 技术文档
|
||||
|
||||
- **[API 参考](./API_REFERENCE.md)** (计划中)
|
||||
- 核心类和方法文档
|
||||
- 使用示例
|
||||
- 参数说明
|
||||
|
||||
- **[架构设计](./ARCHITECTURE.md)** (计划中)
|
||||
- 系统架构图
|
||||
- 模块设计原理
|
||||
- 技术选型说明
|
||||
|
||||
## 🔍 快速查找
|
||||
|
||||
### 按使用场景查找
|
||||
|
||||
| 我想要... | 参考文档 |
|
||||
|----------|----------|
|
||||
| 了解项目功能 | [README.md](../README.md) |
|
||||
| 快速使用程序 | [README.md - 使用指南](../README.md#使用指南) |
|
||||
| 设置开发环境 | [环境配置指南](./ENVIRONMENT_SETUP.md) |
|
||||
| 参与代码开发 | [开发者指南](./DEVELOPER_GUIDE.md) |
|
||||
| 解决安装问题 | [环境配置指南 - 常见问题](./ENVIRONMENT_SETUP.md#常见问题解决) |
|
||||
| 了解代码架构 | [开发者指南 - 项目架构](./DEVELOPER_GUIDE.md#项目架构) |
|
||||
| 学习按键原理 | [开发者指南 - 核心技术](./DEVELOPER_GUIDE.md#核心技术) |
|
||||
|
||||
### 按技术领域查找
|
||||
|
||||
| 技术领域 | 相关文档 |
|
||||
|---------|----------|
|
||||
| Python 环境 | [环境配置指南 - Python 环境](./ENVIRONMENT_SETUP.md#python-环境) |
|
||||
| PyQt5 界面 | [开发者指南 - PyQt5 技术](./DEVELOPER_GUIDE.md#pyqt5-技术) |
|
||||
| Windows API | [开发者指南 - Windows API 技术](./DEVELOPER_GUIDE.md#windows-api-技术) |
|
||||
| 按键模拟 | [开发者指南 - 按键发送原理](./DEVELOPER_GUIDE.md#按键发送原理) |
|
||||
| 依赖管理 | [环境配置指南 - 依赖管理](./ENVIRONMENT_SETUP.md#依赖管理最佳实践) |
|
||||
| 测试调试 | [开发者指南 - 测试指南](./DEVELOPER_GUIDE.md#测试指南) |
|
||||
|
||||
### 按问题类型查找
|
||||
|
||||
| 问题类型 | 解决方案文档 |
|
||||
|---------|-------------|
|
||||
| 安装失败 | [环境配置指南 - 常见问题](./ENVIRONMENT_SETUP.md#常见问题解决) |
|
||||
| 权限问题 | [开发者指南 - 调试技巧](./DEVELOPER_GUIDE.md#调试技巧) |
|
||||
| 按键无效 | [开发者指南 - 常见问题 Q1](./DEVELOPER_GUIDE.md#q1-按键发送不生效) |
|
||||
| 界面卡顿 | [开发者指南 - 常见问题 Q2](./DEVELOPER_GUIDE.md#q2-界面冻结) |
|
||||
| 版本冲突 | [环境配置指南 - 问题2](./ENVIRONMENT_SETUP.md#问题-2-pyqt5-qt5-版本冲突) |
|
||||
|
||||
## 📝 文档贡献
|
||||
|
||||
### 文档规范
|
||||
|
||||
- 使用 Markdown 格式
|
||||
- 遵循中文技术文档规范
|
||||
- 包含完整的目录结构
|
||||
- 提供实用的代码示例
|
||||
- 及时更新版本信息
|
||||
|
||||
### 贡献流程
|
||||
|
||||
1. **发现问题**: 在使用过程中发现文档问题或缺失
|
||||
2. **创建 Issue**: 在 GitHub 上创建文档改进建议
|
||||
3. **编写内容**: 按照现有格式编写或修改文档
|
||||
4. **提交 PR**: 提交 Pull Request 进行审查
|
||||
5. **合并更新**: 审查通过后合并到主分支
|
||||
|
||||
### 文档维护
|
||||
|
||||
- **定期更新**: 随代码变更同步更新文档
|
||||
- **版本标记**: 重要变更需要标记版本号
|
||||
- **链接检查**: 定期检查文档内链接有效性
|
||||
- **示例验证**: 确保代码示例能正常运行
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
### 官方资源
|
||||
|
||||
- **项目仓库**: [GitHub Repository](https://github.com/your-repo)
|
||||
- **问题反馈**: [GitHub Issues](https://github.com/your-repo/issues)
|
||||
- **功能讨论**: [GitHub Discussions](https://github.com/your-repo/discussions)
|
||||
|
||||
### 技术参考
|
||||
|
||||
- **PyQt5 官方文档**: [https://doc.qt.io/qtforpython/](https://doc.qt.io/qtforpython/)
|
||||
- **Windows API 参考**: [https://docs.microsoft.com/en-us/windows/win32/api/](https://docs.microsoft.com/en-us/windows/win32/api/)
|
||||
- **Python 官方文档**: [https://docs.python.org/3/](https://docs.python.org/3/)
|
||||
|
||||
### 社区资源
|
||||
|
||||
- **PyQt5 中文教程**: 各大技术博客和教程网站
|
||||
- **Windows 编程资源**: MSDN 和相关技术社区
|
||||
- **Python 包管理**: pip 和 uv 官方文档
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024-01-01
|
||||
**文档版本**: v1.0
|
||||
**维护者**: xiao liu
|
||||
739
main.py
739
main.py
|
|
@ -1,739 +0,0 @@
|
|||
import sys
|
||||
import time
|
||||
import os
|
||||
import resources_rc
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QLineEdit, QCheckBox, QComboBox,
|
||||
QGridLayout, QMessageBox, QDialog)
|
||||
from PyQt5.QtCore import QTimer, Qt, QSettings
|
||||
import win32gui
|
||||
import win32con
|
||||
import win32api
|
||||
import win32process
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
|
||||
# 检查并请求管理员权限
|
||||
def is_admin():
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
if not is_admin():
|
||||
# 如果不是管理员权限,则重新以管理员身份启动程序
|
||||
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
|
||||
sys.exit(0)
|
||||
|
||||
# Constants for virtual keys
|
||||
VK_SPACE = 0x20
|
||||
VK_RETURN = 0x0D
|
||||
VK_TAB = 0x09
|
||||
VK_ESCAPE = 0x1B
|
||||
VK_BACK = 0x08
|
||||
VK_INSERT = 0x2D
|
||||
VK_DELETE = 0x2E
|
||||
VK_HOME = 0x24
|
||||
VK_END = 0x23
|
||||
VK_PRIOR = 0x21 # Page Up
|
||||
VK_NEXT = 0x22 # Page Down
|
||||
VK_LEFT = 0x25
|
||||
VK_RIGHT = 0x27
|
||||
VK_UP = 0x26
|
||||
VK_DOWN = 0x28
|
||||
VK_F1 = 0x70
|
||||
VK_F2 = 0x71
|
||||
VK_F3 = 0x72
|
||||
VK_F4 = 0x73
|
||||
VK_F5 = 0x74
|
||||
VK_F6 = 0x75
|
||||
VK_F7 = 0x76
|
||||
VK_F8 = 0x77
|
||||
VK_F9 = 0x78
|
||||
VK_F10 = 0x79
|
||||
VK_F11 = 0x7A
|
||||
VK_F12 = 0x7B
|
||||
|
||||
VK_SCROLL = 0x91
|
||||
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_CHAR = 0x0102
|
||||
|
||||
# Constants for SendInput
|
||||
INPUT_KEYBOARD = 1
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
KEYEVENTF_UNICODE = 0x0004
|
||||
|
||||
|
||||
# 鼠标事件常量
|
||||
MOUSEEVENTF_MOVE = 0x0001
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002
|
||||
MOUSEEVENTF_LEFTUP = 0x0004
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000
|
||||
# SendInput常量
|
||||
INPUT_MOUSE = 0
|
||||
INPUT_KEYBOARD = 1
|
||||
|
||||
# Define required structures for SendInput
|
||||
class MOUSEINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dx", wintypes.LONG),
|
||||
("dy", wintypes.LONG),
|
||||
("mouseData", wintypes.DWORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class KEYBDINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wVk", wintypes.WORD),
|
||||
("wScan", wintypes.WORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class HARDWAREINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("uMsg", wintypes.DWORD),
|
||||
("wParamL", wintypes.WORD),
|
||||
("wParamH", wintypes.WORD),
|
||||
]
|
||||
|
||||
|
||||
class INPUT_union(ctypes.Union):
|
||||
_fields_ = [
|
||||
("mi", MOUSEINPUT),
|
||||
("ki", KEYBDINPUT),
|
||||
("hi", HARDWAREINPUT),
|
||||
]
|
||||
|
||||
|
||||
class INPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("type", wintypes.DWORD),
|
||||
("u", INPUT_union),
|
||||
]
|
||||
|
||||
|
||||
class AboutMeDlg(QDialog): # 从 QWidget 改为 QDialog
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("关于")
|
||||
self.setFixedSize(400, 200)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
title = QLabel("KeyPresser", self)
|
||||
title.setStyleSheet("font-size: 18pt; font-weight: bold;")
|
||||
layout.addWidget(title)
|
||||
|
||||
desc = QLabel("一个简单的按键模拟工具", self)
|
||||
layout.addWidget(desc)
|
||||
|
||||
version = QLabel("版本: 专业版 (Python 无钩子版)", self)
|
||||
layout.addWidget(version)
|
||||
|
||||
author = QLabel("作者: xiao liu", self)
|
||||
layout.addWidget(author)
|
||||
|
||||
ok_button = QPushButton("确定", self)
|
||||
ok_button.clicked.connect(self.accept) # 从 self.close 改为 self.accept
|
||||
layout.addWidget(ok_button)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
|
||||
class KeyPresser(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.target_hwnd = None
|
||||
self.timers = []
|
||||
|
||||
# 设置应用程序和窗口图标
|
||||
self.setWindowIcon(QIcon(':/aaa.ico')) # 添加这行代码
|
||||
|
||||
self.init_ui()
|
||||
self.load_settings()
|
||||
|
||||
# Add a timer to poll for hotkey
|
||||
self.hotkeyTimer = QTimer(self)
|
||||
self.hotkeyTimer.timeout.connect(self.check_hotkey)
|
||||
self.hotkeyTimer.start(50) # Check every 50ms for more responsive hotkey
|
||||
|
||||
# 添加到__init__或init_ui方法中
|
||||
self.mouseTimer = QTimer(self)
|
||||
self.mouseTimer.timeout.connect(self.click_mouse)
|
||||
|
||||
# Last key state
|
||||
self.lastKeyState = False
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("KeyPresser(无钩子版)")
|
||||
self.setFixedWidth(300)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Window selection
|
||||
label = QLabel("选择窗口:", self)
|
||||
layout.addWidget(label)
|
||||
|
||||
self.selectedWindowLabel = QLabel("未选择窗口", self)
|
||||
self.selectedWindowLabel.setStyleSheet("color: green;")
|
||||
layout.addWidget(self.selectedWindowLabel)
|
||||
|
||||
selectButton = QPushButton("选择窗口", self)
|
||||
layout.addWidget(selectButton)
|
||||
|
||||
# Mode selection for key pressing
|
||||
modeLayout = QHBoxLayout()
|
||||
modeLabel = QLabel("按键模式:", self)
|
||||
modeLayout.addWidget(modeLabel)
|
||||
|
||||
self.modeComboBox = QComboBox(self)
|
||||
self.modeComboBox.addItem("PostMessage (常规模式)", "post")
|
||||
self.modeComboBox.addItem("SendInput (后台模式)", "send")
|
||||
self.modeComboBox.addItem("SendMessage (直接模式)", "send_message")
|
||||
modeLayout.addWidget(self.modeComboBox)
|
||||
|
||||
layout.addLayout(modeLayout)
|
||||
|
||||
# Space key settings
|
||||
spaceLayout = QHBoxLayout()
|
||||
self.spaceCheckBox = QCheckBox(self)
|
||||
spaceLayout.addWidget(self.spaceCheckBox)
|
||||
|
||||
spaceLabel = QLabel("空格键时间间隔 (毫秒):", self)
|
||||
spaceLayout.addWidget(spaceLabel)
|
||||
|
||||
self.spaceIntervalLineEdit = QLineEdit(self)
|
||||
self.spaceIntervalLineEdit.setText("1000")
|
||||
spaceLayout.addWidget(self.spaceIntervalLineEdit)
|
||||
|
||||
layout.addLayout(spaceLayout)
|
||||
|
||||
# 鼠标左键设置 - 修复布局
|
||||
mouseLayout = QHBoxLayout()
|
||||
self.mouseCheckBox = QCheckBox(self)
|
||||
mouseLayout.addWidget(self.mouseCheckBox)
|
||||
|
||||
mouseLabel = QLabel("鼠标左键时间间隔 (毫秒):", self)
|
||||
mouseLayout.addWidget(mouseLabel) # 修复:将mouseLabel.addWidget(mouseLabel)改为mouseLayout.addWidget(mouseLabel)
|
||||
|
||||
self.mouseIntervalLineEdit = QLineEdit(self)
|
||||
self.mouseIntervalLineEdit.setText("1000")
|
||||
mouseLayout.addWidget(self.mouseIntervalLineEdit)
|
||||
|
||||
layout.addLayout(mouseLayout)
|
||||
|
||||
|
||||
# Custom keys
|
||||
keysLabel = QLabel("自定义按键和时间间隔 (毫秒):", self)
|
||||
layout.addWidget(keysLabel)
|
||||
|
||||
keysLayout = QGridLayout()
|
||||
self.keyCheckBoxes = []
|
||||
self.keyCombos = []
|
||||
self.intervalLineEdits = []
|
||||
|
||||
for i in range(10):
|
||||
checkbox = QCheckBox(self)
|
||||
keysLayout.addWidget(checkbox, i, 0)
|
||||
self.keyCheckBoxes.append(checkbox)
|
||||
|
||||
combobox = QComboBox(self)
|
||||
self.populate_key_combos(combobox)
|
||||
keysLayout.addWidget(combobox, i, 1)
|
||||
self.keyCombos.append(combobox)
|
||||
|
||||
lineEdit = QLineEdit(self)
|
||||
lineEdit.setText("1000")
|
||||
keysLayout.addWidget(lineEdit, i, 2)
|
||||
self.intervalLineEdits.append(lineEdit)
|
||||
|
||||
# Create timer for this key
|
||||
timer = QTimer(self)
|
||||
timer.timeout.connect(lambda checked=False, index=i: self.press_keys(index))
|
||||
self.timers.append(timer)
|
||||
|
||||
layout.addLayout(keysLayout)
|
||||
|
||||
# Start/Stop buttons
|
||||
startButton = QPushButton("开始", self)
|
||||
layout.addWidget(startButton)
|
||||
|
||||
stopButton = QPushButton("停止", self)
|
||||
layout.addWidget(stopButton)
|
||||
|
||||
labelPrompt = QLabel("修改配置后需点击开始按钮以使更改生效。", self)
|
||||
labelPrompt.setStyleSheet("color: red;")
|
||||
layout.addWidget(labelPrompt)
|
||||
|
||||
self.instructionLabel = QLabel("停止中", self)
|
||||
self.instructionLabel.setStyleSheet("color: green;")
|
||||
layout.addWidget(self.instructionLabel)
|
||||
|
||||
# Hotkey selection
|
||||
hotkeyLayout = QHBoxLayout()
|
||||
hotkeyLabel = QLabel("开始/停止快捷键:", self)
|
||||
hotkeyLayout.addWidget(hotkeyLabel)
|
||||
|
||||
self.hotkeyComboBox = QComboBox(self)
|
||||
self.populate_key_combos(self.hotkeyComboBox)
|
||||
self.hotkeyComboBox.setCurrentText("Home") # Default to Home key
|
||||
hotkeyLayout.addWidget(self.hotkeyComboBox)
|
||||
|
||||
layout.addLayout(hotkeyLayout)
|
||||
|
||||
# Debug info
|
||||
self.debugLabel = QLabel("状态: 管理员模式 (使用轮询检测热键)", self)
|
||||
self.debugLabel.setStyleSheet("color: blue;")
|
||||
layout.addWidget(self.debugLabel)
|
||||
|
||||
# Thread input settings
|
||||
threadLayout = QHBoxLayout()
|
||||
self.attachThreadCheckBox = QCheckBox("关联线程输入", self)
|
||||
self.attachThreadCheckBox.setChecked(True)
|
||||
threadLayout.addWidget(self.attachThreadCheckBox)
|
||||
|
||||
layout.addLayout(threadLayout)
|
||||
|
||||
aboutButton = QPushButton("关于", self)
|
||||
layout.addWidget(aboutButton)
|
||||
|
||||
# Connect signals
|
||||
selectButton.clicked.connect(self.select_window)
|
||||
startButton.clicked.connect(self.start_pressing)
|
||||
stopButton.clicked.connect(self.stop_pressing)
|
||||
aboutButton.clicked.connect(self.about_me)
|
||||
|
||||
self.spaceTimer = QTimer(self)
|
||||
self.spaceTimer.timeout.connect(self.press_space)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def populate_key_combos(self, comboBox):
|
||||
keys = [
|
||||
("F1", VK_F1), ("F2", VK_F2), ("F3", VK_F3), ("F4", VK_F4),
|
||||
("F5", VK_F5), ("F6", VK_F6), ("F7", VK_F7), ("F8", VK_F8),
|
||||
("F9", VK_F9), ("F10", VK_F10), ("F11", VK_F11), ("F12", VK_F12),
|
||||
("A", ord('A')), ("B", ord('B')), ("C", ord('C')), ("D", ord('D')),
|
||||
("E", ord('E')), ("F", ord('F')), ("G", ord('G')), ("H", ord('H')),
|
||||
("I", ord('I')), ("J", ord('J')), ("K", ord('K')), ("L", ord('L')),
|
||||
("M", ord('M')), ("N", ord('N')), ("O", ord('O')), ("P", ord('P')),
|
||||
("Q", ord('Q')), ("R", ord('R')), ("S", ord('S')), ("T", ord('T')),
|
||||
("U", ord('U')), ("V", ord('V')), ("W", ord('W')), ("X", ord('X')),
|
||||
("Y", ord('Y')), ("Z", ord('Z')),
|
||||
("0", ord('0')), ("1", ord('1')), ("2", ord('2')), ("3", ord('3')),
|
||||
("4", ord('4')), ("5", ord('5')), ("6", ord('6')), ("7", ord('7')),
|
||||
("8", ord('8')), ("9", ord('9')),
|
||||
("Space", VK_SPACE), ("Enter", VK_RETURN), ("Tab", VK_TAB),
|
||||
("Esc", VK_ESCAPE), ("Backspace", VK_BACK), ("Insert", VK_INSERT),
|
||||
("Delete", VK_DELETE), ("Home", VK_HOME), ("End", VK_END),
|
||||
("Page Up", VK_PRIOR), ("Page Down", VK_NEXT),
|
||||
("Left Arrow", VK_LEFT), ("Right Arrow", VK_RIGHT),
|
||||
("Up Arrow", VK_UP), ("Down Arrow", VK_DOWN),("Scroll", VK_SCROLL)
|
||||
]
|
||||
|
||||
for text, value in keys:
|
||||
comboBox.addItem(text, value)
|
||||
|
||||
def check_hotkey(self):
|
||||
"""Check if the selected hotkey is pressed"""
|
||||
# Get the selected hotkey
|
||||
key_code = self.hotkeyComboBox.currentData()
|
||||
|
||||
# Check if key is pressed
|
||||
key_state = win32api.GetAsyncKeyState(key_code) & 0x8000 != 0
|
||||
|
||||
# Detect key press (transition from not pressed to pressed)
|
||||
if key_state and not self.lastKeyState:
|
||||
if self.instructionLabel.text() == "运行中":
|
||||
self.stop_pressing()
|
||||
else:
|
||||
self.start_pressing()
|
||||
|
||||
self.lastKeyState = key_state
|
||||
|
||||
def select_window(self):
|
||||
"""Allow user to select a target window by clicking on it"""
|
||||
self.selectedWindowLabel.setText("请点击目标窗口...")
|
||||
|
||||
# Hide our window temporarily
|
||||
self.hide()
|
||||
QApplication.processEvents()
|
||||
time.sleep(0.2) # Give a small delay for user to see the message
|
||||
|
||||
# Poll for mouse click
|
||||
while True:
|
||||
if win32api.GetAsyncKeyState(0x01) & 0x8000: # Left mouse button pressed
|
||||
cursor_pos = win32gui.GetCursorPos()
|
||||
window_handle = win32gui.WindowFromPoint(cursor_pos)
|
||||
if window_handle:
|
||||
window_text = win32gui.GetWindowText(window_handle)
|
||||
self.target_hwnd = window_handle
|
||||
|
||||
# Get process ID for debugging
|
||||
try:
|
||||
_, process_id = win32process.GetWindowThreadProcessId(window_handle)
|
||||
self.debugLabel.setText(f"状态: 已选择窗口 (PID: {process_id})")
|
||||
except:
|
||||
pass
|
||||
|
||||
self.selectedWindowLabel.setText(window_text)
|
||||
break
|
||||
QApplication.processEvents()
|
||||
time.sleep(0.1)
|
||||
|
||||
# Show our window again
|
||||
time.sleep(0.2) # Wait for mouse release
|
||||
self.show()
|
||||
|
||||
def start_pressing(self):
|
||||
"""Start sending keystrokes to the target window"""
|
||||
if not self.target_hwnd:
|
||||
QMessageBox.warning(self, "警告", "请选择窗口后,再点击开始!")
|
||||
return
|
||||
|
||||
self.instructionLabel.setText("运行中")
|
||||
self.stop_all_timers()
|
||||
|
||||
# Check if target window still exists
|
||||
if not win32gui.IsWindow(self.target_hwnd):
|
||||
QMessageBox.warning(self, "警告", "目标窗口已关闭,请重新选择!")
|
||||
self.target_hwnd = None
|
||||
self.selectedWindowLabel.setText("未选择窗口")
|
||||
self.instructionLabel.setText("停止中")
|
||||
return
|
||||
|
||||
# Try to attach to target window thread if option is checked
|
||||
if self.attachThreadCheckBox.isChecked():
|
||||
try:
|
||||
target_thread_id = win32process.GetWindowThreadProcessId(self.target_hwnd)[0]
|
||||
current_thread_id = win32api.GetCurrentThreadId()
|
||||
win32process.AttachThreadInput(current_thread_id, target_thread_id, True)
|
||||
self.debugLabel.setText("状态: 已关联线程输入")
|
||||
except Exception as e:
|
||||
self.debugLabel.setText(f"状态: 线程关联失败 ({str(e)})")
|
||||
|
||||
# Start the space key timer if checked
|
||||
if self.spaceCheckBox.isChecked():
|
||||
self.press_space()
|
||||
interval = int(self.spaceIntervalLineEdit.text())
|
||||
self.spaceTimer.start(interval)
|
||||
|
||||
# 如果选中,启动鼠标点击定时器
|
||||
if self.mouseCheckBox.isChecked():
|
||||
self.click_mouse() # 立即执行
|
||||
interval = int(self.mouseIntervalLineEdit.text())
|
||||
self.mouseTimer.start(interval)
|
||||
|
||||
# Start the custom key timers if checked
|
||||
for i in range(10):
|
||||
if self.keyCheckBoxes[i].isChecked() and self.keyCombos[i].currentIndex() != -1:
|
||||
self.press_keys(i)
|
||||
interval = int(self.intervalLineEdits[i].text())
|
||||
self.timers[i].start(interval)
|
||||
|
||||
def stop_pressing(self):
|
||||
"""Stop sending keystrokes"""
|
||||
self.instructionLabel.setText("停止中")
|
||||
self.stop_all_timers()
|
||||
|
||||
# Try to detach from target window thread if option is checked
|
||||
if self.attachThreadCheckBox.isChecked() and self.target_hwnd:
|
||||
try:
|
||||
target_thread_id = win32process.GetWindowThreadProcessId(self.target_hwnd)[0]
|
||||
current_thread_id = win32api.GetCurrentThreadId()
|
||||
win32process.AttachThreadInput(current_thread_id, target_thread_id, False)
|
||||
self.debugLabel.setText("状态: 已解除线程关联")
|
||||
except:
|
||||
pass
|
||||
|
||||
def about_me(self):
|
||||
"""Show the about dialog"""
|
||||
about_dialog = AboutMeDlg(self)
|
||||
about_dialog.exec_()
|
||||
|
||||
def stop_all_timers(self):
|
||||
"""Stop all key press timers"""
|
||||
self.spaceTimer.stop()
|
||||
self.mouseTimer.stop() # 添加鼠标定时器停止
|
||||
for timer in self.timers:
|
||||
timer.stop()
|
||||
|
||||
def press_space(self):
|
||||
"""Send space key to target window"""
|
||||
if self.target_hwnd and win32gui.IsWindow(self.target_hwnd):
|
||||
key_mode = self.modeComboBox.currentData()
|
||||
key_code = VK_SPACE
|
||||
|
||||
if key_mode == "post":
|
||||
# Use PostMessage method (original method)
|
||||
scan_code = win32api.MapVirtualKey(key_code, 0)
|
||||
|
||||
# Key down
|
||||
lparam = (scan_code << 16) | 1
|
||||
win32gui.PostMessage(self.target_hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
|
||||
# Key up
|
||||
lparam = (scan_code << 16) | (1 | (1 << 30) | (1 << 31))
|
||||
win32gui.PostMessage(self.target_hwnd, WM_KEYUP, key_code, lparam)
|
||||
|
||||
elif key_mode == "send_message":
|
||||
# Use SendMessage method (synchronous, might work better for some apps)
|
||||
scan_code = win32api.MapVirtualKey(key_code, 0)
|
||||
|
||||
# Key down
|
||||
lparam = (scan_code << 16) | 1
|
||||
win32gui.SendMessage(self.target_hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
|
||||
# Send WM_CHAR message (important for text input)
|
||||
win32gui.SendMessage(self.target_hwnd, WM_CHAR, ord(' '), lparam)
|
||||
|
||||
# Key up
|
||||
lparam = (scan_code << 16) | (1 | (1 << 30) | (1 << 31))
|
||||
win32gui.SendMessage(self.target_hwnd, WM_KEYUP, key_code, lparam)
|
||||
|
||||
else: # send mode - uses SendInput
|
||||
# Use SendInput method (works for background windows sometimes)
|
||||
self.send_key(key_code)
|
||||
|
||||
def press_keys(self, index):
|
||||
"""Send keys to target window"""
|
||||
if self.target_hwnd and win32gui.IsWindow(self.target_hwnd):
|
||||
key_code = self.keyCombos[index].currentData()
|
||||
key_mode = self.modeComboBox.currentData()
|
||||
|
||||
if key_mode == "post":
|
||||
# Use PostMessage method (original method)
|
||||
scan_code = win32api.MapVirtualKey(key_code, 0)
|
||||
|
||||
# Key down
|
||||
lparam = (scan_code << 16) | 1
|
||||
win32gui.PostMessage(self.target_hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
|
||||
# Send WM_CHAR for regular keys
|
||||
if key_code >= ord('A') and key_code <= ord('Z'):
|
||||
# Convert to lowercase for character input
|
||||
char_code = key_code + 32 # ASCII difference between uppercase and lowercase
|
||||
win32gui.PostMessage(self.target_hwnd, WM_CHAR, char_code, lparam)
|
||||
elif key_code >= ord('0') and key_code <= ord('9') or key_code == VK_SPACE:
|
||||
win32gui.PostMessage(self.target_hwnd, WM_CHAR, key_code, lparam)
|
||||
|
||||
# Key up
|
||||
lparam = (scan_code << 16) | (1 | (1 << 30) | (1 << 31))
|
||||
win32gui.PostMessage(self.target_hwnd, WM_KEYUP, key_code, lparam)
|
||||
|
||||
elif key_mode == "send_message":
|
||||
# Use SendMessage method (synchronous, might work better for some apps)
|
||||
scan_code = win32api.MapVirtualKey(key_code, 0)
|
||||
|
||||
# Key down
|
||||
lparam = (scan_code << 16) | 1
|
||||
win32gui.SendMessage(self.target_hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
|
||||
# Send WM_CHAR for regular keys
|
||||
if key_code >= ord('A') and key_code <= ord('Z'):
|
||||
# Convert to lowercase for character input
|
||||
char_code = key_code + 32 # ASCII difference between uppercase and lowercase
|
||||
win32gui.SendMessage(self.target_hwnd, WM_CHAR, char_code, lparam)
|
||||
elif key_code >= ord('0') and key_code <= ord('9') or key_code == VK_SPACE:
|
||||
win32gui.SendMessage(self.target_hwnd, WM_CHAR, key_code, lparam)
|
||||
|
||||
# Key up
|
||||
lparam = (scan_code << 16) | (1 | (1 << 30) | (1 << 31))
|
||||
win32gui.SendMessage(self.target_hwnd, WM_KEYUP, key_code, lparam)
|
||||
|
||||
else: # send mode - uses SendInput
|
||||
# Use SendInput method (works for background windows sometimes)
|
||||
self.send_key(key_code)
|
||||
|
||||
def send_key(self, key_code):
|
||||
"""Send a key press using SendInput (works for background windows sometimes)"""
|
||||
# Try to set target window to foreground (optional, may help in some cases)
|
||||
try:
|
||||
if self.target_hwnd:
|
||||
win32gui.SetForegroundWindow(self.target_hwnd)
|
||||
except:
|
||||
pass # Ignore if activation fails
|
||||
|
||||
# Create Input structure for key down
|
||||
inputs = (INPUT * 2)()
|
||||
|
||||
# Key down event
|
||||
inputs[0].type = INPUT_KEYBOARD
|
||||
inputs[0].u.ki.wVk = key_code
|
||||
inputs[0].u.ki.wScan = win32api.MapVirtualKey(key_code, 0)
|
||||
inputs[0].u.ki.dwFlags = 0
|
||||
inputs[0].u.ki.time = 0
|
||||
inputs[0].u.ki.dwExtraInfo = None
|
||||
|
||||
# Key up event
|
||||
inputs[1].type = INPUT_KEYBOARD
|
||||
inputs[1].u.ki.wVk = key_code
|
||||
inputs[1].u.ki.wScan = win32api.MapVirtualKey(key_code, 0)
|
||||
inputs[1].u.ki.dwFlags = KEYEVENTF_KEYUP
|
||||
inputs[1].u.ki.time = 0
|
||||
inputs[1].u.ki.dwExtraInfo = None
|
||||
|
||||
# Send input
|
||||
ctypes.windll.user32.SendInput(2, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
||||
|
||||
def click_mouse(self):
|
||||
"""向目标窗口发送鼠标点击"""
|
||||
if self.target_hwnd and win32gui.IsWindow(self.target_hwnd):
|
||||
key_mode = self.modeComboBox.currentData()
|
||||
|
||||
# 获取窗口客户区坐标
|
||||
try:
|
||||
# 获取窗口矩形和客户区矩形
|
||||
#window_rect = win32gui.GetWindowRect(self.target_hwnd)
|
||||
#client_rect = win32gui.GetClientRect(self.target_hwnd)
|
||||
|
||||
# 计算客户区中心
|
||||
#client_center_x = client_rect[2] // 2
|
||||
#client_center_y = client_rect[3] // 2
|
||||
|
||||
# 将客户区坐标映射到屏幕坐标
|
||||
#pt = win32gui.ClientToScreen(self.target_hwnd, (client_center_x, client_center_y))
|
||||
|
||||
if key_mode == "post" or key_mode == "send_message":
|
||||
# 使用PostMessage/SendMessage进行鼠标点击
|
||||
# 将屏幕坐标转换为客户区坐标
|
||||
#client_pt = win32gui.ScreenToClient(self.target_hwnd, pt)
|
||||
lparam = 0 #client_pt[0] | (client_pt[1] << 16)
|
||||
|
||||
if key_mode == "post":
|
||||
# 鼠标按下
|
||||
win32gui.PostMessage(self.target_hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam)
|
||||
# 鼠标抬起
|
||||
win32gui.PostMessage(self.target_hwnd, win32con.WM_LBUTTONUP, 0, lparam)
|
||||
else: # send_message
|
||||
# 鼠标按下
|
||||
win32gui.SendMessage(self.target_hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam)
|
||||
# 鼠标抬起
|
||||
win32gui.SendMessage(self.target_hwnd, win32con.WM_LBUTTONUP, 0, lparam)
|
||||
|
||||
else: # send模式 - 使用SendInput
|
||||
# 移动光标到目标位置(可选,某些应用程序可能需要)
|
||||
#try:
|
||||
# win32api.SetCursorPos(pt)
|
||||
#except:
|
||||
# pass
|
||||
|
||||
# 初始化鼠标输入结构
|
||||
inputs = (INPUT * 2)()
|
||||
|
||||
# 鼠标按下事件
|
||||
inputs[0].type = INPUT_MOUSE
|
||||
inputs[0].u.mi.dx = 0 #pt[0]
|
||||
inputs[0].u.mi.dy = 0 #pt[1]
|
||||
inputs[0].u.mi.mouseData = 0
|
||||
inputs[0].u.mi.dwFlags = MOUSEEVENTF_LEFTDOWN
|
||||
inputs[0].u.mi.time = 0
|
||||
inputs[0].u.mi.dwExtraInfo = None
|
||||
|
||||
# 鼠标抬起事件
|
||||
inputs[1].type = INPUT_MOUSE
|
||||
inputs[1].u.mi.dx = 0 #pt[0]
|
||||
inputs[1].u.mi.dy = 0 #pt[1]
|
||||
inputs[1].u.mi.mouseData = 0
|
||||
inputs[1].u.mi.dwFlags = MOUSEEVENTF_LEFTUP
|
||||
inputs[1].u.mi.time = 0
|
||||
inputs[1].u.mi.dwExtraInfo = None
|
||||
|
||||
# 发送输入
|
||||
ctypes.windll.user32.SendInput(2, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
||||
|
||||
except Exception as e:
|
||||
print(f"发送鼠标点击时出错: {str(e)}")
|
||||
|
||||
def load_settings(self):
|
||||
"""Load saved settings"""
|
||||
settings = QSettings("FinnSoft", "KeyPresser")
|
||||
|
||||
# 在load_settings方法中添加:
|
||||
self.mouseCheckBox.setChecked(settings.value("mouseCheckBox", False, type=bool))
|
||||
self.mouseIntervalLineEdit.setText(settings.value("mouseIntervalLineEdit", "1000"))
|
||||
|
||||
self.spaceCheckBox.setChecked(settings.value("spaceCheckBox", False, type=bool))
|
||||
self.spaceIntervalLineEdit.setText(settings.value("spaceIntervalLineEdit", "1000"))
|
||||
|
||||
# Mode selection
|
||||
mode_index = settings.value("modeIndex", 0, type=int)
|
||||
if 0 <= mode_index < self.modeComboBox.count():
|
||||
self.modeComboBox.setCurrentIndex(mode_index)
|
||||
|
||||
# Thread attachment option
|
||||
self.attachThreadCheckBox.setChecked(settings.value("attachThread", True, type=bool))
|
||||
|
||||
for i in range(10):
|
||||
self.keyCheckBoxes[i].setChecked(settings.value(f"keyCheckBox{i}", False, type=bool))
|
||||
combo_index = settings.value(f"keyCombo{i}", 0, type=int)
|
||||
if 0 <= combo_index < self.keyCombos[i].count():
|
||||
self.keyCombos[i].setCurrentIndex(combo_index)
|
||||
self.intervalLineEdits[i].setText(settings.value(f"intervalLineEdit{i}", "1000"))
|
||||
|
||||
# Hotkey
|
||||
hotkey_index = settings.value("hotkeyIndex", 47, type=int) # Default to Home key
|
||||
if 0 <= hotkey_index < self.hotkeyComboBox.count():
|
||||
self.hotkeyComboBox.setCurrentIndex(hotkey_index)
|
||||
|
||||
def save_settings(self):
|
||||
"""Save current settings"""
|
||||
settings = QSettings("FinnSoft", "KeyPresser")
|
||||
|
||||
settings.setValue("spaceCheckBox", self.spaceCheckBox.isChecked())
|
||||
settings.setValue("spaceIntervalLineEdit", self.spaceIntervalLineEdit.text())
|
||||
|
||||
# 在save_settings方法中添加:
|
||||
settings.setValue("mouseCheckBox", self.mouseCheckBox.isChecked())
|
||||
settings.setValue("mouseIntervalLineEdit", self.mouseIntervalLineEdit.text())
|
||||
|
||||
# Mode selection
|
||||
settings.setValue("modeIndex", self.modeComboBox.currentIndex())
|
||||
|
||||
# Thread attachment option
|
||||
settings.setValue("attachThread", self.attachThreadCheckBox.isChecked())
|
||||
|
||||
for i in range(10):
|
||||
settings.setValue(f"keyCheckBox{i}", self.keyCheckBoxes[i].isChecked())
|
||||
settings.setValue(f"keyCombo{i}", self.keyCombos[i].currentIndex())
|
||||
settings.setValue(f"intervalLineEdit{i}", self.intervalLineEdits[i].text())
|
||||
|
||||
# Hotkey
|
||||
settings.setValue("hotkeyIndex", self.hotkeyComboBox.currentIndex())
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event"""
|
||||
self.save_settings()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 启用高DPI缩放支持
|
||||
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(QIcon(':/aaa.ico'))
|
||||
window = KeyPresser()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,19 +1,27 @@
|
|||
[project]
|
||||
name = "keypress"
|
||||
name = "keypressremark"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "JianGuo",email = "jianguo.li@outlook.com"}
|
||||
]
|
||||
description = "一个简单的按键模拟工具"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"psutil (>=7.0.0,<8.0.0)",
|
||||
"keyboard (>=0.13.5,<0.14.0)",
|
||||
"serial (>=0.0.97,<0.0.98)"
|
||||
requires-python = ">=3.11"
|
||||
authors = [
|
||||
{name = "xiao liu", email = ""}
|
||||
]
|
||||
dependencies = [
|
||||
"PyQt5==5.15.11",
|
||||
"pyqt5-qt5==5.15.2",
|
||||
"pywin32==311",
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pyinstaller>=6.0.0",
|
||||
]
|
||||
# index-url = "https://pypi.org/simple/"
|
||||
|
||||
[project.scripts]
|
||||
keypressremark = "src.main:main"
|
||||
|
|
|
|||
265
readme.md
265
readme.md
|
|
@ -1,4 +1,263 @@
|
|||
## 自动按键精灵
|
||||
1. 使用poetry管理
|
||||
# KeyPressRemark
|
||||
|
||||
打包命令:pyinstaller --noconsole --onefile -i aaa.ico main.py
|
||||
一个功能强大的Windows按键模拟工具,支持向指定窗口发送按键和鼠标点击事件。本工具特别适用于游戏辅助、自动化测试、重复性操作等场景。
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
- **精准窗口选择**:通过点击选择目标窗口,无需手动查找窗口句柄
|
||||
- **多种发送模式**:
|
||||
- **PostMessage**:异步消息发送,适用于大多数应用
|
||||
- **SendMessage**:同步消息发送,确保按键顺序
|
||||
- **SendInput**:系统级输入模拟,兼容性最佳
|
||||
- **自定义按键组合**:支持10个可自定义的按键配置
|
||||
- **自动循环功能**:
|
||||
- 空格键自动发送(可调节间隔)
|
||||
- 鼠标左键自动点击(可调节间隔)
|
||||
- **快捷键控制**:F1开始/停止,方便操作
|
||||
- **智能设置保存**:自动保存用户配置,下次启动自动恢复
|
||||
|
||||
## 📋 环境要求
|
||||
|
||||
- **操作系统**:Windows 7/8/10/11
|
||||
- **Python 版本**:3.11+
|
||||
- **权限要求**:管理员权限(程序会自动请求)
|
||||
- **硬件要求**:无特殊要求
|
||||
|
||||
## 🛠️ 安装与运行
|
||||
|
||||
### 方式一:使用 uv(推荐)
|
||||
|
||||
uv 是一个快速的 Python 包管理器,安装速度比 pip 更快。
|
||||
|
||||
1. **安装 uv**:
|
||||
```bash
|
||||
pip install uv
|
||||
```
|
||||
|
||||
2. **克隆项目**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd KeyPressRemark
|
||||
```
|
||||
|
||||
3. **创建虚拟环境并安装依赖**:
|
||||
```bash
|
||||
uv venv --python 3.11
|
||||
# Windows
|
||||
.venv\Scripts\activate
|
||||
# 或者直接使用 uv pip 安装
|
||||
uv pip install PyQt5==5.15.11 pyqt5-qt5==5.15.2 pywin32==311
|
||||
```
|
||||
|
||||
4. **运行程序**:
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
### 方式二:传统 pip 方式
|
||||
|
||||
1. **克隆项目**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd KeyPressRemark
|
||||
```
|
||||
|
||||
2. **创建虚拟环境**:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. **安装依赖**:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **运行程序**:
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
### 方式三:使用打包版本
|
||||
|
||||
从 [Releases](releases) 下载预编译的 exe 文件,双击运行即可。
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 基本操作
|
||||
|
||||
1. **选择目标窗口**:
|
||||
- 点击"选择窗口"按钮
|
||||
- 鼠标变为十字形后,点击要控制的窗口
|
||||
- 成功选择后会显示窗口标题和进程信息
|
||||
|
||||
2. **配置按键**:
|
||||
- 在"自定义按键"区域,为每个按钮选择对应的按键
|
||||
- 支持字母、数字、功能键等
|
||||
|
||||
3. **选择发送模式**:
|
||||
- **PostMessage**:适用于大多数游戏和应用
|
||||
- **SendMessage**:适用于需要同步响应的应用
|
||||
- **SendInput**:适用于对按键检测严格的应用
|
||||
|
||||
4. **开始使用**:
|
||||
- 配置完成后,直接点击按键按钮发送
|
||||
- 或按 F1 开始自动循环功能
|
||||
|
||||
### 高级功能
|
||||
|
||||
#### 自动循环
|
||||
- **空格键循环**:勾选后自动按设定间隔发送空格键
|
||||
- **鼠标点击循环**:勾选后自动按设定间隔点击鼠标左键
|
||||
- **间隔设置**:支持毫秒级精度,最小间隔10ms
|
||||
|
||||
#### 快捷键控制
|
||||
- **F1**:开始/停止自动循环功能
|
||||
- **ESC**:紧急停止所有自动功能
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
KeyPressRemark/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── main.py # 程序入口点
|
||||
│ ├── core/ # 核心功能模块
|
||||
│ │ ├── admin_check.py # 管理员权限检查和申请
|
||||
│ │ ├── constants.py # 常量定义(按键码、消息常量等)
|
||||
│ │ ├── key_sender.py # 按键发送核心逻辑
|
||||
│ │ └── window_selector.py # 窗口选择功能实现
|
||||
│ ├── gui/ # 图形用户界面模块
|
||||
│ │ ├── main_window.py # 主窗口界面和逻辑
|
||||
│ │ ├── about_dialog.py # 关于对话框
|
||||
│ │ └── components/ # 可复用UI组件
|
||||
│ └── utils/ # 工具和辅助模块
|
||||
│ ├── settings.py # 设置管理(保存/加载配置)
|
||||
│ └── structures.py # Windows API结构体定义
|
||||
├── resources/ # 资源文件目录
|
||||
│ ├── icons/ # 应用图标
|
||||
│ ├── images/ # 图片资源
|
||||
│ └── qrc/ # Qt资源文件
|
||||
├── tests/ # 单元测试文件
|
||||
├── docs/ # 项目文档
|
||||
├── dist/ # 打包输出目录
|
||||
├── pyproject.toml # 项目配置文件
|
||||
├── requirements.txt # 依赖列表
|
||||
└── README.md # 项目说明文档
|
||||
```
|
||||
|
||||
## 🔧 开发说明
|
||||
|
||||
### 核心模块详解
|
||||
|
||||
#### core/key_sender.py
|
||||
负责实际的按键和鼠标事件发送,实现了三种不同的发送模式:
|
||||
|
||||
- **PostMessage模式**:异步发送,性能最佳
|
||||
- **SendMessage模式**:同步发送,响应可靠
|
||||
- **SendInput模式**:系统级模拟,兼容性最好
|
||||
|
||||
#### core/window_selector.py
|
||||
实现窗口选择功能,通过鼠标点击获取目标窗口句柄。
|
||||
|
||||
#### gui/main_window.py
|
||||
主界面实现,包含所有UI组件和事件处理逻辑。
|
||||
|
||||
#### utils/settings.py
|
||||
配置管理模块,自动保存和恢复用户设置。
|
||||
|
||||
### 依赖说明
|
||||
|
||||
本项目主要依赖以下库:
|
||||
|
||||
- **PyQt5 (5.15.11)**:GUI框架
|
||||
- 用于创建图形用户界面
|
||||
- 提供丰富的UI组件和事件处理
|
||||
|
||||
- **pyqt5-qt5 (5.15.2)**:Qt5运行时库
|
||||
- 注意:Windows平台需要使用5.15.2版本
|
||||
- 新版本5.15.17不支持Windows平台
|
||||
|
||||
- **pywin32 (311)**:Windows API封装
|
||||
- 提供Python访问Windows API的能力
|
||||
- 用于窗口操作、消息发送等系统级功能
|
||||
|
||||
### 依赖版本说明
|
||||
|
||||
⚠️ **重要提示**:本项目在Windows平台上有特定的依赖版本要求:
|
||||
|
||||
- `pyqt5-qt5` 必须使用 **5.15.2** 版本
|
||||
- 不能使用最新的 5.15.17 版本,因为该版本不提供Windows平台的wheel文件
|
||||
- 如果遇到依赖安装问题,请使用 `uv pip install` 而不是 `uv add`
|
||||
|
||||
## 📦 打包部署
|
||||
|
||||
### 使用 PyInstaller 打包
|
||||
|
||||
1. **安装打包工具**:
|
||||
```bash
|
||||
uv add --dev pyinstaller
|
||||
```
|
||||
|
||||
2. **打包为单个exe文件**:
|
||||
```bash
|
||||
pyinstaller --noconsole --onefile --icon=resources/icons/aaa.ico src/main.py --name=KeyPressRemark
|
||||
```
|
||||
|
||||
3. **打包为目录形式**(推荐,启动更快):
|
||||
```bash
|
||||
pyinstaller --noconsole --icon=resources/icons/aaa.ico src/main.py --name=KeyPressRemark
|
||||
```
|
||||
|
||||
### 打包选项说明
|
||||
|
||||
- `--noconsole`:不显示控制台窗口
|
||||
- `--onefile`:打包为单个exe文件
|
||||
- `--icon`:设置应用图标
|
||||
- `--name`:设置输出文件名
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **管理员权限**:程序需要管理员权限才能向其他应用发送按键
|
||||
2. **杀毒软件**:部分杀毒软件可能将按键模拟程序误报为恶意软件
|
||||
3. **兼容性**:不同应用对按键模拟的支持程度不同,建议测试不同的发送模式
|
||||
4. **性能**:高频率按键发送可能影响系统性能,建议合理设置间隔时间
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
1. Fork本项目
|
||||
2. 创建feature分支:`git checkout -b feature/new-feature`
|
||||
3. 提交更改:`git commit -am 'Add new feature'`
|
||||
4. 推送分支:`git push origin feature/new-feature`
|
||||
5. 提交Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证,详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 👨💻 作者
|
||||
|
||||
**xiao liu**
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v0.1.0 (当前版本)
|
||||
- ✨ 基础功能实现
|
||||
- ✨ 支持三种按键发送模式
|
||||
- ✨ 窗口选择功能
|
||||
- ✨ 自定义按键配置
|
||||
- ✨ 自动循环功能
|
||||
- ✨ 设置保存和恢复
|
||||
|
||||
## 🔮 计划功能
|
||||
|
||||
- [ ] 按键序列录制和回放
|
||||
- [ ] 更多快捷键支持
|
||||
- [ ] 多窗口同时控制
|
||||
- [ ] 定时任务功能
|
||||
- [ ] 界面主题切换
|
||||
|
||||
---
|
||||
|
||||
如有问题或建议,欢迎提交 [Issue](issues) 或联系作者。
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
PyQt5==5.15.6
|
||||
pywin32==306
|
||||
python-qt5
|
||||
PyQt5==5.15.11
|
||||
pywin32==311
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,85 @@
|
|||
"""
|
||||
管理员权限检查模块
|
||||
|
||||
本模块负责检查当前程序是否以管理员权限运行,如果没有则自动请求提升权限。
|
||||
按键模拟功能需要管理员权限才能向其他应用程序发送按键事件。
|
||||
|
||||
技术原理:
|
||||
- 使用Windows Shell API检查当前用户权限
|
||||
- 通过UAC (User Account Control) 机制请求权限提升
|
||||
- 使用ShellExecuteW API重新启动程序
|
||||
|
||||
作者: xiao liu
|
||||
版本: v0.1.0
|
||||
"""
|
||||
|
||||
import sys
|
||||
import ctypes
|
||||
|
||||
|
||||
def is_admin():
|
||||
"""
|
||||
检查当前程序是否以管理员权限运行
|
||||
|
||||
使用Windows Shell32 API的IsUserAnAdmin函数来检查当前进程
|
||||
是否具有管理员权限。这个函数是检查进程权限的标准方法。
|
||||
|
||||
Returns:
|
||||
bool: 如果具有管理员权限返回True,否则返回False
|
||||
|
||||
Note:
|
||||
此函数仅在Windows系统上有效,其他操作系统会返回False
|
||||
"""
|
||||
try:
|
||||
# 调用Windows API检查管理员权限
|
||||
# IsUserAnAdmin() 返回非零值表示具有管理员权限
|
||||
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||
except:
|
||||
# 如果API调用失败(比如在非Windows系统上),返回False
|
||||
return False
|
||||
|
||||
|
||||
def request_admin_privileges():
|
||||
"""
|
||||
请求管理员权限,如果当前没有管理员权限则重新启动程序
|
||||
|
||||
工作流程:
|
||||
1. 检查当前是否已经具有管理员权限
|
||||
2. 如果没有,使用UAC机制请求权限提升
|
||||
3. 以管理员权限重新启动当前程序
|
||||
4. 退出当前进程
|
||||
|
||||
技术细节:
|
||||
- 使用ShellExecuteW API启动新进程
|
||||
- "runas" 动词触发UAC权限提升对话框
|
||||
- 传递当前的命令行参数确保程序状态一致
|
||||
|
||||
Note:
|
||||
此函数会导致程序重新启动,当前进程会被终止
|
||||
"""
|
||||
if not is_admin():
|
||||
# 当前没有管理员权限,需要重新启动程序
|
||||
|
||||
# 构造命令行参数字符串
|
||||
# sys.argv[0] 是程序路径,sys.argv[1:] 是传递的参数
|
||||
args = " ".join(sys.argv)
|
||||
|
||||
# 使用ShellExecuteW API以管理员权限启动程序
|
||||
# 参数说明:
|
||||
# - None: 父窗口句柄(无)
|
||||
# - "runas": 执行动词,表示以管理员身份运行
|
||||
# - sys.executable: Python解释器路径
|
||||
# - args: 完整的命令行参数
|
||||
# - None: 工作目录(使用当前目录)
|
||||
# - 1: 显示方式(SW_SHOWNORMAL)
|
||||
ctypes.windll.shell32.ShellExecuteW(
|
||||
None, # 父窗口
|
||||
"runas", # 执行动词(触发UAC)
|
||||
sys.executable, # 可执行文件路径
|
||||
args, # 命令行参数
|
||||
None, # 工作目录
|
||||
1 # 显示方式
|
||||
)
|
||||
|
||||
# 退出当前进程,新的管理员权限进程将接管
|
||||
sys.exit(0)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Virtual Key Constants
|
||||
VK_SPACE = 0x20
|
||||
VK_RETURN = 0x0D
|
||||
VK_TAB = 0x09
|
||||
VK_ESCAPE = 0x1B
|
||||
VK_BACK = 0x08
|
||||
VK_INSERT = 0x2D
|
||||
VK_DELETE = 0x2E
|
||||
VK_HOME = 0x24
|
||||
VK_END = 0x23
|
||||
VK_PRIOR = 0x21 # Page Up
|
||||
VK_NEXT = 0x22 # Page Down
|
||||
VK_LEFT = 0x25
|
||||
VK_RIGHT = 0x27
|
||||
VK_UP = 0x26
|
||||
VK_DOWN = 0x28
|
||||
VK_F1 = 0x70
|
||||
VK_F2 = 0x71
|
||||
VK_F3 = 0x72
|
||||
VK_F4 = 0x73
|
||||
VK_F5 = 0x74
|
||||
VK_F6 = 0x75
|
||||
VK_F7 = 0x76
|
||||
VK_F8 = 0x77
|
||||
VK_F9 = 0x78
|
||||
VK_F10 = 0x79
|
||||
VK_F11 = 0x7A
|
||||
VK_F12 = 0x7B
|
||||
VK_SCROLL = 0x91
|
||||
|
||||
# Windows Message Constants
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_CHAR = 0x0102
|
||||
|
||||
# SendInput Constants
|
||||
INPUT_KEYBOARD = 1
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
KEYEVENTF_UNICODE = 0x0004
|
||||
|
||||
# Mouse Event Constants
|
||||
MOUSEEVENTF_MOVE = 0x0001
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002
|
||||
MOUSEEVENTF_LEFTUP = 0x0004
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000
|
||||
|
||||
# Input Types
|
||||
INPUT_MOUSE = 0
|
||||
INPUT_KEYBOARD = 1
|
||||
|
||||
# Key combinations for UI
|
||||
KEY_COMBINATIONS = [
|
||||
("F1", VK_F1), ("F2", VK_F2), ("F3", VK_F3), ("F4", VK_F4),
|
||||
("F5", VK_F5), ("F6", VK_F6), ("F7", VK_F7), ("F8", VK_F8),
|
||||
("F9", VK_F9), ("F10", VK_F10), ("F11", VK_F11), ("F12", VK_F12),
|
||||
("A", ord('A')), ("B", ord('B')), ("C", ord('C')), ("D", ord('D')),
|
||||
("E", ord('E')), ("F", ord('F')), ("G", ord('G')), ("H", ord('H')),
|
||||
("I", ord('I')), ("J", ord('J')), ("K", ord('K')), ("L", ord('L')),
|
||||
("M", ord('M')), ("N", ord('N')), ("O", ord('O')), ("P", ord('P')),
|
||||
("Q", ord('Q')), ("R", ord('R')), ("S", ord('S')), ("T", ord('T')),
|
||||
("U", ord('U')), ("V", ord('V')), ("W", ord('W')), ("X", ord('X')),
|
||||
("Y", ord('Y')), ("Z", ord('Z')),
|
||||
("0", ord('0')), ("1", ord('1')), ("2", ord('2')), ("3", ord('3')),
|
||||
("4", ord('4')), ("5", ord('5')), ("6", ord('6')), ("7", ord('7')),
|
||||
("8", ord('8')), ("9", ord('9')),
|
||||
("Space", VK_SPACE), ("Enter", VK_RETURN), ("Tab", VK_TAB),
|
||||
("Esc", VK_ESCAPE), ("Backspace", VK_BACK), ("Insert", VK_INSERT),
|
||||
("Delete", VK_DELETE), ("Home", VK_HOME), ("End", VK_END),
|
||||
("Page Up", VK_PRIOR), ("Page Down", VK_NEXT),
|
||||
("Left Arrow", VK_LEFT), ("Right Arrow", VK_RIGHT),
|
||||
("Up Arrow", VK_UP), ("Down Arrow", VK_DOWN), ("Scroll", VK_SCROLL)
|
||||
]
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
"""
|
||||
KeySender - 按键和鼠标事件发送模块
|
||||
|
||||
本模块实现了三种不同的按键发送方式:
|
||||
1. PostMessage - 异步消息发送,性能最佳,适用于大多数应用
|
||||
2. SendMessage - 同步消息发送,响应可靠,确保按键顺序
|
||||
3. SendInput - 系统级输入模拟,兼容性最好,适用于严格的应用
|
||||
|
||||
技术原理:
|
||||
- 使用Windows API进行底层按键模拟
|
||||
- 支持向指定窗口发送按键和鼠标事件
|
||||
- 自动处理按键扫描码和消息参数
|
||||
|
||||
作者: xiao liu
|
||||
版本: v0.1.0
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import win32gui
|
||||
import win32api
|
||||
import win32con
|
||||
from .constants import *
|
||||
from utils.structures import INPUT
|
||||
|
||||
|
||||
class KeySender:
|
||||
"""
|
||||
按键发送器类
|
||||
|
||||
负责向目标窗口发送按键和鼠标事件,支持三种不同的发送模式。
|
||||
"""
|
||||
|
||||
def __init__(self, target_hwnd=None):
|
||||
"""
|
||||
初始化按键发送器
|
||||
|
||||
Args:
|
||||
target_hwnd (int, optional): 目标窗口句柄. Defaults to None.
|
||||
"""
|
||||
self.target_hwnd = target_hwnd
|
||||
|
||||
def set_target_window(self, hwnd):
|
||||
"""
|
||||
设置目标窗口句柄
|
||||
|
||||
Args:
|
||||
hwnd (int): 窗口句柄
|
||||
"""
|
||||
self.target_hwnd = hwnd
|
||||
|
||||
def send_key_post_message(self, key_code):
|
||||
"""
|
||||
使用PostMessage方式发送按键
|
||||
|
||||
PostMessage是异步发送方式,消息会被放入目标窗口的消息队列中,
|
||||
然后立即返回,不等待处理完成。这种方式性能最佳,适用于大多数应用。
|
||||
|
||||
Args:
|
||||
key_code (int): 按键的虚拟键码
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True,失败返回False
|
||||
"""
|
||||
# 检查目标窗口是否有效
|
||||
if not self.target_hwnd or not win32gui.IsWindow(self.target_hwnd):
|
||||
return False
|
||||
|
||||
# 将虚拟键码转换为扫描码
|
||||
# 扫描码是键盘硬件相关的代码,某些应用可能需要
|
||||
scan_code = win32api.MapVirtualKey(key_code, 0)
|
||||
|
||||
# 构造按键按下事件的lparam参数
|
||||
# lparam包含重复次数、扫描码、扩展键标志等信息
|
||||
# 格式:[31] 转换状态 [30] 前一个状态 [29] 上下文代码 [28:25] 保留
|
||||
# [24] 扩展键 [23:16] 扫描码 [15:0] 重复次数
|
||||
lparam = (scan_code << 16) | 1
|
||||
|
||||
# 发送按键按下消息
|
||||
win32gui.PostMessage(self.target_hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
|
||||
# 对于可打印字符,还需要发送WM_CHAR消息
|
||||
# WM_CHAR消息包含字符的Unicode值,用于文本输入
|
||||
if key_code >= ord('A') and key_code <= ord('Z'):
|
||||
# 大写字母转换为小写
|
||||
char_code = key_code + 32
|
||||
win32gui.PostMessage(self.target_hwnd, WM_CHAR, char_code, lparam)
|
||||
elif (key_code >= ord('0') and key_code <= ord('9')) or key_code == VK_SPACE:
|
||||
# 数字和空格键直接发送
|
||||
win32gui.PostMessage(self.target_hwnd, WM_CHAR, key_code, lparam)
|
||||
|
||||
# 构造按键释放事件的lparam参数
|
||||
# 设置第30位(前一个状态)和第31位(转换状态)为1,表示按键释放
|
||||
lparam = (scan_code << 16) | (1 | (1 << 30) | (1 << 31))
|
||||
|
||||
# 发送按键释放消息
|
||||
win32gui.PostMessage(self.target_hwnd, WM_KEYUP, key_code, lparam)
|
||||
|
||||
return True
|
||||
|
||||
def send_key_send_message(self, key_code):
|
||||
"""
|
||||
使用SendMessage方式发送按键
|
||||
|
||||
SendMessage是同步发送方式,会等待目标窗口处理完消息后才返回。
|
||||
这种方式响应可靠,能确保按键的处理顺序,适用于需要同步响应的应用。
|
||||
|
||||
Args:
|
||||
key_code (int): 按键的虚拟键码
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True,失败返回False
|
||||
"""
|
||||
# 检查目标窗口是否有效
|
||||
if not self.target_hwnd or not win32gui.IsWindow(self.target_hwnd):
|
||||
return False
|
||||
|
||||
# 将虚拟键码转换为扫描码
|
||||
scan_code = win32api.MapVirtualKey(key_code, 0)
|
||||
|
||||
# 构造按键按下事件的lparam参数
|
||||
lparam = (scan_code << 16) | 1
|
||||
|
||||
# 发送按键按下消息(同步)
|
||||
win32gui.SendMessage(self.target_hwnd, WM_KEYDOWN, key_code, lparam)
|
||||
|
||||
# 对于可打印字符,发送WM_CHAR消息
|
||||
if key_code >= ord('A') and key_code <= ord('Z'):
|
||||
char_code = key_code + 32 # 转换为小写
|
||||
win32gui.SendMessage(self.target_hwnd, WM_CHAR, char_code, lparam)
|
||||
elif (key_code >= ord('0') and key_code <= ord('9')) or key_code == VK_SPACE:
|
||||
win32gui.SendMessage(self.target_hwnd, WM_CHAR, key_code, lparam)
|
||||
|
||||
# 构造按键释放事件的lparam参数
|
||||
lparam = (scan_code << 16) | (1 | (1 << 30) | (1 << 31))
|
||||
|
||||
# 发送按键释放消息(同步)
|
||||
win32gui.SendMessage(self.target_hwnd, WM_KEYUP, key_code, lparam)
|
||||
|
||||
return True
|
||||
|
||||
def send_key_send_input(self, key_code):
|
||||
"""
|
||||
使用SendInput方式发送按键
|
||||
|
||||
SendInput是系统级的输入模拟方式,通过系统的输入队列发送按键事件。
|
||||
这种方式兼容性最好,能被大多数应用正确识别,适用于对按键检测严格的应用。
|
||||
|
||||
Args:
|
||||
key_code (int): 按键的虚拟键码
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True,失败返回False
|
||||
"""
|
||||
# 尝试将目标窗口设置为前台窗口
|
||||
# SendInput发送的是全局输入事件,需要目标窗口处于前台才能接收
|
||||
try:
|
||||
if self.target_hwnd:
|
||||
win32gui.SetForegroundWindow(self.target_hwnd)
|
||||
except:
|
||||
# 如果设置前台窗口失败,继续执行(某些情况下可能会被系统阻止)
|
||||
pass
|
||||
|
||||
# 创建INPUT结构数组,包含按键按下和释放两个事件
|
||||
inputs = (INPUT * 2)()
|
||||
|
||||
# 设置按键按下事件
|
||||
inputs[0].type = INPUT_KEYBOARD # 输入类型:键盘
|
||||
inputs[0].u.ki.wVk = key_code # 虚拟键码
|
||||
inputs[0].u.ki.wScan = win32api.MapVirtualKey(key_code, 0) # 扫描码
|
||||
inputs[0].u.ki.dwFlags = 0 # 标志:0表示按键按下
|
||||
inputs[0].u.ki.time = 0 # 时间戳:0表示系统自动生成
|
||||
inputs[0].u.ki.dwExtraInfo = None # 额外信息
|
||||
|
||||
# 设置按键释放事件
|
||||
inputs[1].type = INPUT_KEYBOARD # 输入类型:键盘
|
||||
inputs[1].u.ki.wVk = key_code # 虚拟键码
|
||||
inputs[1].u.ki.wScan = win32api.MapVirtualKey(key_code, 0) # 扫描码
|
||||
inputs[1].u.ki.dwFlags = KEYEVENTF_KEYUP # 标志:KEYEVENTF_KEYUP表示按键释放
|
||||
inputs[1].u.ki.time = 0 # 时间戳
|
||||
inputs[1].u.ki.dwExtraInfo = None # 额外信息
|
||||
|
||||
# 调用SendInput API发送输入事件
|
||||
# 参数:事件数量、事件数组指针、单个事件结构大小
|
||||
ctypes.windll.user32.SendInput(2, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
||||
return True
|
||||
|
||||
def send_key(self, key_code, mode="post"):
|
||||
"""
|
||||
根据指定模式发送按键
|
||||
|
||||
Args:
|
||||
key_code (int): 按键的虚拟键码
|
||||
mode (str): 发送模式,可选值:
|
||||
"post" - 使用PostMessage
|
||||
"send_message" - 使用SendMessage
|
||||
"send" - 使用SendInput
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True,失败返回False
|
||||
"""
|
||||
if mode == "post":
|
||||
return self.send_key_post_message(key_code)
|
||||
elif mode == "send_message":
|
||||
return self.send_key_send_message(key_code)
|
||||
elif mode == "send":
|
||||
return self.send_key_send_input(key_code)
|
||||
return False
|
||||
|
||||
def send_mouse_click(self, mode="post"):
|
||||
"""
|
||||
向目标窗口发送鼠标左键点击事件
|
||||
|
||||
支持三种发送模式,与按键发送类似:
|
||||
- post/send_message: 向目标窗口发送鼠标消息
|
||||
- send: 使用SendInput发送系统级鼠标事件
|
||||
|
||||
Args:
|
||||
mode (str): 发送模式,可选值:"post", "send_message", "send"
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True,失败返回False
|
||||
"""
|
||||
# 检查目标窗口是否有效
|
||||
if not self.target_hwnd or not win32gui.IsWindow(self.target_hwnd):
|
||||
return False
|
||||
|
||||
try:
|
||||
if mode == "post" or mode == "send_message":
|
||||
# 使用消息方式发送鼠标点击
|
||||
# lparam通常包含鼠标坐标,这里设为0表示窗口左上角
|
||||
lparam = 0
|
||||
|
||||
if mode == "post":
|
||||
# 异步发送鼠标按下和释放消息
|
||||
win32gui.PostMessage(self.target_hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam)
|
||||
win32gui.PostMessage(self.target_hwnd, win32con.WM_LBUTTONUP, 0, lparam)
|
||||
else: # send_message
|
||||
# 同步发送鼠标按下和释放消息
|
||||
win32gui.SendMessage(self.target_hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam)
|
||||
win32gui.SendMessage(self.target_hwnd, win32con.WM_LBUTTONUP, 0, lparam)
|
||||
else: # send mode
|
||||
# 使用SendInput方式发送系统级鼠标事件
|
||||
inputs = (INPUT * 2)()
|
||||
|
||||
# 设置鼠标按下事件
|
||||
inputs[0].type = INPUT_MOUSE # 输入类型:鼠标
|
||||
inputs[0].u.mi.dx = 0 # X坐标偏移
|
||||
inputs[0].u.mi.dy = 0 # Y坐标偏移
|
||||
inputs[0].u.mi.mouseData = 0 # 鼠标数据(滚轮等)
|
||||
inputs[0].u.mi.dwFlags = MOUSEEVENTF_LEFTDOWN # 标志:左键按下
|
||||
inputs[0].u.mi.time = 0 # 时间戳
|
||||
inputs[0].u.mi.dwExtraInfo = None # 额外信息
|
||||
|
||||
# 设置鼠标释放事件
|
||||
inputs[1].type = INPUT_MOUSE # 输入类型:鼠标
|
||||
inputs[1].u.mi.dx = 0 # X坐标偏移
|
||||
inputs[1].u.mi.dy = 0 # Y坐标偏移
|
||||
inputs[1].u.mi.mouseData = 0 # 鼠标数据
|
||||
inputs[1].u.mi.dwFlags = MOUSEEVENTF_LEFTUP # 标志:左键释放
|
||||
inputs[1].u.mi.time = 0 # 时间戳
|
||||
inputs[1].u.mi.dwExtraInfo = None # 额外信息
|
||||
|
||||
# 发送鼠标输入事件
|
||||
ctypes.windll.user32.SendInput(2, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"发送鼠标点击时出错: {str(e)}")
|
||||
return False
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import time
|
||||
import win32gui
|
||||
import win32api
|
||||
import win32process
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
|
||||
class WindowSelector:
|
||||
def __init__(self):
|
||||
self.target_hwnd = None
|
||||
|
||||
def select_window(self, parent_widget=None):
|
||||
"""Allow user to select a target window by clicking on it"""
|
||||
if parent_widget:
|
||||
parent_widget.selectedWindowLabel.setText("请点击目标窗口...")
|
||||
parent_widget.hide()
|
||||
|
||||
QApplication.processEvents()
|
||||
time.sleep(0.2)
|
||||
|
||||
while True:
|
||||
if win32api.GetAsyncKeyState(0x01) & 0x8000: # Left mouse button pressed
|
||||
cursor_pos = win32gui.GetCursorPos()
|
||||
window_handle = win32gui.WindowFromPoint(cursor_pos)
|
||||
if window_handle:
|
||||
window_text = win32gui.GetWindowText(window_handle)
|
||||
self.target_hwnd = window_handle
|
||||
|
||||
# Get process ID for debugging
|
||||
if parent_widget:
|
||||
try:
|
||||
_, process_id = win32process.GetWindowThreadProcessId(window_handle)
|
||||
parent_widget.debugLabel.setText(f"状态: 已选择窗口 (PID: {process_id})")
|
||||
except:
|
||||
pass
|
||||
|
||||
parent_widget.selectedWindowLabel.setText(window_text)
|
||||
|
||||
break
|
||||
QApplication.processEvents()
|
||||
time.sleep(0.1)
|
||||
|
||||
if parent_widget:
|
||||
time.sleep(0.2)
|
||||
parent_widget.show()
|
||||
|
||||
return self.target_hwnd, window_text if 'window_text' in locals() else ""
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("关于")
|
||||
self.setFixedSize(400, 200)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
title = QLabel("KeyPresser", self)
|
||||
title.setStyleSheet("font-size: 18pt; font-weight: bold;")
|
||||
layout.addWidget(title)
|
||||
|
||||
desc = QLabel("一个简单的按键模拟工具", self)
|
||||
layout.addWidget(desc)
|
||||
|
||||
version = QLabel("版本: 专业版 (Python 无钩子版)", self)
|
||||
layout.addWidget(version)
|
||||
|
||||
author = QLabel("作者: xiao liu", self)
|
||||
layout.addWidget(author)
|
||||
|
||||
ok_button = QPushButton("确定", self)
|
||||
ok_button.clicked.connect(self.accept)
|
||||
layout.addWidget(ok_button)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
import win32gui
|
||||
import win32api
|
||||
import win32process
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QLabel, QLineEdit, QCheckBox, QComboBox,
|
||||
QGridLayout, QMessageBox)
|
||||
from PyQt5.QtCore import QTimer, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from core.constants import KEY_COMBINATIONS, VK_SPACE
|
||||
from core.window_selector import WindowSelector
|
||||
from core.key_sender import KeySender
|
||||
from utils.settings import SettingsManager
|
||||
from .about_dialog import AboutDialog
|
||||
|
||||
|
||||
class KeyPresser(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.target_hwnd = None
|
||||
self.timers = []
|
||||
self.window_selector = WindowSelector()
|
||||
self.key_sender = KeySender()
|
||||
self.settings_manager = SettingsManager()
|
||||
|
||||
# Set application icon
|
||||
self.setWindowIcon(QIcon('resources/icons/aaa.ico'))
|
||||
|
||||
self.init_ui()
|
||||
self.settings_manager.load_settings(self)
|
||||
|
||||
# Hotkey timer
|
||||
self.hotkeyTimer = QTimer(self)
|
||||
self.hotkeyTimer.timeout.connect(self.check_hotkey)
|
||||
self.hotkeyTimer.start(50)
|
||||
|
||||
# Mouse timer
|
||||
self.mouseTimer = QTimer(self)
|
||||
self.mouseTimer.timeout.connect(self.click_mouse)
|
||||
|
||||
# Space timer
|
||||
self.spaceTimer = QTimer(self)
|
||||
self.spaceTimer.timeout.connect(self.press_space)
|
||||
|
||||
# Last key state for hotkey detection
|
||||
self.lastKeyState = False
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("KeyPresser(无钩子版)")
|
||||
self.setFixedWidth(300)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Window selection
|
||||
label = QLabel("选择窗口:", self)
|
||||
layout.addWidget(label)
|
||||
|
||||
self.selectedWindowLabel = QLabel("未选择窗口", self)
|
||||
self.selectedWindowLabel.setStyleSheet("color: green;")
|
||||
layout.addWidget(self.selectedWindowLabel)
|
||||
|
||||
selectButton = QPushButton("选择窗口", self)
|
||||
layout.addWidget(selectButton)
|
||||
|
||||
# Mode selection
|
||||
modeLayout = QHBoxLayout()
|
||||
modeLabel = QLabel("按键模式:", self)
|
||||
modeLayout.addWidget(modeLabel)
|
||||
|
||||
self.modeComboBox = QComboBox(self)
|
||||
self.modeComboBox.addItem("PostMessage (常规模式)", "post")
|
||||
self.modeComboBox.addItem("SendInput (后台模式)", "send")
|
||||
self.modeComboBox.addItem("SendMessage (直接模式)", "send_message")
|
||||
modeLayout.addWidget(self.modeComboBox)
|
||||
|
||||
layout.addLayout(modeLayout)
|
||||
|
||||
# Space key settings
|
||||
spaceLayout = QHBoxLayout()
|
||||
self.spaceCheckBox = QCheckBox(self)
|
||||
spaceLayout.addWidget(self.spaceCheckBox)
|
||||
|
||||
spaceLabel = QLabel("空格键时间间隔 (毫秒):", self)
|
||||
spaceLayout.addWidget(spaceLabel)
|
||||
|
||||
self.spaceIntervalLineEdit = QLineEdit(self)
|
||||
self.spaceIntervalLineEdit.setText("1000")
|
||||
spaceLayout.addWidget(self.spaceIntervalLineEdit)
|
||||
|
||||
layout.addLayout(spaceLayout)
|
||||
|
||||
# Mouse settings
|
||||
mouseLayout = QHBoxLayout()
|
||||
self.mouseCheckBox = QCheckBox(self)
|
||||
mouseLayout.addWidget(self.mouseCheckBox)
|
||||
|
||||
mouseLabel = QLabel("鼠标左键时间间隔 (毫秒):", self)
|
||||
mouseLayout.addWidget(mouseLabel)
|
||||
|
||||
self.mouseIntervalLineEdit = QLineEdit(self)
|
||||
self.mouseIntervalLineEdit.setText("1000")
|
||||
mouseLayout.addWidget(self.mouseIntervalLineEdit)
|
||||
|
||||
layout.addLayout(mouseLayout)
|
||||
|
||||
# Custom keys
|
||||
keysLabel = QLabel("自定义按键和时间间隔 (毫秒):", self)
|
||||
layout.addWidget(keysLabel)
|
||||
|
||||
keysLayout = QGridLayout()
|
||||
self.keyCheckBoxes = []
|
||||
self.keyCombos = []
|
||||
self.intervalLineEdits = []
|
||||
|
||||
for i in range(10):
|
||||
checkbox = QCheckBox(self)
|
||||
keysLayout.addWidget(checkbox, i, 0)
|
||||
self.keyCheckBoxes.append(checkbox)
|
||||
|
||||
combobox = QComboBox(self)
|
||||
self.populate_key_combos(combobox)
|
||||
keysLayout.addWidget(combobox, i, 1)
|
||||
self.keyCombos.append(combobox)
|
||||
|
||||
lineEdit = QLineEdit(self)
|
||||
lineEdit.setText("1000")
|
||||
keysLayout.addWidget(lineEdit, i, 2)
|
||||
self.intervalLineEdits.append(lineEdit)
|
||||
|
||||
# Create timer for this key
|
||||
timer = QTimer(self)
|
||||
timer.timeout.connect(lambda checked=False, index=i: self.press_keys(index))
|
||||
self.timers.append(timer)
|
||||
|
||||
layout.addLayout(keysLayout)
|
||||
|
||||
# Control buttons
|
||||
startButton = QPushButton("开始", self)
|
||||
layout.addWidget(startButton)
|
||||
|
||||
stopButton = QPushButton("停止", self)
|
||||
layout.addWidget(stopButton)
|
||||
|
||||
labelPrompt = QLabel("修改配置后需点击开始按钮以使更改生效。", self)
|
||||
labelPrompt.setStyleSheet("color: red;")
|
||||
layout.addWidget(labelPrompt)
|
||||
|
||||
self.instructionLabel = QLabel("停止中", self)
|
||||
self.instructionLabel.setStyleSheet("color: green;")
|
||||
layout.addWidget(self.instructionLabel)
|
||||
|
||||
# Hotkey selection
|
||||
hotkeyLayout = QHBoxLayout()
|
||||
hotkeyLabel = QLabel("开始/停止快捷键:", self)
|
||||
hotkeyLayout.addWidget(hotkeyLabel)
|
||||
|
||||
self.hotkeyComboBox = QComboBox(self)
|
||||
self.populate_key_combos(self.hotkeyComboBox)
|
||||
self.hotkeyComboBox.setCurrentText("Home")
|
||||
hotkeyLayout.addWidget(self.hotkeyComboBox)
|
||||
|
||||
layout.addLayout(hotkeyLayout)
|
||||
|
||||
# Debug info
|
||||
self.debugLabel = QLabel("状态: 管理员模式 (使用轮询检测热键)", self)
|
||||
self.debugLabel.setStyleSheet("color: blue;")
|
||||
layout.addWidget(self.debugLabel)
|
||||
|
||||
# Thread input settings
|
||||
threadLayout = QHBoxLayout()
|
||||
self.attachThreadCheckBox = QCheckBox("关联线程输入", self)
|
||||
self.attachThreadCheckBox.setChecked(True)
|
||||
threadLayout.addWidget(self.attachThreadCheckBox)
|
||||
|
||||
layout.addLayout(threadLayout)
|
||||
|
||||
aboutButton = QPushButton("关于", self)
|
||||
layout.addWidget(aboutButton)
|
||||
|
||||
# Connect signals
|
||||
selectButton.clicked.connect(self.select_window)
|
||||
startButton.clicked.connect(self.start_pressing)
|
||||
stopButton.clicked.connect(self.stop_pressing)
|
||||
aboutButton.clicked.connect(self.about_me)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def populate_key_combos(self, comboBox):
|
||||
"""Populate combo box with key combinations"""
|
||||
for text, value in KEY_COMBINATIONS:
|
||||
comboBox.addItem(text, value)
|
||||
|
||||
def check_hotkey(self):
|
||||
"""Check if the selected hotkey is pressed"""
|
||||
key_code = self.hotkeyComboBox.currentData()
|
||||
key_state = win32api.GetAsyncKeyState(key_code) & 0x8000 != 0
|
||||
|
||||
if key_state and not self.lastKeyState:
|
||||
if self.instructionLabel.text() == "运行中":
|
||||
self.stop_pressing()
|
||||
else:
|
||||
self.start_pressing()
|
||||
|
||||
self.lastKeyState = key_state
|
||||
|
||||
def select_window(self):
|
||||
"""Select target window"""
|
||||
hwnd, window_text = self.window_selector.select_window(self)
|
||||
if hwnd:
|
||||
self.target_hwnd = hwnd
|
||||
self.key_sender.set_target_window(hwnd)
|
||||
|
||||
def start_pressing(self):
|
||||
"""Start sending keystrokes to the target window"""
|
||||
if not self.target_hwnd:
|
||||
QMessageBox.warning(self, "警告", "请选择窗口后,再点击开始!")
|
||||
return
|
||||
|
||||
self.instructionLabel.setText("运行中")
|
||||
self.stop_all_timers()
|
||||
|
||||
if not win32gui.IsWindow(self.target_hwnd):
|
||||
QMessageBox.warning(self, "警告", "目标窗口已关闭,请重新选择!")
|
||||
self.target_hwnd = None
|
||||
self.selectedWindowLabel.setText("未选择窗口")
|
||||
self.instructionLabel.setText("停止中")
|
||||
return
|
||||
|
||||
# Thread attachment
|
||||
if self.attachThreadCheckBox.isChecked():
|
||||
try:
|
||||
target_thread_id = win32process.GetWindowThreadProcessId(self.target_hwnd)[0]
|
||||
current_thread_id = win32api.GetCurrentThreadId()
|
||||
win32process.AttachThreadInput(current_thread_id, target_thread_id, True)
|
||||
self.debugLabel.setText("状态: 已关联线程输入")
|
||||
except Exception as e:
|
||||
self.debugLabel.setText(f"状态: 线程关联失败 ({str(e)})")
|
||||
|
||||
# Start space key timer
|
||||
if self.spaceCheckBox.isChecked():
|
||||
self.press_space()
|
||||
interval = int(self.spaceIntervalLineEdit.text())
|
||||
self.spaceTimer.start(interval)
|
||||
|
||||
# Start mouse click timer
|
||||
if self.mouseCheckBox.isChecked():
|
||||
self.click_mouse()
|
||||
interval = int(self.mouseIntervalLineEdit.text())
|
||||
self.mouseTimer.start(interval)
|
||||
|
||||
# Start custom key timers
|
||||
for i in range(10):
|
||||
if self.keyCheckBoxes[i].isChecked() and self.keyCombos[i].currentIndex() != -1:
|
||||
self.press_keys(i)
|
||||
interval = int(self.intervalLineEdits[i].text())
|
||||
self.timers[i].start(interval)
|
||||
|
||||
def stop_pressing(self):
|
||||
"""Stop sending keystrokes"""
|
||||
self.instructionLabel.setText("停止中")
|
||||
self.stop_all_timers()
|
||||
|
||||
if self.attachThreadCheckBox.isChecked() and self.target_hwnd:
|
||||
try:
|
||||
target_thread_id = win32process.GetWindowThreadProcessId(self.target_hwnd)[0]
|
||||
current_thread_id = win32api.GetCurrentThreadId()
|
||||
win32process.AttachThreadInput(current_thread_id, target_thread_id, False)
|
||||
self.debugLabel.setText("状态: 已解除线程关联")
|
||||
except:
|
||||
pass
|
||||
|
||||
def stop_all_timers(self):
|
||||
"""Stop all timers"""
|
||||
self.spaceTimer.stop()
|
||||
self.mouseTimer.stop()
|
||||
for timer in self.timers:
|
||||
timer.stop()
|
||||
|
||||
def press_space(self):
|
||||
"""Send space key to target window"""
|
||||
mode = self.modeComboBox.currentData()
|
||||
self.key_sender.send_key(VK_SPACE, mode)
|
||||
|
||||
def press_keys(self, index):
|
||||
"""Send custom keys to target window"""
|
||||
key_code = self.keyCombos[index].currentData()
|
||||
mode = self.modeComboBox.currentData()
|
||||
self.key_sender.send_key(key_code, mode)
|
||||
|
||||
def click_mouse(self):
|
||||
"""Send mouse click to target window"""
|
||||
mode = self.modeComboBox.currentData()
|
||||
self.key_sender.send_mouse_click(mode)
|
||||
|
||||
def about_me(self):
|
||||
"""Show about dialog"""
|
||||
about_dialog = AboutDialog(self)
|
||||
about_dialog.exec_()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event"""
|
||||
self.settings_manager.save_settings(self)
|
||||
super().closeEvent(event)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
"""
|
||||
KeyPressRemark - Windows 按键模拟工具主入口
|
||||
|
||||
这是应用程序的主入口点,负责:
|
||||
1. 初始化应用程序环境
|
||||
2. 检查和申请管理员权限
|
||||
3. 配置高DPI显示支持
|
||||
4. 创建并显示主窗口
|
||||
|
||||
作者: xiao liu
|
||||
版本: v0.1.0
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
# 添加项目根目录到Python路径,确保能正确导入模块
|
||||
import os
|
||||
project_root = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 导入Qt资源文件(包含图标等资源)
|
||||
import resources.qrc.resources_rc
|
||||
|
||||
# 导入核心功能模块
|
||||
from core.admin_check import request_admin_privileges
|
||||
from gui.main_window import KeyPresser
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
应用程序主函数
|
||||
|
||||
执行流程:
|
||||
1. 检查并申请管理员权限(按键模拟需要管理员权限)
|
||||
2. 启用高DPI缩放支持(支持高分辨率显示器)
|
||||
3. 创建Qt应用程序实例
|
||||
4. 设置应用程序图标
|
||||
5. 创建并显示主窗口
|
||||
6. 启动事件循环
|
||||
"""
|
||||
# 第一步:检查并申请管理员权限
|
||||
# 按键模拟功能需要管理员权限才能向其他应用发送按键事件
|
||||
request_admin_privileges()
|
||||
|
||||
# 第二步:启用高DPI缩放支持
|
||||
# 确保应用在高分辨率显示器上正常显示
|
||||
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
|
||||
# 第三步:创建Qt应用程序实例
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 第四步:设置应用程序图标
|
||||
app.setWindowIcon(QIcon('resources/icons/aaa.ico'))
|
||||
|
||||
# 第五步:创建主窗口实例
|
||||
window = KeyPresser()
|
||||
|
||||
# 第六步:显示主窗口
|
||||
window.show()
|
||||
|
||||
# 第七步:启动Qt事件循环,直到程序退出
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 当作为主程序运行时,调用main函数
|
||||
main()
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from PyQt5.QtCore import QSettings
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, company="FinnSoft", application="KeyPresser"):
|
||||
self.settings = QSettings(company, application)
|
||||
|
||||
def save_settings(self, widget):
|
||||
"""Save current settings from the main widget"""
|
||||
self.settings.setValue("spaceCheckBox", widget.spaceCheckBox.isChecked())
|
||||
self.settings.setValue("spaceIntervalLineEdit", widget.spaceIntervalLineEdit.text())
|
||||
self.settings.setValue("mouseCheckBox", widget.mouseCheckBox.isChecked())
|
||||
self.settings.setValue("mouseIntervalLineEdit", widget.mouseIntervalLineEdit.text())
|
||||
self.settings.setValue("modeIndex", widget.modeComboBox.currentIndex())
|
||||
self.settings.setValue("attachThread", widget.attachThreadCheckBox.isChecked())
|
||||
self.settings.setValue("hotkeyIndex", widget.hotkeyComboBox.currentIndex())
|
||||
|
||||
for i in range(10):
|
||||
self.settings.setValue(f"keyCheckBox{i}", widget.keyCheckBoxes[i].isChecked())
|
||||
self.settings.setValue(f"keyCombo{i}", widget.keyCombos[i].currentIndex())
|
||||
self.settings.setValue(f"intervalLineEdit{i}", widget.intervalLineEdits[i].text())
|
||||
|
||||
def load_settings(self, widget):
|
||||
"""Load saved settings into the main widget"""
|
||||
widget.mouseCheckBox.setChecked(self.settings.value("mouseCheckBox", False, type=bool))
|
||||
widget.mouseIntervalLineEdit.setText(self.settings.value("mouseIntervalLineEdit", "1000"))
|
||||
widget.spaceCheckBox.setChecked(self.settings.value("spaceCheckBox", False, type=bool))
|
||||
widget.spaceIntervalLineEdit.setText(self.settings.value("spaceIntervalLineEdit", "1000"))
|
||||
|
||||
mode_index = self.settings.value("modeIndex", 0, type=int)
|
||||
if 0 <= mode_index < widget.modeComboBox.count():
|
||||
widget.modeComboBox.setCurrentIndex(mode_index)
|
||||
|
||||
widget.attachThreadCheckBox.setChecked(self.settings.value("attachThread", True, type=bool))
|
||||
|
||||
for i in range(10):
|
||||
widget.keyCheckBoxes[i].setChecked(self.settings.value(f"keyCheckBox{i}", False, type=bool))
|
||||
combo_index = self.settings.value(f"keyCombo{i}", 0, type=int)
|
||||
if 0 <= combo_index < widget.keyCombos[i].count():
|
||||
widget.keyCombos[i].setCurrentIndex(combo_index)
|
||||
widget.intervalLineEdits[i].setText(self.settings.value(f"intervalLineEdit{i}", "1000"))
|
||||
|
||||
hotkey_index = self.settings.value("hotkeyIndex", 47, type=int) # Default to Home key
|
||||
if 0 <= hotkey_index < widget.hotkeyComboBox.count():
|
||||
widget.hotkeyComboBox.setCurrentIndex(hotkey_index)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
|
||||
class MOUSEINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dx", wintypes.LONG),
|
||||
("dy", wintypes.LONG),
|
||||
("mouseData", wintypes.DWORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class KEYBDINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wVk", wintypes.WORD),
|
||||
("wScan", wintypes.WORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class HARDWAREINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("uMsg", wintypes.DWORD),
|
||||
("wParamL", wintypes.WORD),
|
||||
("wParamH", wintypes.WORD),
|
||||
]
|
||||
|
||||
|
||||
class INPUT_union(ctypes.Union):
|
||||
_fields_ = [
|
||||
("mi", MOUSEINPUT),
|
||||
("ki", KEYBDINPUT),
|
||||
("hi", HARDWAREINPUT),
|
||||
]
|
||||
|
||||
|
||||
class INPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("type", wintypes.DWORD),
|
||||
("u", INPUT_union),
|
||||
]
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.4"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keypressremark"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "pyqt5" },
|
||||
{ name = "pyqt5-qt5" },
|
||||
{ name = "pywin32" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyinstaller" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "pyqt5", specifier = "==5.15.11" },
|
||||
{ name = "pyqt5-qt5", specifier = "==5.15.2" },
|
||||
{ name = "pywin32", specifier = "==311" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pyinstaller", specifier = ">=6.0.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2023.2.7"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.16.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyinstaller-hooks-contrib" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/94/1f62e95e4a28b64cfbb5b922ef3046f968b47170d37a1e1a029f56ac9cb4/pyinstaller-6.16.0.tar.gz", hash = "sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef", size = 4008473 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/0a/c42ce6e5d3de287f2e9432a074fb209f1fb72a86a72f3903849fdb5e4829/pyinstaller-6.16.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a", size = 1027899 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/d0/f18fedde32835d5a758f464c75924e2154065625f09d5456c3c303527654/pyinstaller-6.16.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0", size = 727990 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/db/c8bb47514ce857b24bf9294cf1ff74844b6a489fa0ab4ef6f923288c4e38/pyinstaller-6.16.0-py3-none-manylinux2014_i686.whl", hash = "sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058", size = 739238 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/3e/451dc784a8fcca0fe9f9b6b802d58555364a95b60f253613a2c83fc6b023/pyinstaller-6.16.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851", size = 737142 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/37/2f457479ef8fa2821cdb448acee2421dfb19fbe908bf5499d1930c164084/pyinstaller-6.16.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3", size = 734133 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/c4/0f7daac4d062a4d1ac2571d8a8b9b5d6812094fcd914d139af591ca5e1ba/pyinstaller-6.16.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0", size = 733817 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/e4/b6127265b42bef883e8873d850becadf748bc5652e5a7029b059328f3c31/pyinstaller-6.16.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454", size = 732912 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/00/c6663107bdf814b2916e71563beabd09f693c47712213bc228994cb2cc65/pyinstaller-6.16.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5", size = 732773 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/14/cabe9bc5f60b95d2e70e7d045ab94b0015ff8f6c8b16e2142d3597e30749/pyinstaller-6.16.0-py3-none-win32.whl", hash = "sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8", size = 1313878 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/99/2005efbc297e7813c1d6f18484aa94a1a81ce87b6a5b497c563681f4c4ea/pyinstaller-6.16.0-py3-none-win_amd64.whl", hash = "sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41", size = 1374706 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/f4/4dfcf69b86d60fcaae05a42bbff1616d48a91e71726e5ed795d773dae9b3/pyinstaller-6.16.0-py3-none-win_arm64.whl", hash = "sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64", size = 1315923 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2025.9"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/83/be0f57c0b77b66c33c2283ebd4ea341022b5a743e97c5fb3bebab82b38b9/pyinstaller_hooks_contrib-2025.9.tar.gz", hash = "sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6", size = 165189 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/26/23b4cfc77d7f808c69f59070e1e8293a579ec281a547c61562357160b346/pyinstaller_hooks_contrib-2025.9-py3-none-any.whl", hash = "sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038", size = 444283 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt5"
|
||||
version = "5.15.11"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "pyqt5-qt5" },
|
||||
{ name = "pyqt5-sip" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt5-qt5"
|
||||
version = "5.15.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/09/99a222b0360616250fb2e6003a54e43a2a06b0774f0f8d5daafb86a2c375/PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154", size = 40546019 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/d4/241a6a518d0bcf0a9fcdcbad5edfed18d43e884317eab8d5230a2b27e206/PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a", size = 59921716 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/7e/ce7c66a541a105fa98b41d6405fe84940564695e29fc7dccf6d9e8c5f898/PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327", size = 43447358 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/97/5d3b222b924fa2ed4c2488925155cd0b03fd5d09ee1cfcf7c553c11c9f66/PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962", size = 50075158 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt5-sip"
|
||||
version = "12.17.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/08/88a20c862f40b5c178c517cdc7e93767967dec5ac1b994e226d517991c9b/pyqt5_sip-12.17.1.tar.gz", hash = "sha256:0eab72bcb628f1926bf5b9ac51259d4fa18e8b2a81d199071135458f7d087ea8", size = 104136 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/e4/451e465c75584a7cbd10e10404317b7443af83f56a64e02080b1f3cda5b5/pyqt5_sip-12.17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5134d637efadd108a70306bab55b3d7feaa951bf6b8162161a67ae847bea9130", size = 122581 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/b2/330f97434b21fbc99ab16f6ce71358ff5ea1bf1f09ed14dfe6b28b5ed8f5/pyqt5_sip-12.17.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:155cf755266c8bf64428916e2ff720d5efa1aec003d4ccc40c003b147dbdac03", size = 276844 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/fd/53925099d0fc8aaf7adee613b6cebfb3fdfcd1238add64ff9edf6711e5f8/pyqt5_sip-12.17.1-cp311-cp311-win32.whl", hash = "sha256:9dfa7fe4ac93b60004430699c4bf56fef842a356d64dfea7cbc6d580d0427d6d", size = 49099 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/f8/f47a849c17676557c4220fbce9fcc24e15736af247c4dddcaf9ff0124b57/pyqt5_sip-12.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ddd214cf40119b86942a5da2da5a7345334955ab00026d8dcc56326b30e6d3c", size = 58988 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/15/291f83f336558300626bebb0c403084ec171bbc8a70683e3376234422eb6/pyqt5_sip-12.17.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c362606de782d2d46374a38523632786f145c517ee62de246a6069e5f2c5f336", size = 124521 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/85/ea1ae099260fd1859d71b31f51760b4226abfa778d5796b76d92c8fe6dcd/pyqt5_sip-12.17.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:140cc582151456103ebb149fefc678f3cae803e7720733db51212af5219cd45c", size = 282182 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/b3/d5b50c721651a0f2ccbef6f8db3dabf3db296b9ec239ba007f5615f57dd7/pyqt5_sip-12.17.1-cp312-cp312-win32.whl", hash = "sha256:9dc1f1525d4d42c080f6cfdfc70d78239f8f67b0a48ea0745497251d8d848b1d", size = 49447 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/b6/474d8b17763683ab45fb364f3a44f25fdc25d97b47b29ad8819b95a15ac8/pyqt5_sip-12.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:d5e2e9e175559017cd161d661e0ee0b551684f824bb90800c5a8c8a3bea9355e", size = 57946 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/58/9ecb688050e79ffe7bbd9fc917aa13f63856a5081ac46bbce87bb11ab971/pyqt5_sip-12.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9ebbd7769ccdaaa6295e9c872553b6cde17f38e171056f17300d8af9a14d1fc8", size = 124485 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/9f/ae691360a9f18e3e06fd297e854d7ad175367e35ea184fd2fcf6c79b8c25/pyqt5_sip-12.17.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b023da906a70af2cf5e6fc1932f441ede07530f3e164dd52c6c2bb5ab7c6f424", size = 281923 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/31/491c45423174a359a4b8a8d84a7b541c453f48497ae928cbe4006bcd3e01/pyqt5_sip-12.17.1-cp313-cp313-win32.whl", hash = "sha256:36dbef482bd638786b909f3bda65b7b3d5cbd6cbf16797496de38bae542da307", size = 49400 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/61/e28681dd5200094f7b2e6671e85c02a4d6693da36d23ad7d39ffbc70b15c/pyqt5_sip-12.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:d04e5551bbc3bcec98acc63b3b0618ddcbf31ff107349225b516fe7e7c0a7c8b", size = 57979 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/f9/06c09dc94474ffe3f518f80e47fc69d34abf8e4a971ae7e7c667d6ff30a7/pyqt5_sip-12.17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c49918287e1ad77956d1589f1d3d432a0be7630c646ea02cf652413a48e14458", size = 124400 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/ae/be6e338ea427deac5cd81a93f51ae3fb6505d99d6d5e5d5341bcc099327e/pyqt5_sip-12.17.1-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:944a4bf1e1ee18ad03a54964c1c6433fb6de582313a1f0b17673e7203e22fc83", size = 282291 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/a3/8b758518bd0dd5d1581f7a6d522c9b4d9b58d05087b1d0b4dfaad5376434/pyqt5_sip-12.17.1-cp314-cp314-win32.whl", hash = "sha256:99a2935fd662a67748625b1e6ffa0a2d1f2da068b9df6db04fa59a4a5d4ee613", size = 50578 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/8c/e96f9877548810b1e537f46fc21ba74552dd4e8c498658114a8353bdf659/pyqt5_sip-12.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:aaa33232cc80793d14fdb3b149b27eec0855612ed66aad480add5ac49b9cee63", size = 59763 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.9.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
|
||||
]
|
||||
Loading…
Reference in New Issue