手把手教你用 Python + PyQt5 做一个可视化图片切图工具
title: 手把手教你用 Python PyQt5 做一个可视化图片切图工具date: 2026-07-03categories: 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_factorself.offset_x),int(iy*self.scale_factorself.offset_y))图片的offset_x/y是实现居中显示的关键——(控件宽 - 缩放后图片宽) // 2。4.2 选区持久化——窗口缩放后不跑偏所有选区数据select_rect统一存储在图片原始坐标系中。每次绘制时通过_widget_rect()实时转换到控件坐标系def_widget_rect(self,img_rect):tlself._from_img(img_rect.x(),img_rect.y())brself._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):hsself.HANDLE_SIZE# 8pxhhhs//2x,y,w,hwr.x(),wr.y(),wr.width(),wr.height()return{tl:QRect(x-hh,y-hh,hs,hs),# 左上角tr:QRect(xw-hh,y-hh,hs,hs),# 右上角bl:QRect(x-hh,yh-hh,hs,hs),# 左下角br:QRect(xw-hh,yh-hh,hs,hs),# 右下角top:QRect(xw//4,y-hh,w//2,hs),# 上边bottom:QRect(xw//4,yh-hh,w//2,hs),left:QRect(x-hh,yh//4,hs,h//2),right:QRect(xw-hh,yh//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 → 重置状态过滤过小选区5pxResize 模式的核心逻辑——根据拖拽的手柄决定修改哪些边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. 在原图上裁取选区croppil_image.crop((x,y,xw,yh))# 2. 计算每个格子尺寸cell_ww//cols cell_hh//rows# 3. 逐格裁剪并保存forrinrange(rows):forcinrange(cols):leftc*cell_w topr*cell_h rightleftcell_wifccols-1elsew bottomtopcell_hifrrows-1elseh tilecrop.crop((left,top,right,bottom))tile.save(fslice_{r1:02d}_{c1:02d}.png,PNG)关键细节最后一行/列不直接使用cell_w * (c1)而是直接用选区的宽/高边界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():appQApplication(sys.argv)app.setStyle(Fusion)# 跨平台统一外观windowSlicerWindow()window.show()sys.exit(app.exec_())if__name____main__:main()九、最后本文实现了一个完整的 PyQt5 图片切图工具核心要点包括坐标映射在控件坐标系和图片坐标系之间做精确换算确保选区不随缩放漂移手柄系统8 个方向拖拽手柄 光标反馈 边界保护提供和 PhotoShop 类似的交互体验拖拽状态机通过_drag_mode区分新建/移动/调整三种操作逻辑清晰且易于扩展Pillow 裁剪最后一行/列自动吸收余数保证无像素丢失整个工具单文件、零外部依赖除 PyQt5 和 Pillow非常适合作为 Python GUI 编程的练手项目也可以直接集成到游戏开发、UI 切图等实际工作流中。源码github下载链接