计算机系统应用教程网站

网站首页 > 技术文章 正文

实战PyQt5: 069-MV框架中的项视图拖放功能

btikc 2024-10-20 05:04:15 技术文章 7 ℃ 0 评论


模型-视图框架完全支持Qt的基本拖放操作,列表、树形和表格部件中的项可以在视图间拖动,数据可以以MIME类型的格式进行导入和导出

Qt提供的标准视图自动支持在视图内部的拖放,其中的项可以被移动以改变显示顺序。在默认情况下,这些视图是不能进行拖放操作的,如果要使用拖放功能,除了项本身也是允许拖放,还需要开启视图的一些属性。

使用项视图部件

QListWidget,QTabWidget和QTreeWidget每一种类型的项都默认配置了一套不同的标识。比如,QListWidgetItem或QTreeWidgetItem的初始标识是启用了enabled, 可复选checkable,可选择selectbale这些标识,同时它们也可以作为拖放操作的数据源;QTableWidgetItem则可以进行编辑操作,并且可以作为拖放操作的目标。

Qt中提供的所有标准项都有一个或两个标志用于拖放操作,为了利用内置的拖放支持,需要在视图中设置相应的属性。

要启动对项的拖动功能,需要把视图的dragEnabled属性设定为True。

要实现将内部或外部项拖放到视图中,需要把视图的viewport()的acceptDrops属性设定为True。

要显示当前拖动的项将被放置在什么地方,则需要设置视图的showDropIndicator属性,它会提供关于项在视图中的持续位置更新信息。

例如,可以用下面的代码在列表部件中启用拖放功能。

         listWidget=QListWidget(self)
         listWidget.setSelectionMode(QAbstractItemView.SingleSelection)
         listWidget.setDragEnabled(True)
         listWidget.viewport().setAcceptDrops(True)
         listWidget.setDropIndicatorShown(True)

这样就可以得到一个可以让项在视图里进行复制的列表部件,它甚至可以让用户在包含相同类型数据的视图间拖动项。注意,在这两种情况下,项是本复制而不是被移动。

如果用户要在视图间移动项,那就要设定列表部件的拖放模式dragDropMode。

         listWidget.setDragDropMode(QAbstractItemView.InternalMove)

使用Model-View框架类

建立一个支持拖放的视图和跟使用项视图部件的方式是一样的,比如,可以用建立QListWidget的方法建立一个QListView视图:

         listView=QListView(self)
         listView.setSelectionMode(QAbstractItemView.ExtendedSelection)
         listView.setDragEnabled(True)
         listView.setAcceptDrops(True)
         listView.setDropIndicatorShown(True)

因为视图所显示的数据由模型来控制存取,所以模型类也要提供对拖放操作的支持。模型类多拖放动作的支持可以通过重新实现QAbstractItemModel.supportedDropActions()函数来指定。下面我们以自定义类DragDropListModel(QStringListModel)来演示拖放功能。

以下的代码实现复制和移动的操作:

         def supportedDropActions(self):
                   return Qt.CopyAction | Qt.MoveAction

虽然可是设定Qt.DropActions里的值的任意组合,但是还是需要重新实现模型中的一些函数才能完成需要支持的功能。例如,为了让一个列表模型能正确地支持Qt.MoveAction动作,不管是直接还是间接从其基类继承,模型必须重新实现QAbstractItemModel.remove()函数。

启动对项的拖放功能

通过重新实现QAbstractItemModel.flags()函数来提供合适的标识,指示模型视图哪些项可以拖动,哪些项可以接收项。例如,通过在返回的标识中包含Qt.ItemsDragEnabled 和Qt.ItemsDropEnabled, 一个基于QAbstractListModel的列表模型就可以使每个项都可以拖放:

         def flags(self, index):
                   defaultFlags=QStringListModel.flags(self, index)
                   if index.isValid():
                            return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultsFlags
                   else:
                            return Qt.ItemIsDropEnabled | defaultFlags
注意,项可以放置在模型的顶层,而拖动操作只对合法项有效。

