项目重构

This commit is contained in:
v_liufushun 2025-10-22 17:26:03 +08:00
parent 9a76798602
commit b9c2a31a5a
27 changed files with 2714 additions and 4324 deletions

592
docs/DEVELOPER_GUIDE.md Normal file
View File

@ -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**: 更多技术文档和示例
欢迎贡献代码和文档!🎉

538
docs/ENVIRONMENT_SETUP.md Normal file
View File

@ -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` 输出)

127
docs/README.md Normal file
View File

@ -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
View File

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

3567
poetry.lock generated

File diff suppressed because one or more lines are too long

View File

@ -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
View File

@ -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) 或联系作者。

View File

@ -1,2 +1,3 @@
PyQt5==5.15.6
pywin32==306
python-qt5
PyQt5==5.15.11
pywin32==311

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

0
src/__init__.py Normal file
View File

0
src/core/__init__.py Normal file
View File

85
src/core/admin_check.py Normal file
View File

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

76
src/core/constants.py Normal file
View File

@ -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)
]

269
src/core/key_sender.py Normal file
View File

@ -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

View File

@ -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
src/gui/__init__.py Normal file
View File

29
src/gui/about_dialog.py Normal file
View File

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

View File

304
src/gui/main_window.py Normal file
View File

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

73
src/main.py Normal file
View File

@ -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
src/utils/__init__.py Normal file
View File

45
src/utils/settings.py Normal file
View File

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

46
src/utils/structures.py Normal file
View File

@ -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),
]

197
uv.lock Normal file
View File

@ -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 },
]