手把手教你用 Python + PyQt5 做一个可视化图片切图工具


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)

换算公式:

python 复制代码
x_img = (x_widget - offset_x) / scale_factor
y_img = (y_widget - offset_y) / scale_factor

代码实现:

python 复制代码
def _to_img(self, wx, wy):
    return QPoint(
        int((wx - self.offset_x) / self.scale_factor),
        int((wy - self.offset_y) / self.scale_factor)
    )

def _from_img(self, ix, iy):
    return QPoint(
        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() 实时转换到控件坐标系:

python 复制代码
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()
    )
    return QRect(tl, br)

这样一来,无论窗口如何缩放、图片如何重绘,选区在原图上的位置始终不变。

4.3 手柄系统------8 个方向的自由调整

选区确认后显示 8 个黄色拖拽手柄(4 个角 + 4 条边的中点),每个手柄对应不同的拖拽行为:

python 复制代码
def _handle_rects(self, wr):
    hs = self.HANDLE_SIZE  # 8px
    hh = hs // 2
    x, 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),
    }

命中检测 + 光标反馈:

python 复制代码
def _hit_handle(self, pos):
    for name, hr in self._handle_rects(wr).items():
        if hr.contains(pos):
            return name
    return None

def _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 模式的核心逻辑------根据拖拽的手柄决定修改哪些边:

python 复制代码
if h in ('tl', 'left', 'bl'):
    r.setX(min(r.right() - 10, r.x() + dix))
if h in ('tl', 'top', 'tr'):
    r.setY(min(r.bottom() - 10, r.y() + diy))
if h in ('tr', 'right', 'br'):
    r.setWidth(max(10, r.width() + dix))
if h in ('bl', 'bottom', 'br'):
    r.setHeight(max(10, r.height() + diy))

边界保护:min(r.right() - 10, ...)max(10, ...) 防止选区被拖拽到反转或消失。


五、切图算法详解

选区 + 网格参数确定后,切图逻辑非常简单:

python 复制代码
# 1. 在原图上裁取选区
crop = pil_image.crop((x, y, x + w, y + h))

# 2. 计算每个格子尺寸
cell_w = w // cols
cell_h = h // rows

# 3. 逐格裁剪并保存
for r in range(rows):
    for c in range(cols):
        left = c * cell_w
        top = r * cell_h
        right = left + cell_w if c < cols - 1 else w
        bottom = top + cell_h if r < rows - 1 else h
        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 核心属性

python 复制代码
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 基础流程

  1. 启动程序:python slicer.py
  2. 点击「打开图片」,选择一张大图
  3. 在图片上按住鼠标左键拖拽,松开后出现黄色选区
  4. 拖拽选区边角的手柄微调大小,或拖动内部移动位置
  5. 右侧设置行数 = 4,列数 = 4,选区上实时显示 4×4 网格
  6. 点击「开始切图」,输出目录下生成 16 个 PNG 文件

7.2 命名规范

复制代码
slice_01_01.png    ← 第1行第1列
slice_01_02.png    ← 第1行第2列
...
slice_04_04.png    ← 第4行第4列

八、完整源码

python 复制代码
# 完整源码见同目录 slicer.py(约 487 行)
# 或访问:https://github.com/HuangHunterPlus/python_image_slicer_tools

核心代码已在文章中分段解析,完整源码在文末附带的 slicer.py 文件中。你也可以直接复制各章节的代码片段自行组装。

这里再贴一下启动入口供参考:

python 复制代码
def main():
    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下载链接

相关推荐
weixin199701080161 小时前
[特殊字符]《京东订单API(jd.order.detail.get)对接ERP:企业认证+OAuth授权避坑指南》(附Python源码)
java·数据库·python
云烟成雨TD2 小时前
LangFlow 1.x 系列【3】入门案例
人工智能·python·agent
创世宇图2 小时前
【Python工程化实战】Python 服务的结构化日志体系:structlog + JSON 输出 + 日志分级策略
python·elk·structlog·结构化日志·可观测性
aaaameliaaa2 小时前
计算斐波那契数(递归、迭代)(1,1,2,3,5.....)
c语言·开发语言·笔记·算法·排序算法
m0_547486662 小时前
《模式识别:使用MATLAB分析与实现》全套PPT课件
开发语言·matlab·模式识别
Tim_102 小时前
【C++】009、extern关键字
java·开发语言
神经智研社2 小时前
ROS2-5章:节点参数parameter详细讲解
windows·microsoft·机器人环境搭建·win11 ros2 开发环境
创世宇图3 小时前
【Python工程化实战】Kubernetes 中 Python 应用的优雅启停与健康检查:零停机滚动更新实战
python·云原生·kubernetes·优雅停机
夜雪一千3 小时前
Python 使用OpenAI调用Qwen3.6-27B-ms模型|完整参数详解
开发语言·python