编码导出数据

当通过拖放操作从模型中导出数据项时,会将它们编码成与一种或多种MIME类型相对应的适当格式。通过重新实现回标准MIME类型的列表的QAbstractItemModel .mimeTypes()函数,来声明可用供项使用的MIME类型。例如,一个仅提供纯文本的模型需提供以下实现:

         def mimeTypes(self):
                   return ['application/vnd.text.list']

模型同时还需要提供对公开格式的数据进行编码的代码实现,这可以通过重新实现QAbstractItemModel.mimeData()函数来提供。

以下代码实现将索引参数相关的项数据编码成纯文本并存储在QMiMeData对象中。

         def mimeData(self, indexes):
                   mmData =QMimeData()
                   encodedData=QByteArray()
                   stream =QDataStream(encodeData, QIODevice.WriteOnly)
                  for index in indexes:
                            if index.isValid():
                                     text =self.data(index, Qt.DisplayRole)
                                     stream.writeQString(str(text))
                   mmData.setData('application/vnd.text.list', ecodedData)
                   return mmData

注意:自定义的数据类型必须申明为meta objects,并且要为其实现流操作。

将释放的数据插入到模型中

模型处理释放的数据的方法依赖于的类型(列表,表格或树形)以及它向用户显示其内容的方法。通常情况下,最适合模型底层数据存储的方法就是处理释放的数据的方法。

不同类型的模型会用不同的方法处理释放的数据。列表和表格模型只提供一个存储项的平面结构。因此,当数据被释放到视图中的一个现有的项上面时,它们可以插入新的行(和列),或者使用提供的数据覆盖掉模型里项的内容。树形模型一般是向他们的底层数据增加包含新数据的子项。

通过重新实现模型的QAbstractItemModel::dropMimeData()函数来实现对释放数据的处理。例如,一个处理简单字符串列表的模型可以提供一种实现,该实现将处理放入现有项目的数据与放入模型顶层(即无效项目)的数据分开处理。

通过重新实现QAbstractItemModel :: canDropMimeData(),模型可以禁止删除某些项目,或者取决于删除的数据。该模型首先必须确保应该执行操作,所提供的数据采用可以使用的格式,并且其在模型中的目的地是有效的:

         def canDropMimeData(self, data, action, row, column, parent):
                   if data.hasFormat('application/vnd.text.list') is False:
                            return False
                   if column > 0:
                            return False
                   return True
 
         def dropMimeData(self, data, action, row, column, parent):
                   if self.canDropMimeData(data,action,row,column,parent) is False
                            return False
                   if action == Qt.IgnoreAction:
                            return True

如果提供的数据不是纯文本,或者给出的用于放置的列号是无效的,那么这个字符串列表模型可以将此操作标志为失败。

根据数据是否被放置在一个现有的项上面作为判断,插入模型的数据将做不同的处理。在这个例子中,我们允许把数据放在现有项之间,列表的第一个项之前,以及在列表的最后一个项之后。

当一个放下操作发生时,如果父项相对应的模型索引是有效的,意味着放下操作发生在一个项上面,如果是无效的,则意味着放下操作发生在视图中对应于模型顶层的某个位置。

                   beginRow =-1
                   if row != -1:
                            beginRow =row

先检查指定的行号看它是否可以用来将项插入到模型中,不管父项的索引是否有效:

                   elif parent.isValid():
                            beginRow=parent.row()

如果父项索引是有的,则放下操作发生在一个项上。在本列表模型中,我们找出项的行号,并用这个值把放下的项插入到模型的顶层。

                   else:
                            beginRow =self.rowCount(QModelIndex())

当放下动作发生在视图的某个位置,同时行号又是不可用的,那我们就把项添加在模型的顶层项。在层次结构模型中,当放下动作发生在一个项上时,插入的项作为该项的子项插入到模型中。

解码导入的数据

