摘要
在日常 Python 开发中,我们经常会遇到这样的场景:电脑上散落着很多 Python 小工具、脚本项目、自动化程序,每次运行都需要进入目录、找到 .py 文件、打开命令行、手动执行命令。时间一长,项目越来越多,管理和启动都变得麻烦。
本文基于一个实际的小工具项目,详细分析如何使用:
wxPython构建桌面 GUIpathlib扫描本地 Python 项目目录.bat批处理文件实现一键启动sqlite3保存文件夹元数据- 剪贴板截图粘贴并以 BLOB 形式存储
unittest做基础功能回归测试
最终实现一个"Python 文件夹一键启动工具":选择一个根目录后,程序会自动列出包含 .py 文件的子文件夹,支持为指定 Python 文件生成启动 bat,运行 bat,预览 bat 内容,删除 bat,并为每个文件夹维护截图、备注和作废状态。
"C:\Users\86182\Desktop\程序管理\py_folder_launcher.py"

一、项目目标
这个工具主要解决几个实际问题:
-
快速发现 Python 项目
- 选择一个根目录
- 自动扫描其中包含
.py文件的子文件夹
-
为 Python 脚本生成一键启动 bat
- 选中
.py文件 - 点击按钮生成
xxx_一键启动.bat
- 选中
-
运行和管理 bat 文件
- 选中
.bat文件可以直接运行 - 可以预览 bat 文件内容
- 可以删除不再需要的 bat 文件
- 选中
-
记录项目元信息
- 使用 SQLite 保存文件夹编号、名称、截图、备注、是否作废等信息
-
支持截图粘贴
- 从剪贴板粘贴截图
- 在界面中显示截图缩略图
- 保存到 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",
)
这里有几个细节值得注意:
- 使用
Path而不是字符串路径,路径处理更清晰。 ensure_ascii=False可以正确保存中文路径。indent=2让config.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
它的逻辑是:
- 遍历根目录下的直接子文件夹
- 对每个子文件夹递归查找
.py文件 - 如果存在
.py文件,就加入结果列表 - 按文件夹名称排序
这里使用了:
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 文件自身所在目录。这样做有两个好处:
- 不依赖当前命令行所在目录
- 即使用户从其他位置双击 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。
核心逻辑是:
- SQLite 中读取 BLOB bytes
- 写入临时图片文件
- 使用
wx.Image(str(temp_path))读取 - 缩放为
240x140 - 设置到
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,不依赖额外测试框架。
测试覆盖了以下功能:
- 扫描包含
.py文件的文件夹 - 列出
.py和.bat文件 - 生成 bat 内容
- 创建一键启动 bat
- 保存和读取 config
- 文件夹是否已生成 bat 的显示逻辑
- SQLite 字段保存
- 图片字节格式识别
- bat 内容预览
- 删除 bat 文件
- 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
十七、可以继续优化的方向
这个工具已经具备比较完整的本地管理能力,但还可以继续扩展:
-
支持搜索文件夹
- 文件夹多时可以按名称过滤
-
支持直接编辑 bat 内容
- 当前只能预览,后续可以加编辑保存
-
截图支持双击放大
- 当前显示的是 240x140 缩略图
-
支持多 Python 解释器
- 例如为不同项目选择不同虚拟环境
-
支持隐藏作废项目
is_invalid字段已经存在,可以在列表中过滤
-
支持导出项目清单
- 导出为 Excel、CSV 或 Markdown
-
把单文件拆分成模块
- GUI、数据库、文件扫描、bat 管理可以拆成多个文件
十八、总结
本文分析了一个基于 wxPython + SQLite 的本地 Python 项目启动管理工具。
它的核心价值在于:
- 自动发现 Python 项目目录
- 为
.py文件生成一键启动 bat - 支持 bat 预览、运行和删除
- 使用 SQLite 保存项目元数据
- 支持截图粘贴和显示
- 通过 unittest 保障核心逻辑稳定
这个项目虽然不复杂,但非常适合作为 Python 桌面工具开发的实战案例。它覆盖了文件系统操作、GUI 编程、批处理生成、本地数据库、剪贴板图片处理和测试回归等多个常见知识点。
对于经常维护本地 Python 小工具的人来说,这类"自用效率工具"非常有价值:不追求复杂架构,但要解决真实问题;不追求炫酷界面,但要稳定、清晰、可维护。
推荐标签
text
Python
wxPython
SQLite
桌面应用
自动化工具
批处理
本地工具开发