PyQt4 的图片切割编辑器

一、 编辑器功能明确

允许用户加载图片、选择切割模式、对切割后的图片片段进行操作(如移动、复制、粘贴、删除等),并支持撤销和重做操作。

环境:Py2.7 PyQt 4.11

二、导入模块介绍

sys: 用于访问与 Python 解释器强相关的变量和函数。

os: 提供操作系统相关功能,如文件路径操作。

random: 用于生成随机数,主要用于自动保存文件名。

json: 用于数据序列化和反序列化,方便保存和加载编辑状态。

glob: 用于文件路径匹配,例如检查自动保存文件是否存在。

PyQt4: 用于创建图形用户界面(GUI)。

三、构造函数分析

复制代码
def __init__(self):
    super(MainWindow, self).__init__()  # 调用父类的构造函数
    self.setWindowTitle('Picture Cutting Editor')  # 设置窗口标题
    self.undoStack = QtGui.QUndoStack(self)  # 创建一个QUndoStack对象,用于撤销和重做
    self.initUI()  # 初始化界面
    self.initSignal()  # 初始化信号连接
    self.image = QtGui.QPixmap()  # 初始化图片对象
    self.center()  # 居中显示窗口
    self.xx = 5
    self.yy = 5
    self._m_cut_type_str = ''  # 初始化切割模式字符串
    self.curFile = ''  # 当前文件路径
    self.locationList = []  # 存储图片位置的列表
    self.cuttingWidgetList = []  # 存储切割小部件的列表
    self.posList = []  # 存储位置信息的列表
    self.location_pixmap_dict = {}  # 存储位置和图片的字典
    self.data = [...]  # 存储数据的列表
    self.auto_save_data = [...]  # 自动保存数据
    self.savepath_autopath_dict = {}  # 保存路径和自动保存路径的连接字典
    self.autoName = ''  # 自动保存文件名

self.undoStack: 用于支持撤销和重做操作。

self.image: 存储当前加载的图片。

self.data: 存储编辑状态的数据,方便保存和加载。

self.auto_save_data: 存储自动保存的数据,防止数据丢失。

四、扩展的类

  1. QtGui.QUndoStack(self) # 创建一个QUndoStack对象,用于撤销和重做
  2. QtGui.QPixmap() # 存储当前加载的图片,QtGui.QPixmap()可以从文件、资源或内存中加载图像,并可以用于显示图像
  3. QtGui.QFileDialog.getOpenFileName: 打开文件选择对话框,让用户选择图片文件。

五、小技巧

  1. & 符号

fileMenu = menubar.addMenu(u'文件(&F)') # 添加"文件"菜单

在 PyQt 中,菜单项或按钮的文本中添加 & 符号是一种特殊的语法,用于定义快捷键(Accelerator)。& 符号后面的字母会变成该菜单项或按钮的快捷键

  1. undoStack

undoStack的宏操作(相当于数据库中的事务):将多个操作绑定在一起,一并撤销:

有了undoStack来实现指令的撤销和恢复真的太容易了。

复制代码
undoStack.beginMacro('bind1'):开始一个宏操作,允许将多个命令组合在一起。
undoStack.push(self.cmd1):将命令推入撤销栈,可以推多个命令进入栈中。
undoStack.endMacro():结束宏操作,将所有推入的命令组合为一个。
  1. 创建一个新的编辑窗口(MainWindow 的实例),并将其显示在屏幕上(QT多窗口模板):

    复制代码
     def newImageFile(self):
         newWindow = MainWindow()  # 创建一个新的 MainWindow 实例
         # 将新窗口实例添加到 windowList 中,以便程序可以统一管理所有打开的窗口。(这是在做商城大作业时的收获,无论是红点对象还是复用滚动框中的滚动对象都建议使用一个列表保存起来。(1. 避免重复创建,减少性能消耗  2.统一管理)
         MainWindow.windowList.append(newWindow)  # 将新窗口添加到窗口列表中
         newWindow.move(self.x() + 50, self.y() + 50)  # 设置新窗口的位置
     	newWindow.show()  # 显示新窗口
  2. Py2获取当前年月日时分秒,并生成一个年-月-日-时-分-秒的字符串

    案例:根据当前时间生成新的自动保存文件名

    复制代码
         from datetime import datetime
             # 获取当前时间
             now = datetime.now()
         	# 格式化为 "年-月-日-时-分-秒" 的字符串
             formatted_time = now.strftime("%Y-%m-%d-%H-%M-%S")
                   autoName = 'auto_save/' + formatted_time + '.json'
  3. Py2 解析Json文件:

    复制代码
         with open(fileName) as fp:
          load_data = json.load(fp)  # 使用 json.load 从文件中加载 JSON 数据,并将其存储到 self.data 中。

六、图片切割编辑器案例

python 复制代码
# -*- coding: utf-8 -*-
import sys
import os
import random
import json
import glob
from PyQt4 import QtGui
from PyQt4 import QtCore