对dropMimeData()实现的同时也必须对数据进行解码, 并把它插入到模型的底层数据结构中。在本文模型中,编码后的项可以被解码并加入到一个字符串列表中。

         encodedData = data.data('application/vnd.text.list')
         stream = QDataStream (encodeData, QIODevice.ReadOnly)
         newItems =[]
         rows = 0
         while stream.atEnd() is False:
                   text = stream.readQString()
                   newItem.append(str(text)
                   rows += 1

字符串就可以插入到底层数据。为了保持一致性,可以通过模型自己的接口实现:

         self.insertRows(beginRow, rows, QModelIndex())
         for text in newItems:
                   idx = self.index(beginRow, 0, QModelIndex())
                   self.setData(idx, text)
                   beginRow += 1
         return True

注意,模型通常要提供 QAbstractItemModel::insertRows()函数和 QAbstractItemModel::setData()函数的实现。

演示代码

建立演示文件dragdropdemo.py完整代码如下:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QStringListModel,QModelIndex,
                          QMimeData,QByteArray, QDataStream, QIODevice)
from PyQt5.QtWidgets import (QApplication, QMainWindow, QListView, QAbstractItemView)
 
class DragDropListModel(QStringListModel):
    def __init__(self, parent=None):
        super(DragDropListModel, self).__init__(parent)
        
    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction
    
    def flags(self, index):
        defaultFlags = QStringListModel.flags(self,index)
        
        if index.isValid():
            return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags
        
    def mimeTypes(self):
        return ['application/vnd.text.list']
    
    def mimeData(self, indexes):
        mmData = QMimeData()
        encodedData = QByteArray()
        stream = QDataStream(encodedData, QIODevice.WriteOnly)
        
        for index in indexes:
            if index.isValid():
                text = self.data(index, Qt.DisplayRole)
                stream.writeQString(str(text))
                
        mmData.setData('application/vnd.text.list', encodedData)
        return mmData
    
    def canDropMimeData(self, data, action, row, column, parent):
        if data.hasFormat('application/vnd.text.list') is False:
            return False
        if column > 0:
            return False       
        return True
    
    def dropMimeData(self, data, action, row, column, parent):
        if self.canDropMimeData(data, action, row, column, parent) is False:
            return False
        
        if action == Qt.IgnoreAction:
            return True
        
        beginRow = -1
        if row != -1:
            beginRow = row
        elif parent.isValid():
            beginRow = parent.row()
        else:
            beginRow = self.rowCount(QModelIndex())
            
        encodedData = data.data('application/vnd.text.list')
        stream = QDataStream(encodedData, QIODevice.ReadOnly)
        newItems=[]
        rows = 0
        
        while stream.atEnd() is False:
            text = stream.readQString()
            newItems.append(str(text))
            rows += 1
            
        self.insertRows(beginRow, rows, QModelIndex())
        for text in newItems:
            idx = self.index(beginRow, 0, QModelIndex())
            self.setData(idx, text)
            beginRow += 1
            
        return True
            
 
class DemoDragDrop(QMainWindow):
    def __init__(self, parent=None):
        super(DemoDragDrop, self).__init__(parent)   
        
         # 设置窗口标题
        self.setWindowTitle('实战PyQt5: Model-View框架 拖放 演示')      
        # 设置窗口大小
        self.resize(480, 320)
      
        self.initUi()
        
    def initUi(self):
        listView = QListView(self)
        listView.setSelectionMode(QAbstractItemView.ExtendedSelection)
        listView.setDragEnabled(True)
        listView.setAcceptDrops(True)
        listView.setDropIndicatorShown(True)
        
        ddm = DragDropListModel()
        ddm.setStringList(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
        listView.setModel(ddm)
        
        self.setCentralWidget(listView)
    
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = DemoDragDrop()
    window.show()
    sys.exit(app.exec())  

运行结果如下图:

本文知识点

  • 启动MV框架视图类的拖放功能;
  • 设置项的拖放操作;
  • 编码导出数据;
  • 将数据插入到释放的位置;
  • 解码导入数据。

喜欢本文内容就关注, 收藏,点赞,评论和转发。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表