Qt学习笔记-----拖放

时间:2022-07-03 10:51:00

拖放,拖动和释放(Drag and Drop),允许用户利用鼠标在不同组件之间或一个组件上进行数据的拖动。如果哪个组件支持拖放操作,就在该组件的设计上实现某些必要的函数。比如说,我们可能需要在MainWindow这个主窗口上实现拖放,那就在这个类中实现一些函数。如果我们自定义了一个组件,比如说定义了一个类MyDnDWidget继承自QWidget,我们就在MyDndWidget类中实现特定的函数来支持拖放。

Drag and Drop Types

QMimeData类是Qt内部的一个容器,它记录着大量拥有Mime Type(元类型)的数据。可以把它当成是一个map,每一个元素都是”Mime Type, Mime Data”这样的键值对,但是使用的时候不要像map一样使用,只是用于理解。

拖放操作不只局限在文本和图像上,任何类型的信息在拖放操作中都能被转换。为了在应用之间拖动信息,这些应用彼此都应该知道他们允许接收哪种数据格式,同时也应该知道他们可以产生哪种数据格式。这个是由MIME Types实现的。

被构造的QDrag对象包含MIME Types的列表,它能使用这些去表现数据,同时接收拖动数据的那个窗体使用这些MIME Types中的一个去访问数据。Mime Type可以是Qt中提供的,也可以是用户自定义的,如果需要自定义一个Mime Type,可以重新实现mimeTypes()函数:

mimeTypes

QStringList QAbstractItemModel::mimeTypes() const

这个函数返回允许的Mime Types列表。默认情况下,内置的models和views使用内部的Mime Type,比如text/uri-list。
当重新实现一个支持拖放操作的model时,如果设计者想要返回的数据格式不同于内部的Mime Type,那就需要重新实现这个函数来返回自定义的Mime Types列表。通常的实现形式为:

QStringList types;
types << /* the custom mime types */;
return types;

如果在自定义model中重新实现了这个函数,那么同时也需要重新实现mimeData()和dropMimeData()函数:

mimeData

QMimeData *QAbstractItemModel::mimeData(const QModelIndexList &indexes) const

这个函数返回一个QMimeData *类型的对象,这个对象包含序列化的数据元素,而那些数据元素存储在形参indexes中。换句话说,就是将模型索引列表indexes中的数据序列化,然后存储在一个QMimeData中,最后返回这个QMimeData指针。序列化的方法是使用QByteArray和QDataStream两个类。通常的实现格式为:

if(indexes.isEmpty())
    return 0;

QMimeData *mimeData = new QMimeData;
QByteArray encodeData;

QDataStream stream(&encodeData, QIODevice::WriteOnly);
foreach(QModelIndex index, indexes)
{
    if(index.value())
    {
        /* 将index中存储的数据取出来 */
        stream << /* 数据1 */ << /* 数据2 */ << /* ... */;
    }
}

mimeData->setData(/* your custom mime types */, encodeData);
return mimeData;

需要注意的是,当模型索引列表indexes为空时,返回0;

dropMimeData

bool QAbstractItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)

当Qt检测到有Mime Data被释放到以该模型为model的组件上时,相应model的这个函数被调用。设计者需要重新实现这个函数来处理被释放的数据,通常是将数据存储到(插入或追加到)model的数据结构中。如果这个释放动作和数据被model处理则返回true,否则返回false;
参数中的row, column, parent指明数据释放的位置。如果row和column为-1,意味着被释放的数据释放到了parent上,通常是追加到parent的孩子中。如果row和column大于等于0,则被插入到parent孩子的特定位置。
通常的实现格式为:

