拖放

拖放提供了一种简单的视觉机制,用户可以使用它来在应用程序之间以及within应用程序内部传输信息。拖放与剪贴板的剪切和粘贴机制在功能上相似。

本文档介绍了基本拖放机制及其在自定义控件中实现的说明。许多 Qt 控件支持拖放操作,如项视图和图形视图框架,以及 Qt 小部件和 Qt Quick 的编辑控件。有关项视图和图形视图的更多信息,请参阅使用项视图进行拖放图形视图框架

拖放类

这些类处理拖放、必要的 MIME 类型编码和解码。

QDrag

支持基于 MIME 的拖放数据传输

QDragEnterEvent

当拖放操作进入小部件时发送的事件

QDragLeaveEvent

当拖放操作离开小部件时发送的事件

QDragMoveEvent

拖放操作进行中发送的事件

QDropEvent

拖放操作完成后发送的事件

QUtiMimeConverter

在 MIME 类型与统一类型标识符(UTI)格式之间进行转换

配置

QStyleHints 对象提供了一些与拖放操作相关的属性

  • QStyleHints::startDragTime() 描述了用户在开始拖放之前,必须将鼠标按钮按在对象上多少毫秒的时间。
  • QStyleHints::startDragDistance()指示用户在按下鼠标按钮的同时,必须将鼠标移动多远才能将此移动解释为拖放。
  • QStyleHints::startDragVelocity() 指示用户必须以多快(以像素/秒为单位)的速度移动鼠标才能开始拖放。值 0 表示没有此限制。

这些数量提供了合理的默认值,符合底层窗口系统,以便您在控件中提供拖放支持时使用。

Qt Quick 中的拖放

本文档的其他部分主要关注如何在 C++ 中实现拖放。对于在 Qt Quick 场景中进行拖放,请参阅 Qt Quick 的DragDragEventDropArea项目的文档,以及Qt Quick 拖放示例。

拖放

要开始拖动,请创建一个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()函数来获取鼠标点击处和当前光标位置之间距离的粗略估计。此函数以精度换取速度,通常适用于此目的。

释放

要能够在窗口部件上接收释放的媒体,为该窗口部件调用setAcceptDrops(true),并重新实现dragEnterEvent()和dropEvent()事件处理函数。

例如,以下代码在一个QWidget子类的构造函数中启用释放事件,使得可以有效地实现释放事件处理程序。

Window::Window(QWidget *parent)
    : QWidget(parent)
{
    ...
    setAcceptDrops(true);
}

通常情况下,dragEnterEvent()函数用于通知Qt该窗口部件可以接收哪些类型的数据。如果您想在重新实现dragMoveEvent()和dropEvent时接收QDragMoveEventQDropEvent,则必须重新实现此函数。

以下代码展示了如何重新实现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中的首选操作调用事件对象的setDropAction()。这确保使用替换的释放操作而不是提议的操作。

对于更高级的应用,重新实现dragMoveEvent()和dragLeaveEvent()可以使贵小部件的某些部分对拖放事件敏感,并使您能够更有效地控制应用程序中的拖放。

子类化复杂小部件

某些标准Qt小部件提供了自己的拖放支持。在子类化这些小部件时,可能需要重新实现dragMoveEvent()、dragEnterEvent()和dropEvent(),以便防止基类提供默认的拖放处理,并处理任何特殊的兴趣点。

拖放操作

在最简单的情况下,拖放动作的目标收到被拖拽数据的副本,而数据来源决定是否删除原始数据。这由CopyAction操作描述。目标也可以选择处理其他动作,特别是MoveActionLinkAction动作。如果来源调用QDrag::exec(),并且返回MoveAction,则来源在需要时负责删除任何原始数据。来源小部件创建的QMimeDataQDrag对象不应删除 - 它们将由Qt销毁。目标是负责接收拖放操作中发送的数据的所有权;这通常通过保留对数据的引用来完成。

如果目标理解LinkAction动作,它应存储到原始信息的引用;来源不需要对数据进行任何进一步的处理。拖放动作的最常见用途是在同一小部件中执行移动;有关此功能的更多信息,请参阅Drop Actions部分。

拖放动作的另一个主要用途是在使用诸如文本/uri-list之类的引用类型时,实际上被拖放的数据是文件或对象的引用。

添加新的拖放类型

拖放不仅限于文本和图像。在任何拖放操作中都可以传输任何类型的信息。要在应用程序之间拖放信息,应用程序能够向彼此指示可接受和产生哪些数据格式。这可以通过MIME类型来实现。来源小部件构建的QDrag对象包含一个由其使用的、表示数据的MIME类型的列表(从最合适的到最不合适的排序),而拖放目标则使用其一来访问数据。对于常见的数据类型,便利函数可透明地处理使用的MIME类型,但对于自定义数据类型,则需要明确地声明它们。

为了实现未由QDrag便利函数覆盖的信息类型的拖放动作,首先要做的是寻找现有的合适格式:互联网数字分配机构(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对象中的数据量的更精细控制。

请注意,在项视图中使用的自定义数据类型必须声明为元对象,并且必须为它们实现流操作符。

放置操作

在剪切板模型中,用户可以剪切或复制源信息,然后在稍后粘贴它。同样,在拖放模型中,用户可以拖动信息的副本或将信息本身拖动到新的位置(移动它)。对于程序员来说,拖放模型有额外的复杂性:程序不知道用户是想剪切还是复制信息,直到操作完成。当在应用程序之间拖动信息时,这可能没有太大区别,但在一个应用程序内部,检查使用了哪种放置操作是很重要的。

我们可以为小部件重新实现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()。

放置矩形

可以通过在小部件的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()
{
    mimeTypeCombo->clear();

    QStringList formats = clipboard->mimeData()->formats();
    if (formats.isEmpty())
        return;

    for (const auto &format : formats) {
        QByteArray data = clipboard->mimeData()->data(format);
        // ...
    }

可以使用selectionChanged()信号来在X11中监视鼠标选择。

示例

与其它应用程序交互

在X11上,使用公开的XDND协议,在Windows上Qt使用OLE标准,而Qt for macOS使用Cocoa拖拽管理器。在X11上,XDND使用MIME,因此不需要翻译。Qt API在不同的平台上是相同的。在Windows上,使用MIME类型作为剪贴板格式名称的MIME感知应用程序可以通过剪贴板进行通信。一些Windows应用程序已经使用MIME命名约定为自己的剪贴板格式命名。

可以通过在Windows上实现QWindowsMimeConverter或在macOS上实现QUtiMimeConverter来注册用于转换专有剪贴板格式的自定义类。

© 2024 Qt公司有限公司。此处包括的文档贡献均归各自所有者享有版权。提供的文档依照自由软件基金会发布的GNU自由文档许可证版本1.3的条款进行授权。Qt及其 respective标志是芬兰及其它全球地区的Qt公司有限公司的商标。所有其他商标均属于其 respective所有者。