文章目录
Drag and Drop
拖放提供了一种简单的可视化机制,用户可以使用它在应用程序之间和应用程序内部传输信息。拖放功能类似于剪贴板的剪切和粘贴机制,均是以QMimeData类为基础的。
本文档介绍了基本的拖放机制,并概述了在自定义控件中启用它的方法。
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 and Drop示例。
拖曳Dragging
要开始拖动,请创建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 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);
...
}
这种特殊的方法使用QPoint::manhattanLength()函数来大致估计鼠标单击发生的位置与当前光标位置之间的距离。此功能以准确性为代价,通常适合于此目的。
放置Dropping
为了能够接收放置在窗口小部件上的媒体,请为窗口小部件调用setAcceptDrops(true),然后重新实现dragEnterEvent()和dropEvent()事件处理函数。
例如,以下代码在QWidget子类的构造函数中启用放置事件,从而可以有效地实现放置事件处理程序:
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()用来解包放置的数据,使用适合您的应用程序的方式处理它。
在下面的代码中,事件中提供的文本将传递到QTextBrowser,并且QComboBox会填充用于描述数据的MIME类型的列表:
void Window::dropEvent(QDropEvent *event)
{
textBrowser->setPlainText(event->mimeData()->text());
mimeTypeCombo->clear();
mimeTypeCombo->addItems(event->mimeData()->formats());
event->acceptProposedAction();
}
在这种情况下,我们接受建议的操作而不检查它是什么。在实际的应用程序中,可能需要从dropEvent()函数返回而不接受建议的操作或不处理该数据(如果该操作不相关)。例如,如果我们不支持在应用程序中指向外部源的链接,则可以选择忽略Qt::LinkAction操作。
重载 Proposed Actions
我们也可能忽略建议的操作,并对数据执行其他操作。为此,我们将在调用accept()之前,使用Qt::DropAction的首选操作调用事件对象的setDropAction()。这样可以确保使用替换放置动作代替提议的动作。
对于更复杂的应用程序,重新实现dragMoveEvent()和dragLeaveEvent()可使您使小部件的某些部分对放置事件敏感,并使您可以更好地控制应用程序中的拖放。
子类化复杂小部件
某些标准Qt小部件提供了自己的拖放支持。在子类化这些小部件时,除dragEnterEvent()和dropEvent()外,可能还需要重新实现dragMoveEvent(),以防止基类提供默认的拖放处理,并处理您感兴趣的任何特殊情况。
拖放动作
在最简单的情况下,拖放操作的目标将收到要拖动的数据的副本,而源将决定是否删除原始数据。这由CopyAction
操作来描述。目标还可以选择处理其他动作,特别是MoveAction
和LinkAction
动作。如果源调用QDrag::exec()并返回MoveAction
,则源负责选择删除任何原始数据。源小部件创建的QMimeData和QDrag对象不应删除-它们将被Qt破坏。目标负责获取在拖放操作中发送的数据的所有权;这通常是通过保留对数据的引用来完成的。
如果目标了解该LinkAction
操作,则应存储其对原始信息的引用;源不需要对数据进行任何进一步的处理。拖放操作最常见的用法是在同一小部件中执行“移动”时;有关此功能的更多信息,请参见“放置动作”部分。
拖动动作的另一个主要用途是使用诸如文本/uri-list之类的引用类型时,其中拖动的数据实际上是对文件或对象的引用。
添加新的拖放类型
拖放不仅限于文本和图像。任何类型的信息都可以通过拖放操作进行传输。为了在应用程序之间拖动信息,应用程序必须能够相互指示它们可以接受哪些数据格式以及可以产生哪些数据格式。这是使用MIME类型实现的。由源构造的QDrag对象包含用于表示数据的MIME类型列表(从最合适的到最不合适的顺序),放置目标使用其中之一来访问数据。对于常见的数据类型,便捷功能处理透明使用的MIME类型,但是对于自定义数据类型,必须明确声明它们。
要对QDrag便利功能未涵盖的信息类型执行拖放操作,第一步也是最重要的一步是寻找合适的现有格式:Internet分配号码授权机构(IANA)提供了以下内容的层次结构列表:信息科学研究所(ISI)的MIME媒体类型。现在和将来,使用标准的MIME类型都会使您的应用程序与其他软件的互操作性最大化。
要支持其他媒体类型,只需使用setData()函数在QMimeData对象中设置数据,即可提供完整的MIME类型和一个包含适当格式数据的QByteArray。以下代码从标签中获取一个像素图,并将其作为可移植网络图形(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对象中存储的数据量。
请注意,项目视图中使用的自定义数据类型必须声明为元对象,并且必须实现它们的流运算符。
放置动作Drop Actions
在剪贴板模型中,用户可以剪切或复制源信息,然后粘贴。类似地,在拖放模型中,用户可以拖动信息的副本,也可以将信息本身拖动到新的位置(移动它)。拖放模型给程序员带来了一个额外的麻烦:在操作完成之前,程序不知道用户是要剪切还是复制信息。在应用程序之间拖动信息时,这通常没有什么区别,但是在应用程序内部,重要的是检查使用了哪个放置动作。
我们可以为小部件重新实现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,但是如果将信息放置在同一应用程序的另一个小部件中,则我们可能会获得不同的放置操作。
可以在小部件的dragMoveEvent() 函数中过滤建议的放置动作。但是,可以在dragEnterEvent() 中接受所有建议的动作,并让用户决定以后要接受哪个动作:
void DragWidget::dragEnterEvent(QDragEnterEvent *event)
{
event->acceptProposedAction();
}
当小部件中发生放置时,将调用dropEvent()处理函数,我们可以依次处理每个可能的动作。首先,我们在同一小部件中处理拖放操作:
void DragWidget::dropEvent(QDropEvent *event)
{
if (event->source() == this && event->possibleActions() & Qt::MoveAction)
return;
在这种情况下,我们拒绝处理move 动作。我们接受并检查的每种类型的放置动作均会相应地进行处理:
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;
}
...
}
请注意,我们在上面的代码中检查了单个放置动作。如上文“覆盖建议的操作”部分中所述,有时有必要覆盖建议的放置操作并从可能的放置操作中选择一个不同的放置操作。为此,您需要检查事件的possibleActions()提供的值中是否存在每个动作,并使用setDropAction()设置放置动作,然后调用accept()。
放置矩形
小部件的dragMoveEvent() 可用于通过仅在光标位于这些区域内时接受建议的放置动作来将放置限制在小部件的某些部分。例如,以下代码在光标位于子窗口小部件(dropFrame)上时接受任何建议的放置动作:
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,则数据将放置在鼠标选择中(仅在X11上)。默认情况下,数据放置在剪贴板上。
例如,我们可以使用以下代码将QLineEdit的内容复制到剪贴板:
QGuiApplication::clipboard()->setText(lineEdit->text(), QClipboard::Clipboard);
MIME类型不同的数据也可以放在剪贴板上。构造一个QMimeData对象,并使用上一节中所述的方法使用setData()函数设置数据;然后可以使用setMimeData()函数将此对象放置在剪贴板上。
所述QClipboard类可以通知关于更改数据的应用程序它经由其包含dataChanged()信号。例如,我们可以通过将此信号连接到小部件中的插槽来监视剪贴板:
connect(clipboard, &QClipboard::dataChanged, this, &ClipWindow::updateClipboard);
连接到该信号的插槽可以使用一种可以表示它的MIME类型读取剪贴板上的数据:
void ClipWindow::updateClipboard()
{
QStringList formats = clipboard->mimeData()->formats();
QByteArray data = clipboard->mimeData()->data(format);
...
}
参考selectionChanged()信号可以在X11被用于监测鼠标选择。
例子
与其他应用程序的互操作
在X11上,使用公共XDND协议,而在Windows上,Qt使用OLE标准,而macOS的Qt使用CocoaDragManager。在X11上,XDND使用MIME,因此无需翻译。无论平台如何,QtAPI都是相同的。在Windows上,支持MIME的应用程序可以使用MIME类型的剪贴板格式名称进行通信。一些Windows应用程序已经将MIME命名约定用于其剪贴板格式。
对于翻译专有的剪贴板格式自定义类可以通过在Windows或重新实现QWinMime注册QMacPasteboardMime在MacOS。