title: 手把手教你用 Python + PyQt5 做一个可视化图片切图工具
date: 2026-07-03
categories: Python PyQt5 工具开发
tags: [Python, PyQt5, Pillow, 图片处理, GUI]
手把手教你用 Python + PyQt5 做一个可视化图片切图工具
一、写在前面
日常游戏开发、UI 设计、精灵表(Sprite Sheet)处理中,我们经常需要把一张大图按网格切分成若干小图。市面上虽然有 PhotoShop、TexturePacker 等工具,但要么太重,要么需要付费,要么操作繁琐。
这篇文章将带你从零实现一个轻量级桌面切图工具——用鼠标框选区域,设置行列数,一键导出。最终效果如下:
(此处可插入工具运行截图:左侧大图显示区 + 右侧控制面板 + 黄色选区网格)
二、技术选型
| 需求 | 选型 | 理由 |
|---|---|---|
| GUI 框架 | PyQt5 | 功能丰富,跨平台,社区成熟 |
| 图片处理 | Pillow (PIL) | 轻量、读写格式全、裁剪接口简洁 |
| 语言 | Python 3.8+ | 开发效率高,生态完善 |
PyQt5 负责界面交互、图片显示、鼠标事件处理;Pillow 负责最终的像素级裁剪与文件输出。
三、整体架构设计
工具分为两层:
┌─────────────────────────────────────────┐ │ SlicerWindow │ ← 主窗口:布局 + 控制逻辑 │ ┌──────────────────┐ ┌──────────────┐ │ │ │ │ │ 控制面板 │ │ │ │ ImageLabel │ │ ├ 行数/列数 │ │ │ │ (QScrollArea) │ │ ├ 输出目录 │ │ │ │ │ │ └ 切图按钮 │ │ │ └──────────────────┘ └──────────────┘ │ └─────────────────────────────────────────┘- ImageLabel:继承
QLabel,负责图片渲染、选区绘制、鼠标交互,是整个工具的核心组件。 - SlicerWindow:继承
QMainWindow,组装布局、管理文件对话框、执行切图导出。
四、核心难点与解决方案
4.1 坐标映射——两个坐标系的换算
图片加载后按比例缩放并居中显示在控件中。鼠标在控件上点击的位置,需要转换成图片的原始像素坐标,才能做精确裁剪。
图片原始坐标 (x_img, y_img) ←→ 控件坐标 (x_widget, y_widget)换算公式:
x_img=(x_widget-offset_x)/scale_factor y_img=(y_widget-offset_y)/scale_factor代码实现:
def_to_img(self,wx,wy):returnQPoint(int((wx-self.offset_x)/self.scale_factor),int((wy-self.offset_y)/self.scale_factor))def_from_img(self,ix,iy):returnQPoint(int(ix*self.scale_factor+self.offset_x),int(iy*self.scale_factor+self.offset_y))图片的
offset_x/y是实现居中显示的关键——(控件宽 - 缩放后图片宽) // 2。
4.2 选区持久化——窗口缩放后不跑偏
所有选区数据(select_rect)统一存储在图片原始坐标系中。每次绘制时通过_widget_rect()实时转换到控件坐标系:
def_widget_rect(self,img_rect):tl=self._from_img(img_rect.x(),img_rect.y())br=self._from_img(img_rect.x()+img_rect.width(),img_rect.y()+img_rect.height())returnQRect(tl,br)这样一来,无论窗口如何缩放、图片如何重绘,选区在原图上的位置始终不变。
4.3 手柄系统——8 个方向的自由调整
选区确认后显示 8 个黄色拖拽手柄(4 个角 + 4 条边的中点),每个手柄对应不同的拖拽行为:
def_handle_rects(self,wr):hs=self.HANDLE_SIZE# 8pxhh=hs//2x,y,w,h=wr.x(),wr.y(),wr.width(),wr.height()return{'tl':QRect(x-hh,y-hh,hs,hs),# 左上角'tr':QRect(x+w-hh,y-hh,hs,hs),# 右上角'bl':QRect(x-hh,y+h-hh,hs,hs),# 左下角'br':QRect(x+w-hh,y+h-hh,hs,hs),# 右下角'top':QRect(x+w//4,y-hh,w//2,hs),# 上边'bottom':QRect(x+w//4,y+h-hh,w//2,hs),'left':QRect(x-hh,y+h//4,hs,h//2),'right':QRect(x+w-hh,y+h//4,hs,h//2),}命中检测 + 光标反馈:
def_hit_handle(self,pos):forname,hrinself._handle_rects(wr).items():ifhr.contains(pos):returnnamereturnNonedef_cursor_for_handle(self,handle):return{'tl':Qt.SizeFDiagCursor,'br':Qt.SizeFDiagCursor,'tr':Qt.SizeBDiagCursor,'bl':Qt.SizeBDiagCursor,'top':Qt.SizeVerCursor,'bottom':Qt.SizeVerCursor,'left':Qt.SizeHorCursor,'right':Qt.SizeHorCursor,}.get(handle)4.4 拖拽状态机——三种操作模式
鼠标交互分为三种模式,通过状态变量_drag_mode区分:
mousePressEvent ├─ 点击手柄 → _drag_mode = 'resize' ├─ 点击内部 → _drag_mode = 'move' └─ 点击外部 → _drag_mode = 'new'(清除旧选区,创建新选区) mouseMoveEvent ├─ resize: 根据手柄名称只修改对应的边/角 ├─ move: 整体偏移选区 └─ new: 从起点拉出新矩形 mouseReleaseEvent → 重置状态,过滤过小选区(<5px)Resize 模式的核心逻辑——根据拖拽的手柄决定修改哪些边:
ifhin('tl','left','bl'):r.setX(min(r.right()-10,r.x()+dix))ifhin('tl','top','tr'):r.setY(min(r.bottom()-10,r.y()+diy))ifhin('tr','right','br'):r.setWidth(max(10,r.width()+dix))ifhin('bl','bottom','br'):r.setHeight(max(10,r.height()+diy))边界保护:min(r.right() - 10, ...)和max(10, ...)防止选区被拖拽到反转或消失。
五、切图算法详解
选区 + 网格参数确定后,切图逻辑非常简单:
# 1. 在原图上裁取选区crop=pil_image.crop((x,y,x+w,y+h))# 2. 计算每个格子尺寸cell_w=w//cols cell_h=h//rows# 3. 逐格裁剪并保存forrinrange(rows):forcinrange(cols):left=c*cell_w top=r*cell_h right=left+cell_wifc<cols-1elsew bottom=top+cell_hifr<rows-1elseh tile=crop.crop((left,top,right,bottom))tile.save(f"slice_{r+1:02d}_{c+1:02d}.png","PNG")关键细节:最后一行/列不直接使用cell_w * (c+1),而是直接用选区的宽/高边界w/h。这是因为w可能不能被cols整除,直接截断会丢失像素。用边界值兜底可以确保覆盖全部选区,不会出现缝隙或丢失。
六、完整代码结构
slicer.py (约 487 行) ← 单文件,无外部资源依赖 ├── ImageLabel(QtWidgets.QLabel) ← 核心画板 │ ├── 属性 (origin_pixmap, select_rect, rows/cols, _drag_*) │ ├── 坐标转换 (_to_img, _from_img, _widget_rect) │ ├── 手柄系统 (_handle_rects, _hit_handle, _cursor_for_handle) │ ├── 事件 (mousePress/Move/Release, resizeEvent, paintEvent) │ └── 信号 rect_changed(QRect) └── SlicerWindow(QMainWindow) ← 主窗口 ├── 左侧 QScrollArea + ImageLabel ├── 右侧控制面板 (打开、行列、目录、切图) └── 导出方法 export_slices()6.1 ImageLabel 核心属性
origin_pixmap:QPixmap# 原始图片scale_factor:float# 缩放比例offset_x,offset_y:int# 居中偏移(像素)select_rect:QRect|None# 选区(图片坐标系)rows,cols:int# 网格行列数_dragging:bool# 是否正在拖拽_drag_mode:str|None# 'new' / 'move' / 'resize'_drag_handle:str|None# 当前拖拽的手柄名称_drag_start_widget:QPoint# 拖拽起始点(控件坐标)_drag_start_rect:QRect# 拖拽起始选区快照6.2 SlicerWindow 主窗口布局
七、使用演示
7.1 基础流程
- 启动程序:
python slicer.py - 点击「打开图片」,选择一张大图
- 在图片上按住鼠标左键拖拽,松开后出现黄色选区
- 拖拽选区边角的手柄微调大小,或拖动内部移动位置
- 右侧设置行数 = 4,列数 = 4,选区上实时显示 4×4 网格
- 点击「开始切图」,输出目录下生成 16 个 PNG 文件
7.2 命名规范
slice_01_01.png ← 第1行第1列 slice_01_02.png ← 第1行第2列 ... slice_04_04.png ← 第4行第4列八、完整源码
# 完整源码见同目录 slicer.py(约 487 行)# 或访问:https://github.com/HuangHunterPlus/python_image_slicer_tools核心代码已在文章中分段解析,完整源码在文末附带的slicer.py文件中。你也可以直接复制各章节的代码片段自行组装。
这里再贴一下启动入口供参考:
defmain():app=QApplication(sys.argv)app.setStyle("Fusion")# 跨平台统一外观window=SlicerWindow()window.show()sys.exit(app.exec_())if__name__=="__main__":main()九、最后
本文实现了一个完整的 PyQt5 图片切图工具,核心要点包括:
- 坐标映射:在控件坐标系和图片坐标系之间做精确换算,确保选区不随缩放漂移
- 手柄系统:8 个方向拖拽手柄 + 光标反馈 + 边界保护,提供和 PhotoShop 类似的交互体验
- 拖拽状态机:通过
_drag_mode区分新建/移动/调整三种操作,逻辑清晰且易于扩展 - Pillow 裁剪:最后一行/列自动吸收余数,保证无像素丢失
整个工具单文件、零外部依赖(除 PyQt5 和 Pillow),非常适合作为 Python GUI 编程的练手项目,也可以直接集成到游戏开发、UI 切图等实际工作流中。
源码github下载链接