Python + wxPython + SQLite 实战:开发一个本地 Python 项目一键启动管理工具

摘要

在日常 Python 开发中,我们经常会遇到这样的场景:电脑上散落着很多 Python 小工具、脚本项目、自动化程序,每次运行都需要进入目录、找到 .py 文件、打开命令行、手动执行命令。时间一长,项目越来越多,管理和启动都变得麻烦。

本文基于一个实际的小工具项目,详细分析如何使用:

  • wxPython 构建桌面 GUI
  • pathlib 扫描本地 Python 项目目录
  • .bat 批处理文件实现一键启动
  • sqlite3 保存文件夹元数据
  • 剪贴板截图粘贴并以 BLOB 形式存储
  • unittest 做基础功能回归测试

最终实现一个"Python 文件夹一键启动工具":选择一个根目录后,程序会自动列出包含 .py 文件的子文件夹,支持为指定 Python 文件生成启动 bat,运行 bat,预览 bat 内容,删除 bat,并为每个文件夹维护截图、备注和作废状态。

"C:\Users\86182\Desktop\程序管理\py_folder_launcher.py"

一、项目目标

这个工具主要解决几个实际问题:

  1. 快速发现 Python 项目

    • 选择一个根目录
    • 自动扫描其中包含 .py 文件的子文件夹
  2. 为 Python 脚本生成一键启动 bat

    • 选中 .py 文件
    • 点击按钮生成 xxx_一键启动.bat
  3. 运行和管理 bat 文件

    • 选中 .bat 文件可以直接运行
    • 可以预览 bat 文件内容
    • 可以删除不再需要的 bat 文件
  4. 记录项目元信息

    • 使用 SQLite 保存文件夹编号、名称、截图、备注、是否作废等信息
  5. 支持截图粘贴

    • 从剪贴板粘贴截图
    • 在界面中显示截图缩略图
    • 保存到 SQLite BLOB 字段中

二、整体架构设计

当前项目是一个单文件桌面应用,核心文件是:

text 复制代码
py_folder_launcher.py

整体结构可以分为三层:

text 复制代码
┌────────────────────────────┐
│        wxPython GUI         │
│  文件夹列表 / 文件列表 / 表单 │
└─────────────┬──────────────┘
              │
┌─────────────▼──────────────┐
│       业务逻辑函数层         │
│ 扫描目录 / 生成bat / 读写截图 │
└─────────────┬──────────────┘
              │
┌─────────────▼──────────────┐
│      本地持久化层            │
│ config.json + SQLite DB     │
└────────────────────────────┘

其中:

  • config.json 用于记住上次选择的根目录
  • launcher_data.db 用于保存文件夹元数据
  • .bat 文件生成在目标 Python 文件所在目录

源码中的两个全局路径定义在 py_folder_launcher.py:15-16

python 复制代码
CONFIG_FILE = Path(__file__).with_name("config.json")
DATABASE_FILE = Path(__file__).with_name("launcher_data.db")

这种设计的优点是部署简单:脚本、配置文件、数据库都放在同一目录,适合个人本地工具。

三、记住上次选择的文件夹

为了避免每次启动程序都重新选择根目录,程序使用 config.json 保存上次选择的路径。

相关代码位于 py_folder_launcher.py:19-33

python 复制代码
def load_config(config_file: Path = CONFIG_FILE) -> Path | None:
    config_file = Path(config_file)
    if not config_file.exists():
        return None
    data = json.loads(config_file.read_text(encoding="utf-8"))
    selected_folder = data.get("selected_folder")
    return Path(selected_folder) if selected_folder else None


