拖放

拖放提供了一种简单的可视化机制,用户可利用它在应用程序之间和内部传输信息。拖放功能类似于剪贴板的剪切和粘贴机制。

本文档介绍了基本的拖放机制,并概述了在自定义控件中启用该机制的方法。Qt 的许多控件也支持拖放操作,如项目视图和图形视图框架,以及Qt WidgetsQt Quick 的编辑控件。有关项目视图和图形视图的更多信息,请参阅《通过项目视图和图形视图框架 使用拖放》。

拖放类

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

QDrag

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

QDragEnterEvent

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

QDragLeaveEvent

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

QDragMoveEvent

拖放操作正在进行时发送的事件

QDropEvent

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

QUtiMimeConverter

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

配置

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

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

拖放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() 函数是一个阻塞函数,其返回值为one of several values 。这些信息表明操作是如何结束的,下文将详细介绍。

请注意,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 Widget 接受的数据类型。如果您想在重新实现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 部件提供自己的拖放支持。在子类化这些部件时,除了dragEnterEvent() 和dropEvent() 之外,可能还需要重新实现dragMoveEvent() 以防止基类提供默认的拖放处理,并处理您感兴趣的任何特殊情况。

拖放操作

在最简单的情况下,拖放操作的目标会收到被拖动数据的副本,而源会决定是否删除原始数据。CopyAction 操作对此进行了描述。目标也可以选择处理其他操作,特别是MoveActionLinkAction 操作。如果源调用QDrag::exec() 并返回MoveAction ,那么源将负责删除任何原始数据(如果它选择这样做)。源 Widget 创建的QMimeDataQDrag 对象不应删除- 它们将被 Qt XML 销毁。目标机负责获取拖放操作中发送的数据的所有权;这通常是通过保留对数据的引用来实现的。

如果目标机能理解LinkAction 操作,它就应存储自己对原始信息的引用;源程序无需对数据进行任何进一步处理。拖放操作最常见的用途是在同一 widget 中执行移动操作;有关此功能的更多信息,请参阅 "拖放操作"部分。

拖放操作的另一个主要用途是使用文本/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 对象中的数据量。

请注意,在项目视图中使用的自定义数据类型必须声明为meta objects ,并且必须为其实现流运算符。

下拉操作

在剪贴板模型中,用户可以剪切复制源信息,然后再粘贴。同样,在拖放模型中,用户可以拖动信息的副本,也可以将信息本身拖到新的位置(移动)。对于程序员来说,拖放模式还有一个额外的麻烦:在操作完成之前,程序并不知道用户想要剪切还是复制信息。在应用程序之间拖动信息时,这通常没有什么区别,但在应用程序内部,检查使用了哪种拖放操作就很重要了。

我们可以为一个部件重新实现 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();
}

当 widget 中发生拖放时,dropEvent() 处理函数将被调用,我们可以依次处理每个可能的操作。首先,我们要处理的是同一 widget 中的拖放操作:

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() 可用于将下拉操作限制在 widget 的特定区域内,即只有当光标位于这些区域内时,才接受提议的下拉操作。例如,当光标位于子部件 (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()
{
    mimeTypeCombo->clear();

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

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

在 X11 上,selectionChanged() 信号可用于监控鼠标选择。

示例

与其他应用程序互操作

在 X11 上,使用的是公共XDND 协议,而在 Windows 上,Qt 使用的是 OLE 标准,Qt for macOS 使用的是 Cocoa 拖动管理器。在 X11 上,XDND 使用 MIME,因此无需翻译。无论平台如何,Qt API 都是一样的。在 Windows 上,支持 MIME 的应用程序可以使用 MIME 类型的剪贴板格式名称进行通信。一些 Windows 应用程序已经为其剪贴板格式使用了 MIME 命名约定。

可通过重新实现 Windows 上的QWindowsMimeConverter 或 macOS 上的QUtiMimeConverter 来注册用于翻译专有剪贴板格式的自定义类。

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.