if(!data->hasFormats(/* the custom mime types */)
    return false;
if(action == Qt::IgnoreAction)
    return true;

/* 确定插入的位置position */

QByteArray encodeData = data->data(/* the custom mime types */);
QDataStream stream(&encodeData, Qt::ReadOnly);
while(!stream.atEnd())
{
    stream >> /* 数据1 */ >> /* 数据2 */ >> /* ... */;

    beginInsertRows(parent, position, position);
    /* model内部数据结构执行插入操作 */
    endInsertRows();
    ++position;
}
return true;

Drop Action

在拖放模型中,用户既可以拖动信息的拷贝(复制)也可以拖动信息本身(剪切)到一个新地方。但是直到拖放操作完成之后程序才知道用户是想剪切还是想拷贝。这在拖动不同组件之间的信息时没有什么影响,但是在同一个组件上拖动时,检查使用了哪种drop action是很重要的。

Qt::DropAction

Qt::MoveAction   移动数据本身(剪切)
Qt::CopyAction   复制数据(复制)
Qt::IgnoreAction 忽略数据

在释放数据的时候,Qt会调用鼠标所在位置上组件的dropEvent()函数,设计者需要在这个函数中告诉拖放操作的数据源drop action以便数据源确定是复制还是剪切。

dropEvent

void QWidget::dropEvent(QDropEvent *event)

通知数据源drop action

event->setDropAction(Qt::MoveAction); /* 通知数据源删除原数据 */
event->accept();

或者

event->setDropAction(Qt::CopyAction); /* 通知数据源保留原数据 */
event->accept();

通常拖动操作都是开始于鼠标点击(mousePressEvent()函数),而每个拖动操作都是一个QDrag类,所以在鼠标点击函数中需要定义一个QDrag类,然后执行拖动操作,等待释放动作的反馈。可以把这个函数看成数据源。

mousePressEvent

void QWidget::mousePressEvent(QMouseEvent *event)
{
    /* ... */
    /* 取出点击位置的数据,进行序列化 */
    QDrag *drag = new QDrag(this);
    drag->setMimeData(mimeData);

    Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
    if(dropAction == Qt::CopyAction || dropAction == IgnoreAction)
    {
        /* 不需要删除原数据 */
    }
    else if(dropAction == Qt::MoveAction)
    {
        /* 删除原数据 */
        update(/* event->pos()处的范围矩形 */)
    }
}

其他一些需要实现的函数

下面这些类处理拖放和必要的mime type的编码和解码:
QDrag:为以Mime Data为基础的拖放转化提供支持。
QDragEnterEvent:这个类型的事件被发送到窗口如果有数据被拖进这个窗口。
QDragLeaveEvent:这个类型的事件被发送到窗口如果有数据被拖离这个窗口。
QDragMoveEvent:当正在拖动时,这个类型的事件会一直被发出。
QDropEvent:当拖放操作完成时,这个类型的时候被发出。

在实现拖放操作时就是围绕这几个类来实现特定的函数。

dragEnterEvent

void QWidget::dragEnterEvent(QDragEnterEvent *event);

protected作用域的函数。当鼠标拖进一个窗体的时候,该窗体的这个函数被调用,传递的事件作为参数。如果这个事件被忽略了(ignored),那么这个窗体不会接收任何拖动事件。换句话说这个函数就是用于提醒Qt该组件接收的数据类型,判断是accept()还是ignore()这个事件。通常的实现格式为:

if(event->mimeData()->hasFormats(/* the custom mime types */) event->accept(); else event->ignore();

dragLeaveEvent

void QWidget::dragLeaveEvent(QDragLeaveEvent *event);

顾名思义,这个函数就是当拖动超出该组件范围时调用的。

dragMoveEvent

void QWidget::dragMoveEvent(QDragMoveEvent *event);

当正在拖动时,会一直调用这个函数。可以改变一些变量比如记录此时的投影矩形,然后在绘制函数中绘制。

paintEvent

void QWidget::paintEvent(QPaintEvent *event);

这个函数就是用于绘制所在类的。它可以被调用由以下理由:
1.repaint()或者update()函数被调用;
2.窗口部件被遮挡并且已经被Qt发现;
3.其他的理由

绘制的时候使用画刷类QPainter

QPainter painter;
painter.begin(this);
/* 绘制代码 */
painter.end();