#定义主窗口类
class MainWindow(QtGui.QMainWindow):
    windowList = []#用于存储所有打开的窗口
    def __init__(self):
        super(MainWindow, self).__init__()  #调用父类的构造函数
        self.setWindowTitle('Picture Cutting Editor')  # 设置窗口标题
        self.undoStack = QtGui.QUndoStack(self)  # 创建一个QUndoStack对象,用于撤销和重做
        self.initUI()  # 初始化界面,设置菜单、工具栏等。
        self.initSignal()  # 初始化信号连接,绑定事件处理函数。
        self.image = QtGui.QPixmap() # 存储当前加载的图片,QtGui.QPixmap()可以从文件、资源或内存中加载图像,并可以用于显示图像
        self.center()  # 居中显示窗口
        self.xx = 5
        self.yy = 5  # 初始化切割网格的行列数,默认为5x5。
        self._m_cut_type_str = ''  # 初始化切割模式字符串,用于描述当前的切割方式。
        self.curFile = ''  # 当前编辑的文件路径。
        self.locationList = []  # 存储图片片段的位置信息。
        self.cuttingWidgetList = []  # 存储切割后的小部件(如图片片段)。
        self.posList = []  # 存储图片片段的位置信息。
        self.location_pixmap_dict = {}  # 存储位置与图片片段的映射关系。
        self.data = [  # 存储编辑状态的数据,方便保存和加载。
            {
                'path': [],  # 存储文件路径
                'type': [],  # 存储切割类型
                'location_list' : [],  # 存储位置列表
                'square_list' : [],  # 存储切割小部件列表
                'pos_list':[],  # 存储位置信息列表
            }
        ]
        self.auto_save_data = [  # 存储自动保存的数据,防止数据丢失。
            {
                'autoSavePath':[],  # 自动保存路径列表
                'connectDict':{},  # 自动保存路径和文件路径的连接字典
            }
        ]
        self.savepath_autopath_dict = {}  # 保存路径和自动保存路径的连接字典
        self.autoName = ''  # 自动保存文件名

    def init(self):
	# 初始化、清空数据结构,重置状态
        self.locationList = []  # 清空图片位置信息列表
        self.cuttingWidgetList = []  # 清空切割小部件列表
        self.posList = []  # 清空位置信息列表
        self.location_pixmap_dict = {}  # 清空位置与图片片段的映射字典
        self.data = [  # 重置存储编辑状态的数据结构
            {
                'path':[],  # 清空文件路径列表
                'type':[],  # 清空切割类型列表
                'location_list':[],  # 清空位置列表
                'square_list':[],  # 清空切割小部件列表
                'pos_list':[],   # 清空位置信息列表
            }
        ]

        self.auto_save_data = [   # 重置自动保存数据结构
            {
                 'autoSavePath':[],  # 清空自动保存路径列表
                 'connectDict':{} # 清空自动保存路径与文件路径的映射字典
            }
        ]

        self.savepath_autopath_dict = {}  # 清空保存路径与自动保存路径的映射字典
        self.piecesList.clear()  # 清空图片片段列表组件
        self.piecesList.locationList = []  # 清空图片片段位置列表
        self.picWidget.cuttingWidgetList = []  # 清空图片编辑区域的切割小部件列表
        self.picWidget.pixmapList = []  # 清空图片编辑区域的图片片段列表
        self.picWidget.posList = []  # 清空图片编辑区域的位置信息列表
        self.picWidget.update()  # 更新图片编辑区域的显示
    
    # 初始化用户界面(UI)
    def initUI(self):
        self.statusBar()  # 创建状态栏,用于显示状态信息
        self.setFocus()  # 设置窗口获取焦点, 确保窗口获取焦点。
        self.setWidgets()  # 调用setWidgets方法,设置主窗口的中心部件
	
	# 创建菜单栏和导航栏的选项
        openFile = QtGui.QAction(QtGui.QIcon('resources/openFile.png'), 'Open', self)
        openFile.setShortcut('Ctrl+O')  # 设置快捷键
        openFile.setStatusTip('Open new file')  # 设置状态栏提示
        self.connect(openFile, QtCore.SIGNAL('triggered()'), self.openFile)  # 将动作与槽函数连接

        newFile = QtGui.QAction(QtGui.QIcon('resources/new.png'), 'New', self)
        newFile.setShortcut('Ctrl+N')
        newFile.setStatusTip('New file')
        self.connect(newFile, QtCore.SIGNAL('triggered()'), self.newImageFile)

        # 导入图片,选择网格大小
        importImage = QtGui.QAction('Import Image', self)
        importImage.setStatusTip('Import image')
        self.connect(importImage, QtCore.SIGNAL('triggered()'), self.showDialog)

        saveFile = QtGui.QAction(QtGui.QIcon('resources/save.png'), 'Save', self)
        saveFile.setShortcut('Ctrl+S')
        saveFile.setStatusTip('Save file')
        self.connect(saveFile, QtCore.SIGNAL('triggered()'), self.saveImageFile)

        save_as = QtGui.QAction(QtGui.QIcon('resources/save_as.png'), 'Save As', self)
        save_as.setShortcut('Ctrl+Shift+S')
        save_as.setStatusTip('Save file as')
        self.connect(save_as, QtCore.SIGNAL('triggered()'), self.saveImageAs)

        undo = QtGui.QAction(QtGui.QIcon('resources/Undo.png'), 'Undo', self)
        undo.setShortcut('Ctrl+Z')
        undo.setStatusTip('Undo')
        self.connect(undo, QtCore.SIGNAL('triggered()'), self.undoImage)

        redo = QtGui.QAction(QtGui.QIcon('resources/Redo.png'), 'Redo', self)
        redo.setShortcut('Ctrl+Y')
        redo.setStatusTip('Redo')
        self.connect(redo, QtCore.SIGNAL('triggered()'), self.redoImage)

        cut =QtGui.QAction(QtGui.QIcon('resources/cut.png'), 'Cut', self)
        cut.setShortcut('Ctrl+X')
        cut.setStatusTip('Cut')
        self.connect(cut, QtCore.SIGNAL('triggered()'), self.cutImage)
        
        copyImage =QtGui.QAction(QtGui.QIcon('resources/copy.png'),'Copy',self)
        copyImage.setShortcut('Ctrl+C')
        copyImage.setStatusTip('Copy')
        self.connect(copyImage, QtCore.SIGNAL('triggered()'),self.copyImage)
        
        pasteImage =QtGui.QAction(QtGui.QIcon('resources/paste.png'), 'Paste', self)
        pasteImage.setShortcut('Ctrl+V')
        pasteImage.setStatusTip('Paste')
        self.connect(pasteImage,QtCore.SIGNAL('triggered()'),self.pasteImage)
        
        deleteImage =QtGui.QAction(QtGui.QIcon('resources/delete.png'), 'Delete', self)
        deleteImage.setShortcut('Delete')
        deleteImage.setStatusTip('delete')
        self.connect(deleteImage,QtCore.SIGNAL('triggered()'), self.deleteImage)
        
        # 创建菜单栏
        menubar = self.menuBar()   # 获取窗口的菜单栏
        # 文件
        fileMenu = menubar.addMenu(u'文件(&F)')  # 添加"文件"菜单
        # 在 PyQt 中,菜单项或按钮的文本中添加 & 符号是一种特殊的语法,用于定义快捷键(Accelerator)。& 符号后面的字母会变成该菜单项或按钮的快捷键
        fileMenu.addAction(openFile) # 添加"打开"选项
        fileMenu.addAction(newFile)
        fileMenu.addAction(importImage)
        fileMenu.addAction(saveFile)
        fileMenu.addAction(save_as)
        # 编辑
        editMenu = menubar.addMenu(u'编辑(&E)')  # 添加"编辑"菜单
        editMenu.addAction(undo)  # 添加"撤销"选项
        editMenu.addAction(redo)
        editMenu.addAction(cut)
        editMenu.addAction(copyImage)
        editMenu.addAction(pasteImage)
        # 网格(设置网格大小 和 长宽)
	
	# 创建工具栏
        self.Toolbar = self.addToolBar(u'工具栏(&T)')  # 添加工具栏
        self.Toolbar.addAction(undo)
        self.Toolbar.addAction(redo)
        self.Toolbar.addAction(openFile)   # 添加"打开"按钮
        self.Toolbar.addAction(newFile)
        self.Toolbar.addAction(saveFile)
        self.Toolbar.addAction(save_as)
        self.Toolbar.addAction(cut)
        self.Toolbar.addAction(copyImage)
        self.Toolbar.addAction(pasteImage)
        self.Toolbar.addAction(deleteImage)
        
        # 使用 self.setGeometry() 设置窗口的初始位置和大小,确保窗口在屏幕上合适的位置显示。
        self.setGeometry(300, 300, 1024, 768)  # 设置窗口的位置和大小
        # 参数解释:窗口左上角的X坐标、Y坐标,窗口宽度、窗口高度
    
    # 初始化信号连接: 将自定义信号连接到对应的槽函数,处理拖拽、保存等操作。
    # 信号与槽是 PyQt 中实现组件间通信的核心机制,通过这种方式可以将组件的事件(如按钮点击、信号发射)与相应的处理函数(槽函数)绑定起来,从而实现交互逻辑。
    def initSignal(self):
        # piecesList: 自定义组件,用于显示图片片段的列表。
        self.piecesList.upSignal.connect(self.upListWidget)  # 自定义信号,。
        self.piecesList.downSignal.connect(self.downListWidget)  # 自定义信号,。
        self.piecesList.saveSignal.connect(self.curSave)  # 自定义信号,。
        # picWidget: 自定义组件,用于显示和编辑图片的区域。
        self.picWidget.upSignal.connect(self.upWidget)  # 自定义信号,。
        self.picWidget.downSignal.connect(self.downWidget)
        self.picWidget.saveSignal.connect(self.curSave)
    
    # 处理用户从列表中拖拽出图片片段的操作。
    # 使用 QUndoStack 的宏功能,将"移除"和"插入"两个操作组合在一起,以便用户可以通过一次撤销操作撤销整个拖拽过程。
    def upListWidget(self):
        item = self.piecesList.currentItem()  # 获取当前选中的图片片段
        row = self.piecesList.row(item)  # 获取该项在列表中的行号
        command=CommandDragFromListWidget(self.piecesList,item,row)  # 自定义的撤销命令类,用于处理从列表中拖拽图片片段的操作。
        self.cmd = command  # 将命令对象存储在类变量中,供后续使用
    
    # 处理用户将图片片段拖拽回列表的操作。
    def downListWidget(self):
        command =CommandDragToListWidget(self.piecesList)  # 一个自定义的撤销命令类,用于处理将图片片段拖拽到列表中的操作。它包含将图片片段插入到新位置的逻辑。
        self.undoStack.beginMacro('bind2')  # 开始一个宏操作,允许将多个命令组合在一起。这样,用户可以通过一次撤销操作撤销整个宏操作,而不是逐个撤销每个命令。
        self.undoStack.push(self.cmd)  # 将之前在 upListWidget 中创建的命令推入撤销栈。这个命令表示从列表中移除图片片段的操作。
        self.undoStack.push(command)  # 将当前命令(表示将图片片段插入到列表的操作)推入撤销栈。
        self.undoStack.endMacro( )  # 结束宏操作,将所有推入的命令组合为一个完整的操作。
    
    # 处理用户从图片编辑区域中拖拽出图片片段的操作。
    def upWidget(self):
        command = CommandDragFromWidget(self.picWidget)  # 一个自定义的撤销命令类,用于处理从图片编辑区域(picWidget)中拖拽出图片片段的操作。
        self.cmd = command  # 将创建的命令对象存储在类变量 self.cmd 中,以便后续的 downWidget 方法可以使用它。

    # 处理用户将图片片段拖拽回图片编辑区域的操作。
    def downWidget(self):
        command = CommandDragToWidget(self.picWidget)  # 自定义的撤销命令类,用于处理将图片片段拖拽到图片编辑区域(picWidget)中的操作。
        self.undoStack.beginMacro('bind1')  # 开始一个宏操作,允许将多个命令组合在一起。
        self.undoStack.push(self.cmd)  # 将之前在 upWidget 中创建的命令推入撤销栈。
        self.undoStack.push(command)  # 将当前命令(表示将图片片段插入到图片编辑区域的操作)推入撤销栈。
        self.undoStack.endMacro()  # 结束宏操作,将所有推入的命令组合为一个完整的操作。

    # 实现了一个对话框,用于让用户选择图片的切割模式
    def showDialog(self):  
        self.dialog = QtGui.QDialog()  # 创建一个对话框窗口
        label = QtGui.QLabel(u'请选择切割模式', self.dialog)  # 创建一个标签,提示用户选择切割模式
        combo = QtGui.QComboBox(self.dialog)  # 创建一个下拉组合框,用于选择切割模式
        combo.addItem('8 * 8')  # 向组合框中添加"8 * 8"选项
        combo.addItem('8 * 4')
        label.move(50, 30)  # 设置标签的位置
        combo.move(50, 50)  # 设置组合框的位置
        self.dialog.setVisible(True)  # 显示对话框
        
        # 将下拉框的选项连接到信号与槽
        self.connect(combo, QtCore.SIGNAL('activated(QString)'), self.onActivated)  # 根据选择设置切割方式
        self.connect(combo, QtCore.SIGNAL('activated(QString)'), self.openImage)  # 加载切割后的图片

    def onActivated(self, text):
        if text == '8 * 8':  #  如果用户选择了"8 * 8",则设置 self.xx 和 self.yy 为 8,表示切割图片为 8x8 张。
            self.xx = 8
            self.yy = 8
            self._m_cut_type_str = '8 * 8'  # 选择的模式存储在 self._m_cut_type_str 中,用于后续的逻辑处理。
        elif text == '8 * 4':
            self.xx = 8
            self.yy = 4
            self._m_cut_type_str = '8 * 4'
        self.dialog.setVisible(False)  # 关闭对话框。

    def openImage(self):  # 如果用户选择了文件
        autoName = 'auto_save/' + str(random.randint(1, 1000))+'.json'  # 生成随机的自动保存文件名
        self.autoName = autoName  # 将自动生成的文件名存储在类变量中
        # QtGui.QFileDialog.getOpenFileName: 打开文件选择对话框,让用户选择图片文件。
        filename = QtGui.QFileDialog.getOpenFileName(self, "Open Image", 'resources', "Image Files (*.png *.jpg *.bmp)")
        if filename :  # 调用loadImage方法加载图片并初始化编辑状态
            self.loadImage(filename)  # self.loadImage: 加载图片并初始化编辑状态。

    def loadImage(self, filename):
        self.init()  # 初始化或重置程序状态,清空之前的编辑数据
        self.undoStack.clear()  # 清空撤销栈,移除之前的撤销和重做记录
        image = QtGui.QPixmap(filename)  # 加载图像文件, QPixmap 是一个轻量级的图像处理类,主要用于在屏幕上显示图像。
        self.image = image  # 将加载的图像存储到类变量中
        self.setupPic(self.xx, self.yy)  # self.setupPic: 根据切割模式(如 8x8 或 8x4)对图片进行切割,并初始化显示。
        self.data[0]['path'].append(str(filename))  # 将图片路径保存起来

    # 创建一个新的图片编辑窗口(MainWindow 的实例),并将其显示在屏幕上。
    def newImageFile(self):
        newWindow = MainWindow()  # 创建一个新的 MainWindow 实例
        # 将新窗口实例添加到 windowList 中,以便程序可以统一管理所有打开的窗口。
        MainWindow.windowList.append(newWindow)  # 将新窗口添加到窗口列表中
        newWindow.move(self.x() + 50, self.y() + 50)  # 设置新窗口的位置
        newWindow.show()  # 显示新窗口
        
    def openFile(self):
        # 检查自动保存文件:
        if glob.glob('auto_save/autosave.json')!= []:  # 使用 glob.glob 检查是否存在自动保存文件autosave.json
            with open('auto_save/autosave.json') as fp:
                load_data = json.load(fp)
            self.auto_save_data = load_data  # 如果存在,加载该文件并将其内容存储到 self.auto_save_data 中。
        # 根据当前时间生成新的自动保存文件名
        from datetime import datetime
	# 获取当前时间
	now = datetime.now()
	# 格式化为 "年-月-日-时-分-秒" 的字符串
	formatted_time = now.strftime("%Y-%m-%d-%H-%M-%S")
        autoName = 'auto_save/' + formatted_time + '.json'
        self.autoName = autoName
        # 打开文件选择对话框:
        fileName = QtGui.QFileDialog.getOpenFileName(self, directory='save')  # 使用 QtGui.QFileDialog.getOpenFileName 打开文件选择对话框,让用户选择一个保存的编辑状态文件。
        if fileName:
            # 如果选择的文件在自动保存数据中存在对应的自动保存路径,弹出一个消息框询问用户是否加载自动保存的数据。
            # 如果用户选择"是",则加载自动保存的数据并覆盖原文件。
            if self.auto_save_data[0]['connectDict'].has_key(str(fileName)):
                rep = QtGui.QMessageBox.question(self, u'存在上一局游戏', u"该图片被打开过,是否加载上一局游戏?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
                if rep == QtGui.QMessageBox.Yes:
                    with open(self.auto_save_data[0]['connectDict'][str(fileName)]) as fpp:
                        data =json.load(fpp)
                    with open(fileName,'w')as fp:
                        json.dump(data,fp,indent=2)
            self.loadFile(fileName)  # 加载文件

    def loadFile(self,fileName):
        # 初始化状态, 清空当前的编辑状态和撤销栈。
        self.init()
        self.undoStack.clear()

        # 加载用户选择的文件内容,并将其存储到 self.data 中。
        with open(fileName) as fp:
            load_data = json.load(fp)  # 使用 json.load 从文件中加载 JSON 数据,并将其存储到 self.data 中。
        self.data = load_data
        self.image = QtGui.QPixmap(QtCore.QString(self.data[0]['path'][0]))  # 使用 QtGui.QPixmap 加载图片文件路径(从 self.data 中获取)。(QtCore.QString 是 PyQt4 中用于处理字符串的类)
        
        #  根据保存的切割模式设置 self.xx 和 self.yy。
        if self.data[0]['type'][0]== '8 * 8':
            self.xx=8
            self.yy=8
        if self.data[0]['type'][0]=='8 * 4':
            self.xx= 8
            self.yy =4
            
        # 切图: 先将图片并缩放到固定大小(400x400, 再切割图片
        # 使用 scaled 方法将图片缩放到固定大小(400x400),忽略纵横比,并使用平滑变换以获得更好的视觉效果。
        size = min(self.image.width(), self.image.height())
        self.image = self.image.copy((self.image.width()-size)/2, (self.image.height() - size)/2, size, size).scaled(400, 400, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
        self.piecesList.clear()  # 清空图片片段列表,移除所有之前的图片片段。

        # 根据切割模式(self.xx 和 self.yy),将图片分割成多个片段。每个片段的大小为 400 / self.xx 和 400 / self.yy。将每个片段存储到 self.location_pixmap_dict 中,键为片段的位置(如 (1, 1) 表示左上角的片段)。
        for y in range(self.xx):
            for x in range(self.yy):
                pieceImage = self.image.copy(x*400/self.yy, y*400/self.xx, 400/self.xx, 400/self.yy)
                self.location_pixmap_dict[(x+1, y+1)] = pieceImage

        # 加载数据: 从 self.data 中恢复位置列表、切割小部件列表和位置信息列表。
        locationList = self.data[0]['location_list']
        print len(self.data[0]['location_list'])
        cuttingWidgetList = self.data[0]['square_list']
        posList = self.data[0]['pos_list']

        # 遍历位置列表,将每个位置恢复为 QtCore.QPoint 对象,并存储到 self.locationList 中。
        # QtCore.QPoint 是 PyQt4 中的一个类,用于表示二维空间中的一个点。它由两个主要属性组成:x 和 y,分别表示点的横坐标和纵坐标。
        for i in range(len(locationList)):
            x = locationList[i][0]
            y = locationList[i][1]
            self.locationList.append(QtCore.QPoint(x, y))
            
        # 遍历切割小部件列表,将每个小部件恢复为 QtCore.QRect 对象,并存储到 self.cuttingWidgetList 中。
        # QtCore.QRect 是 PyQt4 中的一个类,用于表示一个矩形区域。它通常用于定义控件的位置和大小,或者在绘图和布局管理中指定一个矩形范围。
        for j in range(len(cuttingWidgetList)):
            x = cuttingWidgetList[j][0]
            y = cuttingWidgetList[j][1]
            width = cuttingWidgetList[j][2]
            height = cuttingWidgetList[j][3]
            self.cuttingWidgetList.append(QtCore.QRect(x, y, width, height))

        # 遍历位置信息列表,将每个位置恢复为 QtCore.QPoint 对象,并存储到 self.posList 中。
        for k in range(len(posList)):
            x = posList[k][0]
            y = posList[k][1]
            self.posList.append(QtCore.QPoint(x, y))

	# 清空图片片段列表,并根据恢复的数据重新添加图片片段。
	# 遍历位置列表,根据每个位置从 self.location_pixmap_dict 中获取对应的图片片段,并将其添加到图片片段列表中。
        cnt = 0
        for location in self.locationList:
           pixmap = self.location_pixmap_dict[(location.x(), location.y())]
           self.piecesList.addPiece(pixmap, location)

        # 根据位置信息列表生成图片片段列表。
        pixmapList = []
        for pos in self.posList:
            pixmap_r = self.location_pixmap_dict[(pos.x(), pos.y())]
            pixmapList.append(pixmap_r)

        # 设置图片编辑区域
        # 将恢复的切割小部件列表、位置信息列表和图片片段列表设置到图片编辑区域。
        self.picWidget.cuttingWidgetList = self.cuttingWidgetList
        self.picWidget.posList = self.posList
        self.picWidget.pixmapList = pixmapList
        self.picWidget.update()  # 调用 update() 方法,重新绘制图片编辑区域。
        # 调用 setCurrentFile 方法,设置当前文件路径并更新窗口标题。
        self.setCurrentFile(fileName)
  
    def saveImageFile(self):
        # 如果 self.curFile(当前文件路径)不为空,调用 self.saveFile(self.curFile) 方法,将当前编辑状态保存到已有的文件中。
        if self.curFile:
            return self.saveFile(self.curFile)
	return self.saveImageAs()
	
    def saveImageAs(self):
        # 使用 QtGui.QFileDialog.getSaveFileName 打开文件保存对话框,提示用户选择保存路径。
        fileName = QtGui.QFileDialog.getSaveFileName(self, directory='save')
	# 如果用户选择了文件路径(fileName 不为空),调用 self.saveFile(fileName) 方法保存文件。
	if fileName:
            return self.saveFile(fileName)
        return False
      
    def saveFile(self,fileName):
        # 清空保存的数据结构,准备重新填充当前编辑状态。
        self.data[0]['location_list'] = []
        self.data[0]['square_list'] = []
        self.data[0]['pos_list'] = []
        # 遍历 self.locationList,将每个位置(QPoint)的坐标保存到 self.data[0]['location_list'] 中。
        for i in range(len(self.locationList)):
            self.data[0]['location_list'].append([self.locationList[i].x(), self.locationList[i].y()])
        # 遍历 self.cuttingWidgetList,将每个小部件(QRect)的坐标和尺寸保存到 self.data[0]['square_list'] 中。
        for j in range(len(self.cuttingWidgetList)):
            self.data[0]['square_list'].append([self.cuttingWidgetList[j].x(), self.cuttingWidgetList[j].y(), self.cuttingWidgetList[j].width(), self.cuttingWidgetList[j].height()])
        # 遍历 self.posList,将每个位置的坐标保存到 self.data[0]['pos_list'] 中。
        for k in range(len(self.posList)):
            self.data[0][ 'pos_list'].append([self.posList[k].x(), self.posList[k].y()])
        # 如果 self.data[0]['type'] 为空,添加当前切割模式(self._m_cut_type_str)。
        if self.data[0]['type']== []:
            self.data[0]['type'].append(self._m_cut_type_str)
        else :
            self.data[0]['type'][0] =  self._m_cut_type_str  # 如果不为空,更新当前切割模式。
        # 使用 json.dump 将当前编辑状态(self.data)序列化为 JSON 格式,并保存到指定文件中。
        with open(fileName,'w') as fp:
            json.dump(self.data, fp, indent=2)
        # 调用 setCurrentFile 方法,更新当前文件路径并刷新窗口标题。
        self.setCurrentFile(fileName)
        # 返回 True,表示保存操作成功。
        return True

    def setCurrentFile(self,fileName):
        self.curFile = fileName
        if self.curFile:
            shownName = self.strippedName(self.curFile)  # 如果 self.curFile 不为空,调用 self.strippedName 方法获取文件名(不包含路径)。
        else:
            shownName = 'untitled.json' # 如果为空,设置默认文件名为 'untitled.json'。
        # 更新窗口标题,显示当前文件名和应用程序名称。
        self.setWindowTitle("%s[*] - Application" % shownName)

    def strippedName(self, fullFileName):
        return QtCore.QFileInfo(fullFileName).fileName()  # 使用 QtCore.QFileInfo 获取文件名(不包含路径)。fullFileName 是完整的文件路径,fileName() 方法返回文件名部分。
    
    # 实现自动保存功能
    # 用于触发当前编辑状态的自动保存
    def curSave(self):
        # 从 piecesList 和 picWidget 中获取当前的编辑状态(位置列表、切割小部件列表、位置信息列表),并更新到主窗口类的属性中。
        self.locationList = self.piecesList.locationList
        self.cuttingWidgetList = self.picWidget.cuttingWidgetList
        self.posList = self.picWidget.posList
        # 如果当前文件路径(self.curFile)存在,调用 autoSave 方法,将当前编辑状态保存到自动保存文件中(文件名由 self.autoName 指定)。
        if self.curFile:
            self.autoSave(self.autoName)
    # 具体执行保存操作,包括管理自动保存文件的路径列表、保存当前编辑状态到文件中,以及更新自动保存的元数据。
    def autoSave(self, autoName):
        # 如果自动保存路径列表超过 5 个文件,删除最早的自动保存文件(列表的第一个元素),并从列表中移除该路径。
        if len(self.auto_save_data[0]['autoSavePath']) > 5:
            os.remove(self.auto_save_data[0]['autoSavePath'][0])
            del self.auto_save_data[0]['autoSavePath'][0]
        # 如果新的自动保存文件名(autoName)不在路径列表中,且列表长度小于等于 5,直接将新路径添加到列表中。
        # 如果列表长度超过 5,删除最早的路径并添加新路径。
        if autoName not in self.auto_save_data[0]['autoSavePath']:
            if len(self.auto_save_data[0]['autoSavePath']) <= 5:
                self.auto_save_data[0]['autoSavePath'].append(autoName)
            elif len(self.auto_save_data[0]['autoSavePath']) > 5:
                os.remove(self.auto_save_data[0]['autoSavePath'][0])
                del self.auto_save_data[0]['autoSavePath'][0]
                self.auto_save_data[0]['autoSavePath'].append(autoName)
        
        # 将当前的编辑状态(位置列表、切割小部件列表、位置信息列表)转换为简单的列表格式,便于序列化。
        location = []
        square = []
        pos = []
        for i in range(len(self.locationList)):
            location.append([self.locationList[i].x(), self.locationList[i].y()])
        for j in range(len(self.cuttingWidgetList)):
            square.append([self.cuttingWidgetList[j].x(), self.cuttingWidgetList[j].y(), self.cuttingWidgetList[j].width(), self.cuttingWidgetList[j].height()])
        for k in range(len(self.posList)):
            pos.append([self.posList[k].x(), self.posList[k].y()])

        # 更新 self.data 中的编辑状态数据,包括位置列表、切割小部件列表、位置信息列表和切割模式。 
        self.data[0]['location_list'] = location
        self.data[0]['square_list'] = square
        self.data[0]['pos_list'] = pos
        if self.data[0]['type'] == []:
            self.data[0]['type'].append(self._m_cut_type_str)
        else:
            self.data[0]['type'][0] = self._m_cut_type_str

        # 如果当前文件路径存在,将自动保存路径与当前文件路径关联起来,便于后续恢复。
        if self.curFile:
            self.savepath_autopath_dict[str(self.curFile)]= autoName
            self.auto_save_data[0]['connectDict']= self.savepath_autopath_dict
        # 将当前编辑状态(self.data)序列化为 JSON 格式,并保存到指定的自动保存文件中。
        with open(autoName,'w') as fp:
            json.dump(self.data, fp, indent=2)
        # 将自动保存路径列表和其他元数据(self.auto_save_data)保存到 autosave.json 文件中。
        with open('auto_save/autosave.json','w') as fpp:
            json.dump(self.auto_save_data, fpp, indent=2)

    # 调用 QUndoStack 的 undo() 方法,执行最近一次的撤销操作。
    def undoImage(self):
        self.undoStack.undo()
    # 调用 QUndoStack 的 redo() 方法,执行最近一次的重做操作。
    def redoImage(self):
        self.undoStack.redo()

    def cutImage(self):
        item = self.piecesList.currentItem()  # 从 piecesList(图片片段列表)中获取当前选中的项。
        self.copyImage()  # 调用 copyImage 方法,将当前选中的图片片段复制到剪贴板。
        command =CommandCut(self.piecesList, item)  # 创建一个 CommandCut 对象,这是一个自定义的命令类,用于实现剪切操作的逻辑。
        self.undoStack.push(command)  # 将剪切命令推入 QUndoStack,以便支持撤销操作。

    def copyImage(self):
        item = self.piecesList.currentItem()  # 获取当前选中的图片片段项
        itemData = QtCore.QByteArray()  # 创建一个 QDataStream 对象,用于序列化数据
        dataStream = QtCore.QDataStream(itemData, QtCore.QIODevice.WriteOnly)  # 创建一个 QDataStream 对象,用于序列化数据
        pixmap = QtGui.QPixmap(item.data(QtCore.Qt.UserRole))  # 从选中的项中获取 QPixmap 对象
        location = item.data(QtCore.Qt.UserRole + 1).toPoint()  # 从选中的项中获取位置信息(QPoint)

        print "-----copy=-----"
        print location
        dataStream << pixmap << location  # 将 QPixmap 和位置信息序列化到 QByteArray 中

        mimeData = QtCore.QMimeData()  # 创建一个 QMimeData 对象,用于存储剪贴板数据
        mimeData.setData('image/pic', itemData)  # 将 QByteArray 数据存储到 QMimeData 中,指定 MIME 类型为 'image/pic'

        self.clipBoard =QtGui.QApplication.clipboard()  # 获取系统剪贴板
        self.clipBoard.setMimeData(mimeData)  # 将 QMimeData 设置到剪贴板

    def pasteImage(self):
        picData=self.clipBoard.mimeData().data('image/pic')  # 从剪贴板获取 MIME 类型为 'image/pic' 的数据
        dataStream = QtCore.QDataStream(picData, QtCore.QIODevice.ReadOnly)  # 创建一个 QDataStream 对象,用于反序列化数据
        pixmap = QtGui.QPixmap()  # 创建一个 QPixmap 对象,用于存储图片
        location = QtCore.QPoint()  # 创建一个 QPoint 对象,用于存储位置信息
        dataStream >> pixmap >> location  # 从数据流中反序列化 QPixmap 和 QPoint

        command =CommandPaste(self.piecesList, pixmap, location)  # 创建一个粘贴命令对象
        self.undoStack.push(command)  # 将粘贴命令推入撤销栈

    def deleteImage(self):
        item = self.piecesList.currentItem()  # 获取当前选中的图片片段项
        
        command = CommandDelete(self.piecesList, item)  # 创建一个删除命令对象
        self.undoStack.push(command)  # 将删除命令推入撤销栈

    # 将图片缩放到固定大小:400*400px, 然后进行切割再保存到图片片段列表中去
    def setupPic(self, xx, yy):
        # 将图片缩放到固定大小:400*400px
        size = min(self.image.width(), self.image.height())
        self.image = self.image.copy((self.image.width() - size)/2, (self.image.height() - size)/2, size, size).scaled(400, 400, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
        # 清空 piecesList,移除所有之前的图片片段。
        self.piecesList.clear()
        # 然后进行切割再保存到图片片段列表中去
        for x in range(xx):
           for y in range(yy):
               pieceImage = self.image.copy(x*400/self.xx, y*400/self.yy, 400/self.xx, 400/self.yy)
               self.location_pixmap_dict[(x + 1, y + 1)]= pieceImage
               self.piecesList.addPiece(pieceImage, QtCore.QPoint(x + 1, y + 1))

    # 初始化主窗口的布局和组件
    def setWidgets(self):
        frame =QtGui.QFrame()  # 创建一个框架组件
        frameLayout =QtGui.QHBoxLayout(frame)  # 创建一个水平布局管理器

        self.piecesList =PicturesList()  # 创建一个图片片段列表组件
        self.picWidget=PictureWidget()  # 创建一个图片编辑区域组件

        frameLayout.addWidget(self.piecesList)  # 将图片片段列表组件添加到布局中
        frameLayout.addWidget(self.picWidget)  # 将图片编辑区域组件添加到布局中
        self.setCentralWidget(frame)  # 将框架组件设置为主窗口的中心部件

    # 处理窗口关闭事件(退出游戏)
    def closeEvent(self, event):
        reply = QtGui.QMessageBox.question(self, u'退出游戏', u"是否退出游戏?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)  # 使用 QtGui.QMessageBox.question 弹出一个消息框,提示用户是否确定退出。reply 变量存储用户的选择结果。
        if reply == QtGui.QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()

    # 窗口居中显示
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()  # 获取屏幕的几何信息
        size = self.geometry()  # 获取窗口的几何信息
        self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2)  # 将窗口移动到屏幕中央

# PicturesList 类,继承自 QtGui.QListWidget。它用于显示和管理图片片段的列表,并支持拖拽操作。
class PicturesList(QtGui.QListWidget):
    upSignal = QtCore.pyqtSignal()  # 自定义信号
    downSignal =QtCore.pyqtSignal()
    saveSignal = QtCore.pyqtSignal()

    def __init__(self, parent=None):
        super(PicturesList,self).__init__(parent)  # 调用父类的构造函数

        self.setDragEnabled(True)  # 启用拖拽功能
        self.setViewMode(QtGui.QListView.IconMode)  # 设置视图模式为图标模式
        self.setIconSize(QtCore.QSize(60, 60))  # 设置图标大小为60x60 像素
        self.setSpacing(10)  # 设置图标之间的间距为 10 像素
        self.setAcceptDrops(True)  # 启用接受拖拽操作
        self.setDropIndicatorShown(True)  # 显示拖拽指示器
        
        self.pixmapList =[]  # 存储图片片段(存储切图)
        self.locationList=[]  # 存储位置信息
    
    # 添加图片到左侧的资源列表中
    def addPiece(self,pixmap, location):
        self.pixmapList.append(pixmap)  # 将图片片段添加到 pixmapList 中
        self.locationList.append(location)  # 将位置信息添加到 locationList 中
        pieceItem = QtGui.QListWidgetItem(self)  # 创建一个 QListWidgetItem 对象
        pieceItem.setIcon(QtGui.QIcon(pixmap))  # 使用 QIcon 将传入的 pixmap 设置为列表项的图标。
        pieceItem.setData(QtCore.Qt.UserRole, pixmap)  # 将 QPixmap 对象存储为用户数据
        pieceItem.setData(QtCore.Qt.UserRole +1, location)  # 将位置信息存储为用户数据
        pieceItem.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled)  # 设置列表项的标志,使其可启用、可选择、可拖拽
        # 可启用(ItemIsEnabled):用户可以与之交互。
        # 可选择(ItemIsSelectable):用户可以选中该条目。
        # 可拖拽(ItemIsDragEnabled):用户可以拖拽该条目。

    def delete_piece(self, item):
        self.takeItem(self.row(item))  # 从 PicturesList 中移除指定的图片片段。

    def startDrag(self, supportedActions):
        item = self.currentItem()  # 获取QListWidget当前选中的 QListWidgetItem 对象。

        itemData = QtCore.QByteArray()  # 创建一个 QByteArray 对象,用于存储序列化后的数据。
        dataStream = QtCore.QDataStream(itemData, QtCore.QIODevice.WriteOnly)  # 创建一个 QDataStream 对象,用于将数据写入 QByteArray。
        # 从列表项中获取存储的 QPixmap 对象和位置信息(QPoint)。
        self.pixmap = QtGui.QPixmap(item.data(QtCore.Qt.UserRole))
        self.location = item.data(QtCore.Qt.UserRole + 1).toPoint()
        # 使用 QDataStream 将 QPixmap 和 QPoint 序列化到 QByteArray 中。
        dataStream << self.pixmap << self.location

        # 创建一个 QMimeData 对象,用于存储拖拽数据。
        mimeData = QtCore.QMimeData()
        mimeData.setData('image/pic', itemData)  # 将序列化后的数据存储到 QMimeData 中,指定 MIME 类型为 'image/pic'。
        # 创建一个 QDrag 对象,用于处理拖拽操作。
        # 并设置拖拽对象的 MIME 数据、热点位置(拖拽的起始点)和拖拽时显示的图标。
        drag = QtGui.QDrag(self)
        drag.setMimeData(mimeData)
        drag.setHotSpot(QtCore.QPoint(self.pixmap.width()/2, self.pixmap.height()/2))
        drag.setPixmap(self.pixmap)
        # 发射 upSignal 信号,通知其他组件拖拽操作开始。
        self.upSignal.emit()
        # 调用 drag.exec_() 方法,执行拖拽操作,支持移动操作(Qt.MoveAction)。如果拖拽操作成功完成,从列表中移除当前选中的项。
        if drag.exec_(QtCore.Qt.MoveAction) == QtCore.Qt.MoveAction:
            self.takeItem(self.row(item))

    def dragEnterEvent(self,event):
        # 如果拖拽数据包含 'image/pic' 格式,调用 event.accept(),表示接受拖拽操作。否则,调用 event.ignore(),忽略拖拽操作。
        if event.mimeData().hasFormat('image/pic'):  # 检查拖拽事件的 MIME 数据是否包含 'image/pic' 格式。
            event.accept()
        else:  
            event.ignore()

    def dragMoveEvent(self, event):
        # 检查拖拽事件的 MIME 数据是否包含 'image/pic' 格式。如果数据格式匹配,设置拖拽操作类型为 MoveAction,表示允许移动操作。调用 event.accept(),表示接受拖拽操作。
        if event.mimeData().hasFormat('image/pic'):
            event.setDropAction(QtCore.Qt.MoveAction)
            event.accept()
        else:
            event.ignore()  # 忽略不支持的拖拽事件
    # 用于响应拖拽释放事件。通过这个方法,PicturesList 组件可以处理拖拽释放时的数据接收,并通知其他组件拖拽操作完成。
    def dropEvent(self,event):
        if event.mimeData().hasFormat('image/pic'):
            picData = event.mimeData().data('image/pic')  # 获取 MIME 数据中的图片数据
            dataStream = QtCore.QDataStream(picData,QtCore.QIODevice.ReadOnly)  # 创建一个 QDataStream 对象,用于反序列化数据
            self.pixmap = QtGui.QPixmap()  # 创建一个 QPixmap 对象,用于存储图片
            self.location = QtCore.QPoint()  # 创建一个 QPoint 对象,用于存储位置信息
            dataStream >> self.pixmap >> self.location  # 从数据流中反序列化 QPixmap 和 QPoint

            self.downSignal.emit()  # 发射 downSignal 信号,通知其他组件拖拽操作完成

            event.setDropAction(QtCore.Qt.MoveAction)  # 设置拖拽操作类型为 MoveAction
            event.accept()  # 接受拖拽操作
        else :
            event.ignore()  # 忽略不支持的拖拽事件

# PictureWidget 类,继承自 QtGui.QWidget。它用于显示和管理图片编辑区域,并支持拖拽操作。
class PictureWidget(QtGui.QWidget):
    upSignal = QtCore.pyqtSignal()  # 自定义信号
    downSignal = QtCore.pyqtSignal()
    saveSignal = QtCore.pyqtSignal()

    def __init__(self,parent=None):
        super(PictureWidget, self).__init__(parent)

        self.setMinimumSize(640, 670)  # 设置最小尺寸
        self.setMaximumSize(640, 670)  # 设置最大尺寸
        
        self.highlightedRect = QtCore.QRect()  # 创建一个 QRect 对象,用于表示高亮显示的矩形区域。初始值为空矩形。
        self.setAcceptDrops(True)  # 启用组件接受拖拽操作,允许用户将图片片段拖拽到该区域。
        
        self.cuttingWidgetList = []  # 初始化切割小部件列表
        self.pixmapList = []  # 初始化图片片段列表
        self.posList = []  # 初始化位置信息列表

        self.drop_success_flag = True  # 初始化一个布尔标志,用于指示拖拽操作是否成功完成。

    # 处理拖拽进入事件和拖拽离开事件
    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('image/pic'):
            event.accept()
        else:
            event.ignore()

    def dragLeaveEvent(self, event):
        updateRect =self.highlightedRect  # 获取当前高亮显示的矩形区域
        self.highlightedRect = QtCore.QRect()  # 清空高亮显示的矩形区域
        self.update(updateRect)  # 更新组件,重新绘制高亮区域
        event.accept()  # 接受拖拽离开事件
    
    # 处理拖拽移动事件和拖拽释放事件
    def dragMoveEvent(self, event):
        updateRect = self.highlightedRect.unite(self.targetSquare(event.pos()))  # 使用 unite 方法计算当前高亮矩形区域和目标矩形区域的联合区域,确保更新整个相关区域。

        if event.mimeData().hasFormat('image/pic'):  # 检查拖拽事件的 MIME 数据是否包含 'image/pic' 格式。
            self.highlightedRect = self.targetSquare(event.pos())  # 如果数据格式匹配,调用 self.targetSquare(event.pos()) 获取目标矩形区域,并将其设置为高亮显示区域。
            event.setDropAction(QtCore.Qt.MoveAction)  # 设置拖拽操作类型为 MoveAction,表示允许移动操作。
            event.accept()  # 调用 event.accept(),表示接受拖拽操作。
        else:
            self.highlightedRect =QtCore.QRect()  # 如果数据格式不匹配,清空高亮显示的矩形区域,并调用 event.ignore(),忽略拖拽操作。
            event.ignore()

        self.update(updateRect)  # 调用 self.update(updateRect),重新绘制需要更新的矩形区域。

    def dropEvent(self,event):
        if event.mimeData().hasFormat('image/pic'):  # 检查拖拽事件的 MIME 数据是否包含 'image/pic' 格式。
            picData = event.mimeData().data('image/pic')  # 从 MIME 数据中获取 'image/pic' 格式的数据。
            dataStream = QtCore.QDataStream(picData, QtCore.QIODevice.ReadOnly)  # 创建一个 QDataStream 对象,用于从数据中反序列化 QPixmap 和 QPoint。
            self.square = self.targetSquare(event.pos())  # 调用 self.targetSquare(event.pos()) 获取目标矩形区域。
            self.pixmap = QtGui.QPixmap()
            self.location = QtCore.QPoint()
            dataStream >> self.pixmap >> self.location

            self.hightlightedRect = QtCore.QRect()  # 清空高亮显示的矩形区域。

            self.downSignal.emit()  # 发射 downSignal 信号,通知其他组件拖拽操作完成。

            event.setDropAction(QtCore.Qt.MoveAction)  # 设置拖拽操作类型为 MoveAction。 
            event.accept()  # 调用 event.accept(),表示接受拖拽操作。
        else:
            self.hightlightedRect = QtCore.QRect()  # 如果数据格式不匹配,清空高亮显示的矩形区域
            event.ignore()
    
    # 查找特定的切割小部件(矩形区域): 使用 index 方法在 cuttingWidgetList 中查找 pieceRect 的索引。如果找到,返回其索引值。
    def findPiece(self, pieceRect):
        try:
            return self.cuttingWidgetList.index(pieceRect)
        except ValueError:
            return -1  # 如果 pieceRect 未在 cuttingWidgetList 中找到,捕获 ValueError 异常并返回 -1。
    
    # 处理鼠标按下事件,从而实现拖拽操作的开始。
    def mousePressEvent(self, event):
        self.square = self.targetSquare(event.pos())  # 获取鼠标点击位置对应的目标矩形区域
        found = self.findPiece(self.square)  # 查找该矩形区域在 cuttingWidgetList 中的索引

        if found == -1:
            return  # 如果未找到,直接返回

        self.location = self.posList[found]  # 获取该矩形区域的位置信息
        self.pixmap = self.pixmapList[found]  # 获取该矩形区域的图片片段

        # del self.posList[found]
        # del self.pixmapList[found]
        # del self.cuttingWidgetList[found]

        self.upSignal.emit()  # 发射 upSignal 信号,通知其他组件拖拽操作开始

        itemData = QtCore.QByteArray()  # 创建一个 QByteArray 对象,用于存储数据
        dataStream = QtCore.QDataStream(itemData, QtCore.QIODevice.WriteOnly)  # 创建一个 QDataStream 对象,用于序列化数据
        dataStream << self.pixmap << self.location  # 将 QPixmap 和 QPoint 序列化到 QByteArray 中

        mimeData = QtCore.QMimeData()  # 创建一个 QMimeData 对象,用于存储 MIME 类型的数据
        mimeData.setData('image/pic', itemData)  # 将 QByteArray 数据存储到 QMimeData 中,指定 MIME 类型为 'image/pic'

        drag = QtGui.QDrag(self)  # 创建一个 QDrag 对象,用于处理拖拽操作
        drag.setMimeData(mimeData)  # 将 QMimeData 设置到拖拽对象中
        drag.setHotSpot(event.pos() - self.square.topLeft())  # 设置拖拽的热点位置
        drag.setPixmap(self .pixmap)  # 设置拖拽时显示的图标

	if drag.exec_(QtCore.Qt.MoveAction) != QtCore.Qt.MoveAction:
	    self.posList.insert(found, self.location)
	    self.pixmapList.insert(found, self.pixmap)
	    self.cuttingWidgetList.insert(found, self.square)
	    self.update(self.square)

    def paintEvent(self, event):
        painter = QtGui.QPainter()  # 创建一个 QPainter 对象,用于绘制图形。
        painter.begin(self)  # 调用 begin(self) 方法,指定绘制目标为当前组件。
        painter.fillRect(event.rect(), QtCore.Qt.white)  # 使用 fillRect 方法,将整个组件区域填充为白色。

        if self.highlightedRect.isValid():  # 检查 highlightedRect 是否有效。
            painter.setBrush(QtGui.QColor("#ffcccc"))  # 如果有效,设置画刷颜色为浅红色(#ffcccc),画笔为无边框。
            painter.setPen(QtCore.Qt.NoPen)
            painter.drawRect(self.highlightedRect.adjusted(0, 0, -1, -1))  # 使用 drawRect 方法绘制高亮矩形区域,adjusted(0, 0, -1, -1) 用于调整矩形的大小,使其不包含边界。

        # 使用 zip 函数同时遍历 cuttingWidgetList 和 pixmapList。在每个矩形区域内绘制对应的图片片段。
        for rect, pixmap in zip(self.cuttingWidgetList, self.pixmapList):
            painter.drawPixmap(rect, pixmap)

        painter.end() # 调用 end() 方法,结束绘制。
    
    # targetSquare 方法用于计算鼠标位置对应的目标矩形区域, 该方法将鼠标位置对齐到 80x80 的网格上,确保拖拽操作的目标区域是固定的网格单元。
    def targetSquare(self, position):
        # 创建一个 QRect 对象,表示目标矩形区域。矩形的左上角坐标为对齐后的坐标,宽度和高度均为 80 像素。
        return QtCore.QRect(position.x() // 80 * 80, position.y() // 80 * 80, 80, 80)

# 自定义不同情况的撤销命令类,支持撤销和重做操作。
# 自定义的撤销命令类 CommandCut,继承自 QtGui.QUndoCommand
class CommandCut(QtGui.QUndoCommand):
    def __init__(self, piecesList, item):
        super(CommandCut, self).__init__()  # 调用父类的构造函数
        self.piecesList = piecesList  # 存储图片片段列表组件
        self.item = item  # 存储当前操作的图片片段项
        self.row = self.piecesList.row(self.item)  # 获取该项在列表中的行号

    def redo(self):
        self.piecesList.takeItem(self.row)  # 从列表中移除该项
        self.piecesList.saveSignal.emit()  # 发射 saveSignal 信号,通知其他组件保存状态

    def undo(self):
        self.piecesList.insertItem(self.row, self.item)  # 将该项重新插入到列表中
        self.piecesList.saveSignal.emit()  # 发射 saveSignal 信号,通知其他组件保存状态

class CommandPaste(QtGui.QUndoCommand):
    def __init__(self,piecesList, pixmap, location):
        super(CommandPaste,self).__init__()
        self.piecesList = piecesList
        self.pixmap = pixmap
        self.location =location

    def redo(self):
        self.piecesList.addPiece(self.pixmap, self.location)
        self.piecesList.saveSignal.emit()

    def undo(self):
        count = self.piecesList.count()
        self.piecesList.takeItem(self.piecesList.row(self.piecesList.item(count - 1)))
        self.piecesList.saveSignal.emit()

class CommandDragToWidget(QtGui.QUndoCommand):
    def __init__(self, widget):
        super(CommandDragToWidget, self).__init__()
        self.widget = widget
        self.square = self.widget.square
        self.pixmap = self.widget.pixmap
        self.location =self.widget.location

    def redo(self):
        self.widget.cuttingWidgetList.append(self.square)
        self.widget.pixmapList.append(self.pixmap)
        self.widget.posList.append(self.location)
        self.widget.update(self.square)
        self.widget.saveSignal.emit()

    def undo(self):
        found = self.widget.findPiece(self.square)
        if found == -1:
            return

        del self.widget.posList[found]
        del self.widget.pixmapList[found]
        del self.widget.cuttingWidgetList[found]
        self.widget.highlightedRect = QtCore.QRect()
        self.widget.update(self.square)
        self.widget.saveSignal.emit()

class CommandDragToListWidget(QtGui.QUndoCommand):
    def __init__(self, lstWidget):
        super(CommandDragToListWidget, self).__init__()
        self.lstWidget = lstWidget
        self.pixmap = self.lstWidget.pixmap
        self.location = self.lstWidget.location

    def redo(self):
        self.lstWidget.addPiece(self.pixmap, self.location)
        self.lstWidget.saveSignal.emit()

    def undo(self):
        count = self.lstWidget.count()
        self.lstWidget.takeItem(self.lstWidget.row(self.lstWidget.item(count - 1)))
        self.lstWidget.saveSignal.emit()

class CommandDragFromWidget(QtGui.QUndoCommand):
    def __init__(self,widget):
        super(CommandDragFromWidget, self).__init__()
        self.widget = widget
        self.square = self.widget.square
        self.pixmap = self.widget.pixmap
        self.location = self.widget.location
        self.found = self.widget.findPiece(self.square)

    def redo(self):
        if self.found == -1:
            return

        del self.widget.posList[self.found]
        del self.widget.pixmapList[self.found]
        del self.widget.cuttingWidgetList[self.found]
        self.widget.highlightedRect = QtCore.QRect()
        self.widget.update(self.square)
        self.widget.saveSignal.emit()

    def undo(self):
        self.widget.posList.insert(self.found, self.location)
        self .widget.pixmapList.insert(self.found, self.pixmap)
        self.widget.cuttingWidgetList.insert(self.found, self.square)
        self.widget.update(self.square)
        self.widget.saveSignal.emit()

class CommandDragFromListWidget(QtGui.QUndoCommand):
    def __init__(self, lstWidget, item, row):
        super(CommandDragFromListWidget, self).__init__()
        self.row = row
        self.lstWidget = lstWidget
        self.item = item

    def redo(self):
        tmp = self.lstWidget.item(self.row)
        tmp_location = None
        tmp_index = None
        for i in self.lstWidget.locationList:
            if i.x() == tmp.data(QtCore.Qt.UserRole + 1).toPoint().x() and i.y() == tmp.data(QtCore.Qt.UserRole + 1).toPoint().y():
                tmp_index = self.lstWidget.locationList.index(i)
                tmp_location = i
                break
        if tmp_index != None and tmp_location != None:
            self.lstWidget.pixmapList.pop(tmp_index)
            self.lstWidget.locationList.remove(tmp_location)
        self.lstWidget.takeItem(self.row)
        self.lstWidget.saveSignal.emit()

    def undo(self):
        self.lstWidget.insertItem(self.row, self.item)
        self.lstWidget.saveSignal.emit()

# 自定义的撤销命令类 CommandDelete,继承自 QtGui.QUndoCommand。这个类用于实现"删除"操作的撤销和重做功能。
class CommandDelete(QtGui.QUndoCommand):
    def __init__(self, lstWidget, item):
        super(CommandDelete, self).__init__()  # 调用父类的构造函数
        self.lstWidget = lstWidget  # 存储图片片段列表组件
        self.item = item  # 存储当前操作的图片片段项
        self.row = self.lstWidget.row(self.item)  # 获取该项在列表中的行号

    def redo(self):
        self.lstWidget.delete_piece(self.item)  # 从列表中删除该项
        self.lstWidget.saveSignal.emit()  # 发射 saveSignal 信号,通知其他组件保存状态

    def undo(self):
        self.lstWidget.insertItem(self.row, self.item)  # 将该项重新插入到列表中
        self.lstWidget.saveSignal.emit()  # 发射 saveSignal 信号,通知其他组件保存状态

if __name__ == '__main__':  # 如果是主程序运行,执行以下代码;如果是被导入为模块,则跳过以下代码。
    app = QtGui.QApplication(sys.argv)  # QtGui.QApplication: PyQt 的应用程序类,负责管理应用程序的资源,如窗口、图标、菜单等。
    ex = MainWindow()  # 创建一个 MainWindow 类的实例,MainWindow 是自定义的主窗口类,继承自 QtGui.QMainWindow。
    ex.show()  # 调用 show() 方法,将主窗口显示在屏幕上。
    app.exec_()  # 调用 exec_() 方法,启动应用程序的事件循环。

------------------------END-------------------------

才疏学浅,谬误难免,欢迎各位批评指正。

相关推荐
m0_617493941 小时前
Python OpenCV 透视变换(Perspective Transform)详解与实战
开发语言·python·opencv
小李不困还能学1 小时前
PyCharm下载安装与配置教程
ide·python·pycharm
博观而约取厚积而薄发1 小时前
Pytest 从入门到精通,一篇就够(超详细实战教程)
python·测试工具·单元测试·自动化·pytest
imzed1 小时前
使用 Playwright + Pytest 构建 Web UI 自动化测试框架
python·自动化·pytest
普通网友1 小时前
pytest一些常见的插件
开发语言·python·pytest
时空系1 小时前
Python 高性能高压缩打包器 —— 基于 JianPy 语义分析引擎
python
cndes2 小时前
给Miniconda换源,让包下载更迅速
开发语言·python
Metaphor6922 小时前
使用 Python 添加、隐藏和删除 PDF 图层
python·pdf·图层
丨白色风车丨2 小时前
【Python 计算机视觉】基于 Dlib+OpenCV 实现实时人眼疲劳检测(闭眼预警)
python·opencv·计算机视觉