拖放提供了一种简单的可视化机制,用户可以使用它在应用程序之间和应用程序内部传输信息。拖放功能类似于剪贴板的剪切和粘贴机制。
本文档描述了基本的拖放机制,并概述了在自定义控件中启用它的方法。Qt的许多控件也支持拖放操作,例如项目视图和图形视图框架,以及Qt Widgets和Qt Quick的编辑控件。关于项目视图和图形视图的更多信息可以在《使用拖放项目视图》和《图形视图框架》中找到。
拖放类
这些类处理拖放操作以及必要的mime类型编码和解码。
类 | 描述 |
---|---|
QDrag | 支持基于mime的拖放数据传输 |
QDragEnterEvent | 事件,当拖放操作进入小部件时发送给它 |
QDragLeaveEvent | 事件,当拖放操作离开小部件时发送给它 |
QDragMoveEvent | 事件,当拖放操作在进行中时发送 |
QDropEvent | 事件,当拖放操作完成时发送 |
配置
QStyleHints
对象提供了一些拖放操作相关的属性:
-
QStyleHints::startDragTime()
描述了用户在拖动对象之前必须按住鼠标按钮的时间(以毫秒为单位)。 -
QStyleHints::startDragDistance()
表示用户按住鼠标按钮移动被识别为拖动之前必须移动的距离。 -
QStyleHints::startDragVelocity()
表示用户必须以多快的速度(像素/秒)移动鼠标来开始拖动。值为0意味着没有这样的限制。
如果您在控件中提供拖放支持,这些数量提供了与底层窗口系统兼容的合理默认值,供您使用。
在Qt Quick中拖放
文档的其余部分主要关注如何在c++中实现拖放操作。在Qt Quick场景中使用拖放,请阅读Qt Quick drag, DragEvent和DropArea项目的文档,以及Qt Quick drag和drop的例子。
拖动
要开始拖动,创建一个QDrag对象,并调用它的exec()方法。在大多数应用程序中,只有在按下鼠标按钮并移动了一定距离后才开始拖放操作是一个好主意。然而,从一个小部件中启用拖放的最简单方法是重新实现小部件的mousePressEvent()
方法,并且开始拖放操作:
void MainWindow::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton
&& iconLabel->geometry().contains(event->pos())) {
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setText(commentEdit->toPlainText());
drag->setMimeData(mimeData);
drag->setPixmap(iconPixmap);
Qt::DropAction dropAction = drag->exec();
...
}
}
尽管用户可能需要一些时间来完成拖动操作,但就应用程序而言,exec()函数是一个阻塞函数,它返回多个值中的一个。这些表示操作如何结束,并在下面进行更详细的描述。
注意,exec()函数不会阻塞主事件循环。
对于需要区分鼠标点击和拖动的小部件,重新实现小部件的mousePressEvent()
函数来记录拖动的起始位置是有用的:
void DragWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
dragStartPosition = event->pos();
}
下面,在mouseMoveEvent()
函数中,我们可以确定拖动是否应该开始,并且构造一个拖动对象来处理该操作:
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton))
return;
if ((event->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance())
return;
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setData(mimeType, data);
drag->setMimeData(mimeData);
Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
...
}
这种特殊的方法使用QPoint::manhattanLength()
函数来获得鼠标点击位置与当前光标位置之间距离的粗略估计。该函数以精度换取速度,通常适合于此目的。
Dropping
为了能够接收小部件上drop的媒体,调用小部件的setAcceptDrops(true),并重新实现dragEnterEvent()和dropEvent()事件处理函数。
例如,下面的代码在QWidget子类的构造函数中启用drop事件,从而可以有效地实现drop事件处理程序:
Window::Window(QWidget *parent)
: QWidget(parent)
{
...
setAcceptDrops(true);
}
dragEnterEvent()
函数通常用于通知Qt小部件接受的数据类型。如果想在重新实现的dragMoveEvent()
和dropEvent()
函数的中接收QDragMoveEvent
或QDropEvent
事件,则必须重新实现此函数。
下面的代码展示了如何重新实现dragEnterEvent()
函数,告诉拖放系统只处理纯文本:
void Window::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("text/plain"))
event->acceptProposedAction();
}
dropEvent()
用于解包被drop的数据,并以适合您的应用程序的方式处理它。
在下面的代码中,事件中的文本被传递给一个QTextBrowser
,描述数据的MIME类型填充在一个QComboBox
中:
void Window::dropEvent(QDropEvent *event)
{
textBrowser->setPlainText(event->mimeData()->text());
mimeTypeCombo->clear();
mimeTypeCombo->addItems(event->mimeData()->formats());
event->acceptProposedAction();
}
在这种情况下,我们接受操作,而不检查它是什么。在实际应用程序中,可能需要从dropEvent()
函数返回,而不接受建议的操作或处理不相关的数据(如果操作不相关的话)。例如,我们可以选择忽略Qt::LinkAction
操作,如果我们在应用程序中不支持到外部源的链接。
重写推荐的操作
我们也可以忽略推荐的操作,并对数据执行其他操作。为了做到这一点,我们在调用accept()
之前调用事件对象的setDropAction()
函数来设置Qt::DropAction
。这确保使用替换的drop操作而不是建议的操作。
对于更复杂的应用程序,重新实现dragMoveEvent()
和dragLeaveEvent()
将使小部件的某些部分对拖放事件敏感,并使您对应用程序中的拖放操作有更多的控制。
子类化复杂的小部件
某些标准Qt小部件提供了它们自己的拖放支持。当子类化这些小部件时,除了dragEnterEvent
()和dropEvent()
外,可能需要重新实现dragMoveEvent()
,以防止基类提供默认的拖放处理,并处理您感兴趣的任何特殊情况。
拖放操作
在最简单的情况下,拖放操作的目标接收被拖放数据的副本,然后操作的源决定是否删除原始数据。这是由CopyAction
动作描述的。操作目标也可以选择处理其他操作,特别是MoveAction
和LinkAction
操作。如果源调用QDrag::exec()
,并且它返回MoveAction
,那么源负责删除任何原始数据,如果它选择这样做的话。由源窗口小部件创建的QMimeData
和QDrag
对象不应该被删除——它们将被Qt销毁,目标负责获取在拖放操作中发送的数据的所有权;这通常通过保持对数据的引用来实现。
如果目标理解了LinkAction
操作,它应该存储自己对原始信息的引用;源不需要对数据执行任何进一步的处理。拖放操作最常见的用法是在同一个小部件中执行Move
;有关该特性的更多信息,请参阅Drop Actions
一节。
拖动操作的另一个主要用途是使用text/uri-list等引用类型时,拖动的数据实际上是对文件或对象的引用。
添加新的拖放类型
拖放不仅仅局限于文字或图片,任何类型的信息都可以在拖放操作中传输。要在应用程序之间拖动信息,应用程序必须能够相互指示它们可以接受和生成哪些数据格式。这通过使用MIME类型来实现。由源构造的QDrag
对象包含用于表示数据的MIME
类型列表(从最合适到最不合适排序),而拖放目标使用其中的一种类型来访问数据。对于普通数据类型,函数透明地处理MIME类型,但是对于自定义数据类型,需要显式地声明它们。
要实现对QDrag
没有涵盖的信息类型的拖放操作,第一步也是最重要的一步是寻找合适的现有格式:互联网编号分配机构(IANA)在信息科学研究所(ISI)提供了一个分级的MIME媒体类型列表。使用标准MIME类型可以最大化您的应用程序现在和将来与其他软件的互操作性。
要支持额外的媒体类型,只需使用setData()
函数在QMimeData
对象中设置数据,提供完整的MIME
类型和包含适当格式数据的QByteArray
。下面的代码从标签中获取一个pixmap
,并将其作为一个PNG文件存储在QMimeData
对象中:
QByteArray output;
QBuffer outputBuffer(&output);
outputBuffer.open(QIODevice::WriteOnly);
imageLabel->pixmap()->toImage().save(&outputBuffer, "PNG");
mimeData->setData("image/png", output);
当然,对于这种情况,我们可以简单地使用setImageData()
来提供各种格式的图像数据:
mimeData->setImageData(QVariant(*imageLabel->pixmap()));
在这种情况下,QByteArray
方法仍然很有用,因为它可以更好地控制存储在QMimeData
对象中的数据量。
注意,ItemView中使用的自定义数据类型必须声明为元对象,并且必须实现它们的流操作符。
Drop操作
在剪贴板模型中,用户可以剪切或复制源信息,然后粘贴它。类似地,在拖放模型中,用户可以拖动信息的副本,也可以将信息本身拖到一个新的位置(移动它)。对程序员来说,拖放模型还有一个额外的复杂性:在操作完成之前,程序不知道用户是要剪切还是复制信息。在应用程序之间拖动信息时,这通常没有什么区别,但在应用程序内部,检查使用了哪个拖放操作是很重要的。
我们可以为小部件重新实现mouseMoveEvent()
,并使用可能的拖放操作组合启动拖放操作。例如,我们可能想要确保拖动总是移动小部件中的对象:
void DragWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton))
return;
if ((event->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance())
return;
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setData(mimeType, data);
drag->setMimeData(mimeData);
Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
...
}
如果信息被拖放到另一个应用程序中,exec()函数返回的操作可能默认为CopyAction,但是,如果信息被拖放到同一应用程序中的另一个小部件中,我们可能会获得不同的drop操作。
可以在小部件的dragMoveEvent()函数中过滤建议的drop操作。然而,在dragEnterEvent()中接受所有提议的操作并让用户决定他们以后想要接受的操作是可能的:
void DragWidget::dragEnterEvent(QDragEnterEvent *event)
{
event->acceptProposedAction();
}
当小部件中发生drop操作时,将调用dropEvent()
处理程序函数,我们可以依次处理每个可能的操作。首先,我们在同一个小部件中处理拖放操作:
void DragWidget::dropEvent(QDropEvent *event)
{
if (event->source() == this && event->possibleActions() & Qt::MoveAction)
return;
在这个例子中,我们拒绝处理移动操作。我们接受的每一种drop动作都会被检查并相应地处理:
if (event->proposedAction() == Qt::MoveAction) {
event->acceptProposedAction();
// Process the data from the event.
} else if (event->proposedAction() == Qt::CopyAction) {
event->acceptProposedAction();
// Process the data from the event.
} else {
// Ignore the drop.
return;
}
...
}
注意,我们在上面的代码中检查了单个的drop操作。正如上面关于《重写推荐的操作》一节所提到的,有时有必要重写推荐的drop操作,并从可能的drop操作选择中选择一个不同的操作。为此,您需要检查事件的possibleActions()
提供的值中是否存在每个操作,使用setDropAction()
设置删除操作,并调用accept()
。
Drop 矩形
小部件的dragMoveEvent()
函数可以用于将拖放限制在小部件的某些部分,方法是当光标位于这些区域时,只接受建议的拖放操作。例如下面的代码,在光标停留在子部件(dropFrame)上时接受任何建议的drop操作:
void Window::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("text/plain")
&& event->answerRect().intersects(dropFrame->geometry()))
event->acceptProposedAction();
}
如果需要在拖放操作期间提供视觉反馈、滚动窗口或任何适当的操作,也可以使用dragMoveEvent()
。
剪贴板
应用程序还可以通过将数据放在剪贴板上来相互通信。要访问它,您需要从QApplication
对象获得一个QClipboard
对象。
QMimeData
类用于表示与剪贴板之间传输的数据。要将数据放到剪贴板上,可以使用setText()
、setImage()
和setPixmap()
函数来处理常见的数据类型。这些函数类似于在QMimeData
类中找到的那些函数,除了它们还接受一个额外的参数来控制数据存储的位置:如果指定了Clipboard
,数据将放在剪贴板上;如果指定了Selection
,则数据被放置在鼠标选区中。默认情况下,将数据放置在剪贴板上。
例如,我们可以用以下代码将QLineEdit
的内容复制到剪贴板:
QGuiApplication::clipboard()->setText(lineEdit->text(), QClipboard::Clipboard);
不同MIME类型的数据也可以放在剪贴板上。按照上一节描述的方式,构造一个QMimeData
对象并使用setData()
函数设置数据;然后可以使用setMimeData()
函数将该对象放到剪贴板上。
QClipboard
类可以通过其datachchanged()
信号通知应用程序它所包含的数据的更改。例如,我们可以通过将这个信号连接到小部件中的一个槽来监视剪贴板:
connect(clipboard, SIGNAL(dataChanged()), this, SLOT(updateClipboard()));
连接到该信号的槽可以使用一种MIME类型来表示剪贴板上的数据:
void ClipWindow::updateClipboard()
{
QStringList formats = clipboard->mimeData()->formats();
QByteArray data = clipboard->mimeData()->data(format);
...
}