def save_config(config_file: Path, selected_folder: Path) -> None:
    config_file = Path(config_file)
    config_file.write_text(
        json.dumps({"selected_folder": str(Path(selected_folder))}, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

这里有几个细节值得注意:

  1. 使用 Path 而不是字符串路径,路径处理更清晰。
  2. ensure_ascii=False 可以正确保存中文路径。
  3. indent=2config.json 更易读。

程序启动时会调用 load_saved_folder(),自动加载上次保存的目录,见 py_folder_launcher.py:258-264

四、扫描包含 Python 文件的文件夹

工具左侧列表展示的是"包含 .py 文件的文件夹"。

核心函数在 py_folder_launcher.py:105-111

python 复制代码
def find_python_project_dirs(root: Path) -> list[Path]:
    root = Path(root)
    result = []
    for child in sorted((path for path in root.iterdir() if path.is_dir()), key=lambda path: path.name.lower()):
        if any(path.is_file() and path.suffix.lower() == ".py" for path in child.rglob("*.py")):
            result.append(child)
    return result

它的逻辑是:

  1. 遍历根目录下的直接子文件夹
  2. 对每个子文件夹递归查找 .py 文件
  3. 如果存在 .py 文件,就加入结果列表
  4. 按文件夹名称排序

这里使用了:

python 复制代码
child.rglob("*.py")

这意味着即使 .py 文件在子目录的更深层级,也能识别该文件夹是一个 Python 项目目录。

五、右侧文件列表:只显示 py 和 bat

选中左侧某个文件夹后,右侧会列出该文件夹下的 .py.bat 文件。

相关函数在 py_folder_launcher.py:114-119

python 复制代码
def list_launchable_files(folder: Path) -> list[Path]:
    folder = Path(folder)
    return sorted(
        (path for path in folder.iterdir() if path.is_file() and path.suffix.lower() in {".py", ".bat"}),
        key=lambda path: path.name.lower(),
    )

注意这里不是递归扫描,而是只显示当前文件夹下的文件。这种设计更符合"启动入口文件"的使用习惯,避免右侧列表过于复杂。

六、生成一键启动 bat 文件

生成 bat 文件是这个工具的核心功能之一。

代码位于 py_folder_launcher.py:122-141

python 复制代码
def build_bat_content(py_file: Path) -> str:
    py_file = Path(py_file)
    return (
        "@echo off\r\n"
        "cd /d \"%~dp0\"\r\n"
        f"if not exist \"{py_file.name}\" (\r\n"
        f"    echo 找不到要启动的文件:{py_file.name}\r\n"
        "    pause\r\n"
        "    exit /b 1\r\n"
        ")\r\n"
        f"python \"{py_file.name}\"\r\n"
        "pause\r\n"
    )


def create_launcher_bat(py_file: Path) -> Path:
    py_file = Path(py_file)
    bat_file = py_file.with_name(f"{py_file.stem}_一键启动.bat")
    bat_file.write_text(build_bat_content(py_file), encoding="utf-8")
    return bat_file

生成的 bat 内容大致如下:

bat 复制代码
@echo off
cd /d "%~dp0"
if not exist "main.py" (
    echo 找不到要启动的文件:main.py
    pause
    exit /b 1
)
python "main.py"
pause

这里最关键的是:

bat 复制代码
cd /d "%~dp0"

%~dp0 表示 bat 文件自身所在目录。这样做有两个好处:

  1. 不依赖当前命令行所在目录
  2. 即使用户从其他位置双击 bat,也能正确进入脚本所在目录

另外,代码中加入了文件存在性判断:

bat 复制代码
if not exist "main.py" (...)

这可以避免出现模糊的错误信息,而是直接提示用户哪个 Python 文件不存在。

七、左侧文件夹标记:是否已生成 bat

左侧列表会显示哪些文件夹已经生成过一键启动 bat。

相关函数在 py_folder_launcher.py:144-148

python 复制代码
def format_folder_label(folder: Path) -> str:
    folder = Path(folder)
    if any(folder.glob("*_一键启动.bat")):
        return f"[已生成] {folder.name}"
    return folder.name

设计逻辑:

  • 如果文件夹中存在 *_一键启动.bat,显示:
text 复制代码
[已生成] 项目A
  • 如果没有生成 bat,只显示普通文件夹名称:
text 复制代码
项目B

这样做比在右侧文件区域显示 [已生成] 更合理,因为右侧区域负责展示文件,左侧区域负责展示项目状态。

八、运行、预览和删除 bat

1. 运行 bat

运行 bat 的代码在 py_folder_launcher.py:162-163

python 复制代码
def run_bat_file(bat_file: Path) -> None:
    subprocess.Popen(["cmd", "/c", "start", "", str(Path(bat_file))], shell=False)

这里使用:

python 复制代码
cmd /c start

可以让 bat 在新的命令行窗口中运行。

2. 预览 bat 内容

预览函数在 py_folder_launcher.py:151-152

python 复制代码
def read_bat_preview(bat_file: Path) -> str:
    return Path(bat_file).read_text(encoding="utf-8")

界面中选中 .bat 文件时,会把内容显示到只读文本框中,见 py_folder_launcher.py:407-413

3. 删除 bat

删除函数在 py_folder_launcher.py:155-159

python 复制代码
def delete_bat_file(bat_file: Path) -> None:
    bat_file = Path(bat_file)
    if bat_file.suffix.lower() != ".bat":
        raise ValueError("只能删除 bat 文件")
    bat_file.unlink()

这里做了一个非常重要的保护:只允许删除 .bat 文件,避免误删 .py 或其他文件。

九、SQLite 保存文件夹元数据

为了给每个项目文件夹保存更多信息,程序使用 SQLite 数据库 launcher_data.db

建表代码在 py_folder_launcher.py:36-53

python 复制代码
def ensure_database(db_path: Path = DATABASE_FILE) -> None:
    conn = sqlite3.connect(db_path)
    try:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS folder_records (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                folder_path TEXT NOT NULL UNIQUE,
                folder_name TEXT NOT NULL,
                screenshot BLOB,
                notes TEXT NOT NULL DEFAULT '',
                is_invalid INTEGER NOT NULL DEFAULT 0
            )
            """
        )
        conn.commit()
    finally:
        conn.close()

表结构如下:

字段 类型 说明
id INTEGER 自增编号
folder_path TEXT 文件夹完整路径,唯一
folder_name TEXT 文件夹名称
screenshot BLOB 界面截图二进制
notes TEXT 备注信息
is_invalid INTEGER 是否作废

为什么连接要显式关闭?

代码中没有使用简单的:

python 复制代码
with sqlite3.connect(db_path) as conn:
    ...

而是手动:

python 复制代码
conn = sqlite3.connect(db_path)
try:
    ...
finally:
    conn.close()

这是因为在 Python 的 sqlite3 中,with sqlite3.connect(...) as conn 只负责提交或回滚事务,并不会自动关闭连接。在 Windows 下,如果连接未关闭,可能导致数据库文件被锁住。

十、获取或创建文件夹记录

当用户点击左侧文件夹时,程序会自动创建或读取该文件夹对应的数据库记录。

代码在 py_folder_launcher.py:67-83

python 复制代码
def get_or_create_folder_record(db_path: Path, folder: Path) -> dict:
    folder = Path(folder)
    ensure_database(db_path)
    conn = sqlite3.connect(db_path)
    try:
        conn.row_factory = sqlite3.Row
        row = conn.execute("SELECT * FROM folder_records WHERE folder_path = ?", (str(folder),)).fetchone()
        if row is None:
            cursor = conn.execute(
                "INSERT INTO folder_records (folder_path, folder_name) VALUES (?, ?)",
                (str(folder), folder.name),
            )
            conn.commit()
            row = conn.execute("SELECT * FROM folder_records WHERE id = ?", (cursor.lastrowid,)).fetchone()
        return row_to_record(row)
    finally:
        conn.close()

这里使用了参数化 SQL:

python 复制代码
WHERE folder_path = ?

而不是字符串拼接,这可以避免 SQL 注入问题,也能正确处理路径中的特殊字符。

十一、保存截图、备注和作废状态

更新数据库记录的函数在 py_folder_launcher.py:86-96

python 复制代码
def update_folder_record(db_path: Path, record_id: int, screenshot: bytes | None, notes: str, is_invalid: bool) -> None:
    ensure_database(db_path)
    conn = sqlite3.connect(db_path)
    try:
        conn.execute(
            "UPDATE folder_records SET screenshot = ?, notes = ?, is_invalid = ? WHERE id = ?",
            (screenshot, notes, int(is_invalid), record_id),
        )
        conn.commit()
    finally:
        conn.close()

界面上这三个字段是可编辑的:

  • 截图
  • 备注信息
  • 是否作废

保存按钮对应逻辑在 py_folder_launcher.py:391-405

十二、截图粘贴与显示

1. 图片格式识别

程序支持 PNG、JPEG、BMP 三类常见截图格式。

判断函数在 py_folder_launcher.py:99-102

python 复制代码
def is_supported_image_bytes(data: bytes | None) -> bool:
    if not data:
        return False
    return data.startswith(b"\x89PNG\r\n\x1a\n") or data.startswith(b"\xff\xd8\xff") or data.startswith(b"BM")

这里通过文件头魔数判断图片格式:

格式 文件头
PNG \x89PNG\r\n\x1a\n
JPEG \xff\xd8\xff
BMP BM

2. 从剪贴板粘贴截图

粘贴截图逻辑在 py_folder_launcher.py:358-382

这里有一个兼容性处理很关键。当前 wxPython 4.2.4 中没有:

python 复制代码
wx.MemoryOutputStream
wx.MemoryInputStream

因此程序采用"临时 PNG 文件中转"的方式:

python 复制代码
image.SaveFile(str(temp_path), wx.BITMAP_TYPE_PNG)
self.current_screenshot = temp_path.read_bytes()

这种方案虽然不是最优雅,但兼容性好,适合本地工具。

3. 显示截图缩略图

显示截图代码在 py_folder_launcher.py:329-344

核心逻辑是:

  1. SQLite 中读取 BLOB bytes
  2. 写入临时图片文件
  3. 使用 wx.Image(str(temp_path)) 读取
  4. 缩放为 240x140
  5. 设置到 wx.StaticBitmap

十三、wxPython 界面结构

主窗口类是 LauncherFrame,定义在 py_folder_launcher.py:167

界面主要分为三块:

text 复制代码
顶部:路径显示 + 选择文件夹按钮

左侧:包含 py 文件的文件夹列表

右侧:
  - py / bat 文件列表
  - bat 内容预览
  - 文件夹信息编辑区
      - 编号
      - 文件夹名称
      - 截图显示
      - 选择截图 / 粘贴截图 / 清空截图
      - 备注信息
      - 是否作废
      - 保存信息

使用的是 wxPython 中最常见的布局方式:

python 复制代码
wx.BoxSizer(wx.VERTICAL)
wx.BoxSizer(wx.HORIZONTAL)
wx.StaticBoxSizer(wx.VERTICAL, panel, "文件夹信息")

这类布局虽然没有前端 CSS 那么灵活,但对于桌面小工具足够实用。

十四、测试设计

项目配套了 test_py_launcher_helpers.py,使用 Python 标准库 unittest,不依赖额外测试框架。

测试覆盖了以下功能:

  1. 扫描包含 .py 文件的文件夹
  2. 列出 .py.bat 文件
  3. 生成 bat 内容
  4. 创建一键启动 bat
  5. 保存和读取 config
  6. 文件夹是否已生成 bat 的显示逻辑
  7. SQLite 字段保存
  8. 图片字节格式识别
  9. bat 内容预览
  10. 删除 bat 文件
  11. wxPython 内存流 API 兼容性回归

例如,SQLite 记录保存测试位于 test_py_launcher_helpers.py:107-123

python 复制代码
def test_sqlite_folder_record_saves_editable_fields(self):
    with tempfile.TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "launcher_data.db"
        folder = Path(temp_dir) / "project_alpha"
        screenshot = b"fake image bytes"
        folder.mkdir()

        ensure_database(db_path)
        record = get_or_create_folder_record(db_path, folder)
        update_folder_record(db_path, record["id"], screenshot, "hello", True)
        result = get_or_create_folder_record(db_path, folder)

        self.assertEqual(record["folder_name"], "project_alpha")
        self.assertEqual(record["folder_path"], str(folder))
        self.assertEqual(result["screenshot"], screenshot)
        self.assertEqual(result["notes"], "hello")
        self.assertTrue(result["is_invalid"])

wxPython 兼容性回归测试位于 test_py_launcher_helpers.py:141-145

python 复制代码
def test_wx_stream_compatibility_does_not_use_unavailable_memory_streams(self):
    source = Path(py_folder_launcher.__file__).read_text(encoding="utf-8")

    self.assertNotIn("wx.MemoryOutputStream", source)
    self.assertNotIn("wx.MemoryInputStream", source)

这个测试是从实际问题中提炼出来的:当前 wxPython 版本没有这两个 API,因此通过测试防止后续代码再次引入不兼容写法。

十五、几个关键踩坑点

1. bat 启动目录问题

如果 bat 中直接写死绝对路径,文件移动后容易失效。

更稳妥的方式是:

bat 复制代码
cd /d "%~dp0"

这样 bat 永远从自身所在目录启动。

2. sqlite 连接不会被 with 自动关闭

很多人误以为:

python 复制代码
with sqlite3.connect(db_path) as conn:
    ...

会自动关闭连接。

实际上它只处理事务提交和回滚,不负责关闭连接。Windows 下如果连接没关闭,可能导致数据库文件被锁住。

3. wxPython 版本 API 差异

当前环境中的 wxPython 版本是 4.2.4,不存在:

python 复制代码
wx.MemoryOutputStream
wx.MemoryInputStream

所以图片 bytes 和 wx.Image 之间的转换采用临时文件中转方式。

4. 删除操作必须限制文件类型

删除 bat 文件时,代码明确限制:

python 复制代码
if bat_file.suffix.lower() != ".bat":
    raise ValueError("只能删除 bat 文件")

这是一个简单但必要的安全保护。

十六、如何运行

安装依赖:

bash 复制代码
python -m pip install wxPython

运行程序:

bash 复制代码
python py_folder_launcher.py

运行测试:

bash 复制代码
python -m unittest test_py_launcher_helpers.py -v

编译检查:

bash 复制代码
python -m py_compile py_folder_launcher.py

十七、可以继续优化的方向

这个工具已经具备比较完整的本地管理能力,但还可以继续扩展:

  1. 支持搜索文件夹

    • 文件夹多时可以按名称过滤
  2. 支持直接编辑 bat 内容

    • 当前只能预览,后续可以加编辑保存
  3. 截图支持双击放大

    • 当前显示的是 240x140 缩略图
  4. 支持多 Python 解释器

    • 例如为不同项目选择不同虚拟环境
  5. 支持隐藏作废项目

    • is_invalid 字段已经存在,可以在列表中过滤
  6. 支持导出项目清单

    • 导出为 Excel、CSV 或 Markdown
  7. 把单文件拆分成模块

    • GUI、数据库、文件扫描、bat 管理可以拆成多个文件

十八、总结

本文分析了一个基于 wxPython + SQLite 的本地 Python 项目启动管理工具。

它的核心价值在于:

  • 自动发现 Python 项目目录
  • .py 文件生成一键启动 bat
  • 支持 bat 预览、运行和删除
  • 使用 SQLite 保存项目元数据
  • 支持截图粘贴和显示
  • 通过 unittest 保障核心逻辑稳定

这个项目虽然不复杂,但非常适合作为 Python 桌面工具开发的实战案例。它覆盖了文件系统操作、GUI 编程、批处理生成、本地数据库、剪贴板图片处理和测试回归等多个常见知识点。

对于经常维护本地 Python 小工具的人来说,这类"自用效率工具"非常有价值:不追求复杂架构,但要解决真实问题;不追求炫酷界面,但要稳定、清晰、可维护。

推荐标签

text 复制代码
Python
wxPython
SQLite
桌面应用
自动化工具
批处理
本地工具开发
相关推荐
星越华夏1 小时前
python中四种获取文件后缀名的方法
开发语言·python
lunzi_08261 小时前
【学习笔记】《Python编程 从入门到实践》第9章:类、继承、组合与面向对象编程
笔记·python·学习
大蚂蚁2号1 小时前
本地批量音视频转文本免费工具
python·音视频·开源软件
copyer_xyf1 小时前
FastAPI 项目骨架搭建
前端·后端·python
十正1 小时前
aiohttp.TCPConnector 连接池原理详解
网络·python·tcp·aiohttp
LoserChaser2 小时前
Flask 文件上传服务器 - 知识点总结
服务器·python·flask
cd988802 小时前
2026年,哪家电销机器人定制更灵活?
python
二十七剑2 小时前
LangGraph 源码深度解析:_branch.py 条件分支底层实现原理
python
KaMeidebaby2 小时前
卡梅德生物技术快报|噬菌体展示文库构建全流程解析 | 大豆球蛋白纳米抗体筛选实践
人工智能·python·tcp/ip·算法·机器学习