概述
拖放提供了一种简单的可视化机制,用户可以使用它在应用程序之间和应用程序内部传输信息。拖放功能类似于剪贴板的剪切和粘贴机制。
本文档描述了基本的拖放机制,并概述了在自定义控件中启用它的方法。Qt的许多控件也支持拖放操作,如item views和graphics view框架,以及Qt窗口组件和Qt Quick的编辑控件。关于项目视图和图形视图的更多信息可以在使用拖放项目视图和图形视图框架中找到。
拖放类
这些类处理拖放以及必要的mime类型编码和解码。
|----------------------------------------------------------------------------------|-----------------------------|
| QDrag | 支持基于mime的拖放数据传输 |
| QDragEnterEvent | 事件,当拖放操作进入widget时被发送给widget |
| QDragLeaveEvent | 当拖放操作离开widget时发送给widget的事件 |
| QDragMoveEvent | 事件,在拖放操作进行时被发送 |
| QDropEvent | 事件,在拖放操作完成时发送 |
配置
QStyleHints对象提供了一些与拖放操作相关的属性:
- startDragTime()描述了用户在拖动对象之前必须按下鼠标按钮的时间(以毫秒为单位)。
- startDragDistance()表示用户在按住鼠标按钮的同时移动鼠标的距离,直到移动被解释为拖动。
- startDragVelocity()表示用户移动鼠标以多快(以像素/秒为单位)开始拖动。值为0意味着没有这种限制。
如果你在控件中提供拖放支持,这些数量提供了合理的默认值,这些默认值与底层窗口系统兼容。
Qt Quick 拖放
文档的其余部分主要关注如何用c++实现拖放。对于在Qt快速场景中使用拖放,请阅读Qt快速拖放,DragEvent,和DropArea项目的文档,以及Qt快速拖放的例子。
draging拖
要开始拖动,创建一个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()函数来粗略估计鼠标单击发生的位置和当前光标位置之间的距离。此函数以准确性换取速度,通常适用于此目的。
droping
为了能够接收在小部件上丢弃的媒体,请为小部件调用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操作。
建议的覆盖操作
我们也可以忽略建议的操作,并在数据上执行一些其他操作。要做到这一点,我们需要在调用accept()之前用Qt::DropAction中的首选操作调用event对象的setDropAction()。这确保使用替换放置操作,而不是建议的操作。
对于更复杂的应用程序,重新实现dragMoveEvent()和dragLeaveEvent()可以让部件的某些部分对放置事件敏感,从而对应用程序中的拖放有更多的控制。
子类化复杂的部件
某些标准Qt部件提供了自己对拖放的支持。在继承这些部件时,除了dragEnterEvent()和dropEvent()之外,可能还需要重新实现dragMoveEvent(),以防止基类提供默认的拖放处理,并处理您感兴趣的任何特殊情况。
拖放操作
在最简单的情况下,拖放操作的目标接收到被拖动数据的副本,而源决定是否删除原始数据。这由CopyAction操作描述。目标也可以选择处理其他操作,特别是MoveAction和LinkAction操作。如果源调用QDrag::exec(),并且返回MoveAction,则源负责删除任何原始数据,如果它选择这样做。由source widget创建的QMimeData和QDrag对象不应该被删除------它们会被Qt销毁。目标负责获取拖放操作中发送的数据的所有权;这通常通过保留数据的引用来实现。
如果目标理解了LinkAction动作,它应该存储自己对原始信息的引用;数据源不需要对数据进行任何进一步处理。最常使用的拖放操作是在同一个部件中进行移动时;有关此功能的更多信息,请参阅关于放置操作的部分。
拖动操作的另一个主要用途是使用text/uri-list等引用类型时,拖动的数据实际上是对文件或对象的引用。
添加新的拖放类型
拖放不仅限于文本和图像。任何类型的信息都可以在拖放操作中传递。要在应用程序之间拖动信息,应用程序必须能够相互指示它们可以接受哪些数据格式,可以生成哪些数据格式。这是通过使用MIME类型实现的。由源构造的QDrag对象包含一个用来表示数据的MIME类型的列表(从最合适到最不合适排序),并且drop目标使用其中的一个来访问数据。对于常见的数据类型,便利函数可以透明地处理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对象中的数据量。
注意,项目视图中使用的自定义数据类型必须声明为元对象,并且必须实现它们的流操作符。
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;但是,如果信息被放入同一个应用程序中的另一个部件中,我们可能会得到不同的放入动作。
建议的放入操作可以在部件的dragMoveEvent()函数中过滤。不过,也可以在dragEnterEvent()中接受所有提议的操作,然后让用户决定稍后接受哪些操作:
void DragWidget::dragEnterEvent(QDragEnterEvent *event)
{
event->acceptProposedAction();
}
当放置操作在部件中发生时,会调用dropEvent()处理程序函数,这样我们就可以依次处理每个可能的操作。首先,我们在同一个部件中处理拖放操作:
void DragWidget::dropEvent(QDropEvent *event)
{
if (event->source() == this && event->possibleActions() & Qt::MoveAction)
return;
在这种情况下,我们拒绝处理移动操作。我们接受的每一种放入操作都会被检查并相应地处理:
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()。
drop的矩形
小部件的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()信号通知应用程序它包含的数据发生了变化。例如,我们可以通过将这个信号连接到widget中的一个插槽来监视剪贴板:
connect(clipboard, &QClipboard::dataChanged,
this, &ClipWindow::updateClipboard);
连接到这个信号的插槽可以使用一种MIME类型读取剪贴板上的数据,可以用来表示它:
void ClipWindow::updateClipboard()
{
QStringList formats = clipboard->mimeData()->formats();
QByteArray data = clipboard->mimeData()->data(format);
...
}
在X11上可以使用selectionChanged()信号来监视鼠标选择。
例子
- Draggable Icons 可拖放图标
- Draggable Text 可拖放文本
- Drop Site 删除网站
- Fridge Magnets 自由拖放widget
- Drag and Drop Puzzle 拖放拼图
与其他应用程序互操作
在X11上使用公共的XDND协议,而在Windows上Qt使用OLE标准,而在macOS上Qt使用Cocoa拖动管理器。在X11上,XDND使用MIME,因此不需要转换。Qt API与平台无关。在Windows上,支持MIME的应用程序可以通过使用MIME类型的剪贴板格式名称进行通信。已经有一些Windows应用程序为其剪贴板格式使用MIME命名约定。
可以通过在Windows上重新实现QWinMime或在macOS上重新实现QMacPasteboardMime来注册用于转换私有剪贴板格式的自定义类。