KeyPress/docs/DEVELOPER_GUIDE.md

12 KiB
Raw Permalink Blame History

开发者指南

本文档为 KeyPressRemark 项目的开发者提供详细的技术指导和代码贡献指南。

📋 目录

🛠️ 开发环境设置

环境要求

  • 操作系统: Windows 10/11 (开发和测试)
  • Python: 3.11+ (推荐 3.11.x)
  • IDE: PyCharm / VSCode (推荐)
  • 版本控制: Git

开发环境配置

  1. 克隆项目

    git clone <repository-url>
    cd KeyPressRemark
    
  2. 创建虚拟环境

    # 使用 uv (推荐)
    uv venv --python 3.11
    .venv\Scripts\activate
    
    # 或使用 venv
    python -m venv .venv
    .venv\Scripts\activate
    
  3. 安装依赖

    # 开发依赖(包含测试工具)
    uv pip install -r requirements.txt
    uv add --dev pytest pytest-qt black isort flake8
    
  4. IDE 配置

    • 设置 Python 解释器为虚拟环境中的 Python
    • 配置代码格式化工具Black
    • 启用类型检查mypy

调试配置

VSCode launch.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 模式

# 异步发送,消息进入目标窗口队列
win32gui.PostMessage(hwnd, WM_KEYDOWN, key_code, lparam)
win32gui.PostMessage(hwnd, WM_KEYUP, key_code, lparam)

2. SendMessage 模式

# 同步发送,等待处理完成
win32gui.SendMessage(hwnd, WM_KEYDOWN, key_code, lparam)
win32gui.SendMessage(hwnd, WM_KEYUP, key_code, lparam)

3. SendInput 模式

# 系统级模拟,最高兼容性
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. 类型注解

    def send_key(self, key_code: int, mode: str = "post") -> bool:
        """发送按键"""
        pass
    

文档规范

  1. 模块文档

    """
    模块名 - 简短描述
    
    详细说明模块的功能、用途和技术原理。
    
    作者: xiao liu
    版本: v0.1.0
    """
    
  2. 函数文档

    def function_name(param1: type, param2: type) -> return_type:
        """
        函数简短描述
    
        详细说明函数的功能、算法或注意事项。
    
        Args:
            param1: 参数1说明
            param2: 参数2说明
    
        Returns:
            返回值说明
    
        Raises:
            Exception: 异常情况说明
        """
    
  3. 行内注释

    # 检查目标窗口是否有效
    if not self.target_hwnd or not win32gui.IsWindow(self.target_hwnd):
        return False
    

Git 提交规范

使用 Conventional Commits 规范:

<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. 创建分支

    git checkout -b feature/new-feature-name
    
  3. 开发实现

    • 编写核心逻辑
    • 添加单元测试
    • 更新文档
  4. 代码检查

    # 格式化代码
    black src/
    isort src/
    
    # 静态检查
    flake8 src/
    mypy src/
    
    # 运行测试
    pytest tests/
    
  5. 提交代码

    git add .
    git commit -m "feat: 添加新功能"
    git push origin feature/new-feature-name
    
  6. 创建 Pull Request

    • 填写详细的变更说明
    • 添加测试截图(如适用)
    • 等待代码审查

Bug 修复流程

  1. 复现问题

    • 记录复现步骤
    • 确认影响范围
    • 添加回归测试
  2. 定位问题

    • 使用调试器定位
    • 查看日志信息
    • 分析堆栈跟踪
  3. 修复实现

    • 最小化修改范围
    • 确保不引入新问题
    • 验证修复效果

🧪 测试指南

测试框架

项目使用 pytestpytest-qt 进行测试:

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
    • 模拟用户操作
    • 验证界面响应

运行测试

# 运行所有测试
pytest

# 运行特定测试文件
pytest tests/test_key_sender.py

# 运行特定测试函数
pytest tests/test_key_sender.py::test_post_message

# 生成覆盖率报告
pytest --cov=src --cov-report=html

🐛 调试技巧

日志记录

使用 Python 标准库 logging:

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. 检查窗口句柄

    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. 监控消息发送

    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 进行性能分析:

python -m cProfile -o profile.stats src/main.py

查看结果:

import pstats
p = pstats.Stats('profile.stats')
p.sort_stats('cumulative').print_stats(10)

性能优化

按键发送优化

  1. 批量发送

    def send_keys_batch(self, keys: List[int], mode: str = "post"):
        """批量发送按键,减少 API 调用次数"""
        for key_code in keys:
            self.send_key(key_code, mode)
    
  2. 缓存窗口信息

    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. 异步操作

    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. 减少重绘

    # 批量更新界面
    self.setUpdatesEnabled(False)
    # 进行多个界面操作...
    self.setUpdatesEnabled(True)
    

常见问题

Q1: 按键发送不生效

可能原因:

  1. 目标应用需要管理员权限
  2. 应用使用了低级键盘钩子
  3. 窗口句柄已失效

解决方案:

# 检查窗口是否有效
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: 界面冻结

原因: 在主线程执行耗时操作

解决方案:

# 使用 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. 事件监听器是否移除

解决方案:

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 版本问题:

# 强制安装特定版本
uv pip install PyQt5==5.15.11 pyqt5-qt5==5.15.2 --force-reinstall

权限问题:

# 以管理员身份运行命令提示符
# 或者使用 --user 参数
pip install --user PyQt5

📞 获取帮助

  • GitHub Issues: 报告 bug 和功能请求
  • 讨论区: 技术讨论和经验分享
  • Wiki: 更多技术文档和示例

欢迎贡献代码和文档!🎉