admin管理员组文章数量:1437225
PyQt 多标签批量图片查看器开发纪实
前言
一直以来,我都想做一个轻量的图片查看工具,不要花哨,不要臃肿,最好还能支持多标签浏览,就像浏览器一样,一边看一边切换不同文件夹的图片。而且,我希望界面能漂亮一点,使用也足够流畅,于是便有了这次基于 PyQt 的开发尝试。
这篇文章将带你完整走一遍开发过程,包括构思、架构设计、编码、调试、完善,期间遇到的坑也会细细道来。
希望这次分享,能帮到同样想做 PyQt 项目 或者对多标签设计感兴趣的朋友。
一、起点:确定需求
我拿起笔(其实是打开了 Typora),列了几条最基本的需求:
- 支持选择文件夹,批量读取其中的图片
- 每个文件夹,开一个独立标签页,互不干扰
- 支持图片缩放、平移(鼠标操作)
- 图片浏览支持前后切换
- 界面要尽量清爽简洁
思考片刻之后,我又加了些想要实现的细节:
- 界面左侧有文件树,方便快速切换目录
- 支持拖拽打开文件夹
- 未来如果有精力,再加点 快捷键操作,比如翻页、旋转之类的
到这一步,一个大致的产品轮廓已经清晰了。接下来,就是决定怎么实现。
二、选型:为什么用 PyQt?
我有不少选项,比如 Tkinter(太丑了)、PySide(跟 PyQt 类似)、甚至 Electron(性能太差)。
权衡一圈之后,还是决定用老朋友 PyQt5。
原因很简单:
- 成熟稳定,文档资料多
- 自带丰富的控件(QTabWidget、QGraphicsView 都很好用)
- 可以自定义界面样式,做出不那么“老气”的界面
- 最重要的,我用得比较顺手
有了技术选型,接下来就可以进入正式的设计阶段了。
三、架构设计
为了防止后面代码越来越乱,我决定一开始就画个简单的模块流程图。
这张图概括了用户操作和程序内部模块的基本交互。
总结一下主要模块:
MainWindow
:主窗口,负责管理整个应用FolderTreeView
:左侧目录树TabWidget
:中间的标签页管理ImageViewer
:单个标签页内的图片浏览器
设计上,我打算让每一个标签页包含一个自己的 ImageViewer
,互不影响。
四、准备开发环境
很简单,确保本地环境装好:
代码语言:bash复制pip install PyQt5
pip install PyQt5-tools
然后,我在项目根目录新建了以下结构:
代码语言:python代码运行次数:0运行复制image_viewer/
├── main.py
├── main_window.py
├── folder_tree.py
├── tab_manager.py
├── image_viewer.py
├── utils.py
模块化组织,后期维护也会更轻松。
五、动手开干:从 MainWindow 开始
主窗口是整个程序的“大脑”,所以我先搭了一个最简单的骨架。
在 main_window.py
中写下初版:
from PyQt5.QtWidgets import QMainWindow, QSplitter, QWidget, QVBoxLayout
from folder_tree import FolderTreeView
from tab_manager import TabManager
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("多标签图片查看器")
self.resize(1200, 800)
self.init_ui()
def init_ui(self):
splitter = QSplitter()
# 左侧文件夹树
self.folder_tree = FolderTreeView()
splitter.addWidget(self.folder_tree)
# 右侧标签管理
self.tab_manager = TabManager()
splitter.addWidget(self.tab_manager)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 4)
container = QWidget()
layout = QVBoxLayout(container)
layout.addWidget(splitter)
self.setCentralWidget(container)
解释一下思路:
QSplitter
分隔左右两块FolderTreeView
是自定义控件,负责目录浏览TabManager
管理中间的多标签页- 最外层用一个
QVBoxLayout
包起来(方便以后扩展)
有了这个骨架,运行起来,至少能看到一个分栏的界面了。
六、目录树 FolderTreeView
接着,我来写左边的文件夹树。
文件夹浏览,PyQt 里有现成的控件:QTreeView
+ QFileSystemModel
。
所以 folder_tree.py
里是这样:
from PyQt5.QtWidgets import QTreeView
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QFileSystemModel, pyqtSignal
class FolderTreeView(QTreeView):
folder_selected = pyqtSignal(str)
def __init__(self):
super().__init__()
self.model = QFileSystemModel()
self.model.setRootPath("")
self.model.setFilter(self.model.filter() | self.model.Dirs)
self.setModel(self.model)
self.setHeaderHidden(True)
self.doubleClicked.connect(self.on_double_click)
def on_double_click(self, index):
path = self.model.filePath(index)
self.folder_selected.emit(path)
这里我做了个小设计:
- 双击文件夹,就发射一个信号,告诉外部:"用户选了这个目录!"
- 用
pyqtSignal(str)
定义自定义信号,方便外部绑定槽函数
这样,MainWindow
只需要监听这个 folder_selected
信号,就知道用户想打开哪个文件夹了。
七、标签页管理 TabManager
中间的多标签怎么做?
PyQt 自带 QTabWidget
,直接继承就行。
tab_manager.py
:
from PyQt5.QtWidgets import QTabWidget
from image_viewer import ImageViewer
class TabManager(QTabWidget):
def __init__(self):
super().__init__()
self.setTabsClosable(True)
self.tabCloseRequested.connect(self.close_tab)
def open_folder(self, folder_path):
viewer = ImageViewer(folder_path)
index = self.addTab(viewer, folder_path.split("/")[-1])
self.setCurrentIndex(index)
def close_tab(self, index):
widget = self.widget(index)
if widget:
widget.deleteLater()
self.removeTab(index)
这里有两个重要函数:
open_folder(folder_path)
:打开一个新文件夹,创建一个新的ImageViewer
close_tab(index)
:关闭并销毁对应的 tab
简单又清晰。
八、核心组件:ImageViewer
最关键的地方来了——图片浏览器。
一开始我以为可以用 QLabel
+ QPixmap
,结果发现想支持缩放和平移太麻烦了。
于是,我改用更灵活的 QGraphicsView
+ QGraphicsScene
。
在 image_viewer.py
里,我这样搭建:
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt
import os
class ImageViewer(QGraphicsView):
def __init__(self, folder_path):
super().__init__()
self.folder_path = folder_path
self.image_list = self.load_images(folder_path)
self.current_index = 0
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.load_image()
def load_images(self, folder_path):
exts = ['.png', '.jpg', '.jpeg', '.bmp', '.gif']
return [os.path.join(folder_path, f) for f in os.listdir(folder_path) if os.path.splitext(f)[-1].lower() in exts]
def load_image(self):
self.scene.clear()
if not self.image_list:
return
pixmap = QPixmap(self.image_list[self.current_index])
item = QGraphicsPixmapItem(pixmap)
self.scene.addItem(item)
self.fitInView(item, Qt.KeepAspectRatio)
def wheelEvent(self, event):
if event.angleDelta().y() > 0:
self.scale(1.25, 1.25)
else:
self.scale(0.8, 0.8)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Right:
self.next_image()
elif event.key() == Qt.Key_Left:
self.prev_image()
def next_image(self):
if self.image_list:
self.current_index = (self.current_index + 1) % len(self.image_list)
self.load_image()
def prev_image(self):
if self.image_list:
self.current_index = (self.current_index - 1) % len(self.image_list)
self.load_image()
功能一应俱全:
- 滚轮缩放(放大/缩小)
- 鼠标拖拽移动
- 左右方向键切换图片
特别要注意的是:
fitInView
方法可以让图片自适应窗口大小,初次加载很自然。
到这,基本的浏览功能已经成型了!
九、美化与主题定制
开发到这里,功能虽然完整,但 UI 看起来还是挺“原生”的 PyQt 风格:灰灰的、按键样式也不是特别协调。作为一个“追求颜值”的程序员,当然要给它来点主题皮肤。说干就干。
1. Qt StyleSheet (QSS) 入门
Qt 的皮肤其实就是一段类 CSS 语法,叫做 QSS。它几乎可以作用到所有 QWidget 派生的控件上。
我先在项目根目录里新增一个 style.qss
,写上一点简单样式:
/* style.qss */
QMainWindow {
background-color: #f0f0f0;
}
QPushButton {
background-color: #4CAF50;
color: white;
padding: 5px 12px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #45a049;
}
QTabWidget::pane {
border: 1px solid #aaa;
background: white;
border-radius: 4px;
}
QTabBar::tab {
background: #ddd;
padding: 6px;
margin-right: 2px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background: white;
font-weight: bold;
}
接着,在 main.py
中加载这段样式:
import sys
from PyQt5.QtWidgets import QApplication
from main_window import MainWindow
def main():
app = QApplication(sys.argv)
# 加载样式表
with open("style.qss", "r") as f:
app.setStyleSheet(f.read())
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
这样,重启程序后,你就会发现:
- 主窗口背景变成了浅灰色
- 按钮统一成了绿色风格
- 标签页有了圆角、选中时加粗
小结:QSS 为 PyQt 带来了极大的定制自由度,而且上手快。只需一份样式表,就能全面提升界面质感。
2. 自定义图标与资源管理
光靠颜色还不够,我还想给按钮、菜单加上图标。
我一般的做法是把所有静态资源(图标、logo 等)放到 resources/
文件夹里,然后写一个 resources.qrc
文件,之后用 pyrcc5
编译成 Python 模块。这样在代码里就可以通过 :/icons/open.png
的方式直接引用,完全不用管相对路径。
resources.qrc
样例:
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/icons">
<file>open.png</file>
<file>close.png</file>
<file>next.png</file>
<file>prev.png</file>
</qresource>
</RCC>
命令行:
代码语言:bash复制pyrcc5 resources.qrc -o resources_rc.py
然后在代码最前面导入:
代码语言:python代码运行次数:0运行复制import resources_rc # 让 Qt 知道我们的资源
在创建 QAction
或 QPushButton
时,就可以这样用:
open_action = QAction(QIcon(":/icons/open.png"), "打开文件夹", self)
图标就会自动嵌入到可执行文件里,用户体验更好。
十、异常处理与容错
在持续迭代过程中,我发现各种“奇葩用法”会导致程序崩溃。比如:
- 用户选了空文件夹
- 文件夹里有非图片文件
- 图片文件损坏,无法加载
为了让程序更“稳”,我给关键逻辑都包了一层异常处理。
1. 读取图片列表的健壮化
原先的 load_images
直接遍历 os.listdir
,碰到非图片直接跳过,但如果文件名带中文、特殊字符,或者文件夹本身没有访问权限,就会抛异常。于是,我改成:
import imghdr
def load_images(self, folder_path):
imgs = []
for filename in os.listdir(folder_path):
fullpath = os.path.join(folder_path, filename)
if not os.path.isfile(fullpath):
continue
try:
# 用 imghdr 确认文件确实是图片
if imghdr.what(fullpath):
imgs.append(fullpath)
except Exception as e:
print(f"Warning: 跳过无法识别的文件 {fullpath},原因:{e}")
return sorted(imgs)
这样一来:
- 非文件项(如子文件夹)被自动过滤
- imghdr 可以判断常见的图片格式
- 对于权限问题或文件损坏,catch 后打印 Warning,不影响整体流程
2. 加载单张图片时的容错
图片可能损坏,或某些特殊格式无法用 QPixmap 打开,我在 load_image
加了一层:
def load_image(self):
self.scene.clear()
if not self.image_list:
self._show_placeholder("该文件夹中无图片")
return
path = self.image_list[self.current_index]
try:
pixmap = QPixmap(path)
if pixmap.isNull():
raise ValueError("QPixmap 无法加载图片")
item = QGraphicsPixmapItem(pixmap)
self.scene.addItem(item)
self.fitInView(item, Qt.KeepAspectRatio)
except Exception as e:
print(f"Error: 加载图片失败 {path},原因:{e}")
self._show_placeholder("无法显示此图片")
并实现 _show_placeholder
,在场景中央绘制一行文本,提示用户:
from PyQt5.QtWidgets import QGraphicsTextItem
from PyQt5.QtGui import QFont
def _show_placeholder(self, text):
self.scene.clear()
placeholder = QGraphicsTextItem(text)
placeholder.setDefaultTextColor(Qt.gray)
placeholder.setFont(QFont("Arial", 16))
self.scene.addItem(placeholder)
# 居中
rect = self.scene.sceneRect()
placeholder.setPos(rect.width()/2 - placeholder.boundingRect().width()/2,
rect.height()/2 - placeholder.boundingRect().height()/2)
用户体验显著提升:就算图片加载失败,也不会看到空白或崩溃,而是优雅的提示。
十一、拖拽打开文件夹
用户往往习惯“拖拖拖”,所以我想让主窗口支持把文件夹拖进来直接打开。
拖拽在 Qt 中其实分两步:
- 接受拖拽:
dragEnterEvent
- 处理放下:
dropEvent
我给 MainWindow
加上这两个方法:
from PyQt5.QtCore import Qt, QUrl
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 允许拖放
self.setAcceptDrops(True)
...
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
# 只要是文件夹就给过
urls = event.mimeData().urls()
if any(url.isLocalFile() for url in urls):
event.acceptProposedAction()
def dropEvent(self, event):
for url in event.mimeData().urls():
path = url.toLocalFile()
if os.path.isdir(path):
# 传给 TabManager
self.tab_manager.open_folder(path)
event.acceptProposedAction()
拖一堆文件夹进来,就会批量打开,每个文件夹一个标签,酷毙了。
十二、快捷键与右键菜单
到这一步,虽然能用鼠标完成绝大多数操作,但对于“键盘党”来说,还真有些不爽。于是我决定再加点快捷键和右键菜单。
1. 全局快捷键
我在 MainWindow
初始化时,绑定了几个常用动作的快捷键:
from PyQt5.QtWidgets import QShortcut
from PyQt5.QtGui import QKeySequence
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
...
# Ctrl+O 打开文件夹
open_sc = QShortcut(QKeySequence("Ctrl+O"), self)
open_sc.activated.connect(self.open_folder_dialog)
# Ctrl+W 关闭当前标签
close_sc = QShortcut(QKeySequence("Ctrl+W"), self)
close_sc.activated.connect(lambda: self.tab_manager.close_tab(self.tab_manager.currentIndex()))
# Ctrl+Q 退出
quit_sc = QShortcut(QKeySequence("Ctrl+Q"), self)
quit_sc.activated.connect(self.close)
open_folder_dialog
则弹出一个 QFileDialog.getExistingDirectory
来选择目录。
2. 右键菜单
在 ImageViewer
里,重写 contextMenuEvent
,弹出一个简单菜单,让用户可以旋转、复制路径、在外部打开等:
from PyQt5.QtWidgets import QMenu
import subprocess
import platform
class ImageViewer(QGraphicsView):
...
def contextMenuEvent(self, event):
menu = QMenu(self)
rotate_action = menu.addAction("旋转 90°")
copy_path_action = menu.addAction("复制路径")
open_external_action = menu.addAction("外部打开")
action = menu.exec_(self.mapToGlobal(event.pos()))
if action == rotate_action:
self.rotate_image()
elif action == copy_path_action:
QApplication.clipboard().setText(self.image_list[self.current_index])
elif action == open_external_action:
self.open_in_explorer()
def rotate_image(self, angle=90):
# 简单起见,直接对 pixmap 变换,再加载
path = self.image_list[self.current_index]
pixmap = QPixmap(path)
transform = QTransform().rotate(angle)
pixmap = pixmap.transformed(transform)
pixmap.save(path) # 覆盖原图
self.load_image()
def open_in_explorer(self):
path = self.image_list[self.current_index]
if platform.system() == "Windows":
subprocess.Popen(f'explorer /select,"{path}"')
elif platform.system() == "Darwin":
subprocess.Popen(["open", "-R", path])
else:
subprocess.Popen(["xdg-open", os.path.dirname(path)])
Context Menu 流程图
在这里插入图片描述
这样,鼠标右键一顿操作,用户可以轻松完成更多功能。
十三、打包与发布
功能完善后,最后一步就是给用户一个一键启动的可执行文件。Python 脚本直接发给用户,还得装 Python、装依赖,太麻烦。
我选了 PyInstaller 做打包:
代码语言:bash复制pip install pyinstaller
pyinstaller --noconfirm --clean --windowed \
--name MultiImageViewer \
--add-data "resources/;resources/" \
--add-data "style.qss;." \
main.py
--windowed
:不弹命令行窗口--add-data
:把我们的资源文件夹和样式表打进去- 打包后,
dist/MultiImageViewer/
下就有.exe 或可执行程序
用户只要下载这个目录,双击就能运行。跨平台时,Mac、Linux 也只要在相应平台打包一次即可。
十四、性能分析与后续优化
- 缓存缩略图:undefined对于大量图片的文件夹,每次打开都扫描、加载会有点慢。我打算在
utils.py
里加一个缩略图缓存模块,首次打开时生成.thumbs/
缓存,下次直接读。 - 多线程加载:undefined当前所有加载都在主线程,若图片超大会卡死界面。可以借助
QThreadPool + QRunnable
,把扫描和第一张缩略图加载放到后台。完成后用信号通知主 UI 更新。 - 多选操作:undefined未来版本,可以在一个标签里支持在缩略图模式下多选,批量复制、删除、批量重命名等。
- 书签与幻灯片模式:undefined给每个文件夹增加“收藏夹”功能,还能一键进入全屏幻灯片。
以上优化,都是下一步可以继续挖坑、填坑的工作。也欢迎大家提意见,一起完善。
十五、总结与感想
回头看看,从最初的 需求收集 到 模块拆分、核心功能实现、美化打包,整个项目落地花了我整整一个周末。过程中遇到的挑战主要有:
- QGraphicsView 的使用细节:要把图片居中、缩放体验要好,需要熟悉
fitInView
、transformationAnchor
等 API。 - 拖拽事件:第一次写
dragEnterEvent
、dropEvent
时,忘记在窗体上setAcceptDrops(True)
,导致根本没触发,调试半天才发现。 - 跨平台小差异:Windows Explorer、macOS Finder、Linux 文件管理器的命令各不相同,一开始只写了 Windows,再补充到 Unix,才算完整。
但最有成就感的是:当我把最终程序交给同事时,他竟然说“这比某些商业软件还好用”。这句话,让我深刻体会到——用心雕琢,哪怕只是一个“个人小工具”,也能给别人带来惊喜。
希望这篇开发纪实,不仅分享了核心思路和代码细节,也能让你看到一个真实项目从无到有的全过程。如果你也想做类似工具,或者想给我提建议,欢迎在评论区留言交流。最后,祝你编码顺利,早日产出属于自己的炫酷小工具!
<small>作者:繁依Fanyi | 时间:2025-04-28</small>
本文标签: PyQt 多标签批量图片查看器开发纪实
版权声明:本文标题:PyQt 多标签批量图片查看器开发纪实 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/biancheng/1747472716a2699122.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论