Drag and drop
提供了一种简单的可视化机制,用户可以使用它在应用程序之间和应用程序内部传输信息。拖放的功能类似于剪贴板的剪切和粘贴机制。
本文描述了基本的拖放机制,并概述了在自定义控件中启用该机制的方法。Qt的许多控件也支持拖放操作,例如the item views and graphics view framework
项目视图和图形视图框架,以及Qt Widgets和Qt Quick的编辑控件。
以下类处理拖放和必要的mime类型编码和解码。
类 | 描述 | 解释 |
---|---|---|
QDrag | Support for MIME-based drag and drop data transfer | 支持基于MIME的拖放数据传输 |
QDragEnterEvent | Event which is sent to a widget when a drag and drop action enters it | 当拖放操作进入窗口部件时发送的事件 |
QDragLeaveEvent | Event that is sent to a widget when a drag and drop action leaves it | 当拖放操作离开窗口部件时发送的事件 |
QDragMoveEvent | Event which is sent while a drag and drop action is in progress | 在拖放操作进行中时发送的事件 |
QDropEvent | Event which is sent when a drag and drop action is completed | 当拖放操作完成时发送的事件 |
QStyleHints
对象提供与拖放操作相关的一些属性:
QStyleHints::startDragTime()
描述用户在按住鼠标按钮在一个对象上多长时间后,拖动操作开始的毫秒数。QStyleHints::startDragDistance()
指示用户在按住鼠标按钮时移动鼠标的距离,这之后移动才会被解释为拖动。QStyleHints::startDragVelocity()
指示用户必须移动鼠标的速度(以像素/秒计)以启动拖动。值为0意味着没有这样的限制。这些量提供了合理的默认值,如果控件中提供拖放支持,可以使用它们,这些值符合底层窗口系统的要求。
要开始拖动,请创建一个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()
函数是一个阻塞函数,返回下面几个值之一。这些值表示操作的结束方式,并在下面更详细地描述。
常量 | 值 | 描述 | 解释 |
---|---|---|---|
Qt::CopyAction | 0x1 | Copy the data to the target. | 将数据复制到目标。 |
Qt::MoveAction | 0x2 | Move the data from the source to the target. | 将数据从源移到目标。 |
Qt::LinkAction | 0x4 | Create a link from the source to the target. | 从源到目标创建一个链接。 |
Qt::ActionMask | 0xff | ||
Qt::IgnoreAction | 0x0 | Ignore the action (do nothing with the data). | 忽略动作(不对数据执行任何操作)。 |
Qt::TargetMoveAction | 0x8002 | On Windows, this value is used when the ownership of the D&D data should be taken over by the target application, i.e., the source application should not delete the data. On X11 this value is used to do a move. TargetMoveAction is not used on the Mac. | 在Windows上,当目标应用程序应该接管D&D数据的所有权时使用此值,即,源应用程序不应删除数据。在X11上,此值用于执行移动操作。TargetMoveAction在Mac上不使用。 |
请注意,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()
函数来获取鼠标单击发生位置和当前光标位置之间距离的大致估计值。此函数以精度为代价提高了速度,并且通常适用于此目的。
要能够接收放置在小部件上的媒体,为小部件调用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
,并使用描述数据的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()
之前使用 Qt::DropAction
中的首选操作调用事件对象的 setDropAction()
。这样确保使用替换的放置操作而不是建议的操作。
对于更复杂的应用程序,重新实现 dragMoveEvent()
和dragLeaveEvent()
将使您能够使组件的某些部分对拖放事件敏感,并在应用程序中更好地控制拖放。
某些标准的 Qt 组件提供了自己的拖放支持。当子类化这些组件时,除了重新实现 dragEnterEvent()
和dropEvent()
之外,可能还需要重新实现 dragMoveEvent()
,以防止基类提供默认的拖放处理,并处理感兴趣的任何特殊情况。
在最简单的情况下,拖放操作的目标将接收被拖动的数据的副本,源则决定是否删除原始数据。这由 CopyAction
操作描述。目标还可以选择处理其他操作,特别是 MoveAction
和 LinkAction
操作。如果源调用 QDrag::exec()
,并且返回 MoveAction
,则源负责删除任何原始数据(如果选择删除)。源窗口小部件创建的 QMimeData
和 QDrag
对象不应被删除 - 它们将被 Qt 销毁。目标负责接管拖放操作中发送的数据的所有权;通常是通过保留对数据的引用来完成的。
如果目标理解 LinkAction
操作,则应将自己的引用存储到原始信息中;源无需进一步处理数据。拖放操作最常见的用法是在同一个小部件中执行 Move
操作;
拖动操作的另一个主要用途是使用ext/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
对象中的数据量。
请注意,item view
中使用的自定义数据类型
必须声明为元对象,并且必须实现它们的流操作符。
在剪贴板模型中,用户可以剪切或复制源信息,然后稍后粘贴它。同样,在拖放模型中,用户可以拖动信息的副本或者他们可以将信息本身拖动到一个新位置(移动它)。对于程序员来说,拖放模型有一个额外的复杂性:在操作完成之前,程序不知道用户是要剪切还是复制这些信息。当在应用程序之间拖动信息时,通常这没有区别,但在应用程序内部,检查使用了哪些放置操作很重要。
可以重新实现小部件的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()
函数。
widget
的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,SIGNAL(dataChanged()),this,SLOT(updateClipboard()));
连接到此信号的槽可以使用可用于表示其的MIME类型之一之一读取剪贴板上的数据:
void ClipWindow :: updateClipboard()
{
QStringList formats = clipboard->mimeData()->formats();
QByteArray data = clipboard->mimeData()->data(format);
…
}
在X11上,可以使用selectionChanged()信号来监视鼠标选择。