在桌面工具开发中,一个常见需求是快速浏览本地图片,但又不希望引入复杂的Web框架或数据库。本文分享一个基于Python、wxPython和内置HTTP服务器的轻量方案:它扫描指定文件夹中的图片,自动生成包含Lightbox效果的HTML页面,并通过内嵌的HTTP服务提供访问,完全无需依赖外部前端工具或网络。
核心流程
程序运行时的处理链如下:
1. 从photo_browser_config.json读取配置(文件夹列表和当前索引)。
2. 扫描当前文件夹下所有支持的图片文件(.jpg、.png、.webp、.tiff等8种)。
3. 在该文件夹下生成showimage.html,每张图片呈现为卡片,点击后触发全屏Lightbox。
4. 启动一个绑定到127.0.0.1随机端口的ThreadingTCPServer,以该文件夹作为静态资源根目录。
5. 通过wxPython的wx.html2.WebView(Windows下即WebView2)加载http://127.0.0.1:<port>/showimage.html。
双模块镜像设计:稳定打包与测试隔离
项目中photo_browser_core.py和photo_browser_app.py的前几百行代码近乎一致——所有dataclass、scan_images、load_config、ensure_gallery_html等函数都出现了两次。这不是偶然重复,而是刻意为之。photo_browser_core.py作为“逻辑标本”,专门供单元测试导入,不依赖wx;photo_browser_app.py则内联相同逻辑并接上wx外壳,作为PyInstaller的唯一入口。测试文件test_packaging_imports.py中硬性规定:打包入口不得import photo_browser_core。目的是避免PyInstaller在分析hidden import时遗漏自定义模块,导致打包后运行时崩溃。用少量代码重复换来打包链路的确定性,是常见取舍。
配置模型:dataclass + JSON 的稳健异常处理
配置仅包含两个dataclass:- @dataclass
- class FolderEntry:
- label: str
- path: str
- template: str = ""
- @dataclass
- class AppConfig:
- folders: list[FolderEntry] = field(default_factory=list)
- current_index: int = 0
复制代码 load_config()对5种异常输入做了防御:配置文件不存在、JSON损坏、磁盘I/O失败、元素非dict或缺path、current_index越界。每次遇到异常都返回空AppConfig(),确保程序总能启动。保存时注意ensure_ascii=False使中文路径可读,indent=2方便手动排查。
路径定位:打包前后的正确目录
get_app_dir()函数只有8行,却解决了打包后的配置持久化问题:- def get_app_dir() -> Path:
- if getattr(sys, "frozen", False):
- return Path(sys.executable).resolve().parent
- return Path(__file__).resolve().parent
复制代码 sys.frozen是PyInstaller注入的属性,开发时返回源码目录,打包后返回exe所在目录。如果不加这个判断,打包后的配置会被写入临时目录(如%TEMP%\_MEIxxxx),程序关闭即丢失。任何需要写文件的位置都必须经过get_app_dir()。
图片扫描:幂等且不递归
只扫描当前文件夹下的文件(iterdir()),不递归,符合“每个文件夹是一个相册”的设计。排序用name.lower()确保b.PNG不排在a.jpg前面:- SUPPORTED_IMAGE_EXTENSIONS = {'.jpg','.jpeg','.png','.gif','.bmp','.webp','.tif','.tiff'}
- def scan_images(folder: Path | str) -> list[Path]:
- folder_path = Path(folder)
- if not folder_path.exists() or not folder_path.is_dir():
- return []
- images = [path for path in folder_path.iterdir() if path.is_file() and path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS]
- return sorted(images, key=lambda path: path.name.lower())
复制代码
生成HTML:版本检测与平滑升级
ensure_gallery_html()的语义是“确保存在且是最新版”。函数首先检查showimage.html是否存在,并用is_modern_gallery_html()检测是否包含版本标记(data-gallery-version="flex-lightbox-v2")以及所有必需关键字。如果通过检测则复用;否则重新生成。这带来两个好处:升级平滑(修改版本常量即可触发重写),且不覆盖用户手动改动(只要保留标记即视为最新)。测试特意验证了“有标记则复用”和“无标记则升级”两个路径。
URL编码与路径处理
生成卡片时,用urllib.parse.quote编码图片文件名(如“first photo.jpg”转为“first%20photo.jpg”),否则WebView加载会404。alt属性使用html.escape(..., quote=True)避免文件名中的特殊字符破坏HTML。同时使用relative.as_posix()将Windows反斜杠转为URL友好的正斜杠。
HTML模板系统:占位符与强制注入
用户可以为每个文件夹指定HTML模板,程序将生成的相册片段插入其中。占位符替换分三级:优先替换{{IMAGE_GALLERY}},若无则替换</body>之前,最后直接追加末尾。强制注入Lightbox样式和脚本时,通过检测特征字符串(如.photo-card:hover、id="lightbox")判断是否已存在,避免重复注入。这样用户可自定义标题、导航等,无需操心交互部分。
内嵌JavaScript:ES5兼容旧版WebView
代码中看不到let、const、箭头函数或classList.add,而是用var、function、className操作。因为wx.html2.WebView在Windows上默认后端可能是IE内核(wxWEBVIEW_BACKEND_IE),而非Chromium。测试甚至直接断言HTML中不含"=>"和"const "。CSS中也使用了-ms-flexbox等IE老旧前缀。教训:桌面WebView不能假设浏览器现代,兼容最好使用CanIUse中IE11能跑的子集。
为什么用HTTP服务而非file://协议?
直接使用file://会面临同源策略限制、中文路径解析失败、loading="lazy"支持不完整等问题。程序启动一个绑定127.0.0.1随机端口的ThreadingTCPServer,将文件夹作为静态站点根目录。注意事项:allow_reuse_address确保快速切换文件夹不报端口占用;daemon_threads和daemon=True保证主进程退出后线程自动终止;文件夹相同时短路不重启服务器。测试也验证了程序使用LoadURL而非setPage或as_uri。
总结
这个项目展示了一个“生成HTML + 内嵌HTTP + 内嵌浏览器”的桌面应用架构,特别适合需要快速展示本地资源、又不想依赖外部服务器的场景。其设计中的稳健异常处理、打包隔离技巧、版本探测与兼容性考量,都可以直接借鉴到其他Python桌面项目中。 |