Drag and Drop in QT

Drag and Drop

拖放提供了一个简单的可视化机制,用户可以使用它来在应用程序之间和应用程序内部传输数据.拖放功能与剪贴板的剪切和粘贴机制类似。

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

主要类

名称 说明
QDrag 支持基于MIME的拖放数据传输
QDragEnterEvent 拖放操作进入时发送给小部件的事件
QDragLeaveEvent 拖放操作离开时发送给小部件的事件
QDragMoveEvent 拖放操作过程中发送给小部件的事件
QDropEvent 拖放操作完成时发送给小部件的事件

拖放配置

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()函数是一个阻塞函数,它可能返回几个不同的值之一。这些返回值表明了操作的结果如何,将在下面进行更详细的描述。

注意 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,一个QComboBox被填充了用于描述数据的MIME类型列表:

void Window::dropEvent(QDropEvent* event)
{
    textBrowser->setPlainText(event->mimeData()->text());
    mimeTypeCombo->clear();
    mimeTypeCombo->addItems(event->mimeData()->formats());
    
    event->acceptProposedAction();
}

在上面的例子中,我们接受拖拽Action而不检查它是什么。 在真实世界的应用程序中,可能需要从dropEvent()函数返回而不接受拖拽Action或在操作不相关的情况下处理数据。 例如,如果我们不支持在我们的应用程序中连接到外部源,我们可以选择忽略Qt :: LinkAction动作.

重写拖拽Action

我们也可能会忽略拖拽Action,并对数据采取其他行动。 为此,我们将在调用accept()之前,用Qt :: DropAction中的Action调用事件对象的setDropAction()。 这确保使用我们提供的替换Action而不是原来的Action。

对于更复杂的应用程序,重新实现dragMoveEvent()和dragLeaveEvent()将使您的小部件的某些部分对放置事件非常敏感,并让您更好地控制应用程序中的拖放操作。

子类化复杂Widgets

某些标准的Qt小部件提供了自己的拖放支持。 当继承这些小部件时,除dragEnterEvent()和dropEvent()外,可能还需要重新实现dragMoveEvent(),以防止基类提供默认的拖放处理,并处理任何您感兴趣的特殊情况。

拖放Actions

在最简单的情况下,拖放动作的目标接收被拖动的数据的副本,并且由拖动数据来源处决定是否删除原始数据。这是由CopyAction操作描述的。拖放目标也可以处理其他动作,特别是MoveAction和LinkAction动作。如果拖动数据来源调用QDrag :: exec(),并返回MoveAction,则拖动数据来源负责删除任何原始数据(如果它选择的话)。由Qt创建的QMimeData和QDrag对象不应该被删除 - 它们将被Qt破坏。目标负责获取拖放操作中发送的数据的所有权;这通常通过保持对数据的引用来完成。

如果目标了解LinkAction动作,则应该存储自己对原始信息的引用;拖动数据来源不需要对数据执行任何进一步的处理。
拖动操作的另一个主要用途是使用诸如text / 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对象中的数据量的更好的控制。
请注意,项目视图中使用的自定义数据类型必须声明为元对象,并且必须实现它们的流操作符。

拖放 Actions

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

我们可以重新实现一个widget的mouseMoveEvent(),并通过拖放Action的组合来开始一个拖放操作。例如,我们可能要确保拖动总是移动小部件中的对象:

 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,但是如果拖放操作的目标是同一应用程序的其他Widget,我们可能获取到一个不同的Action返回值.

可以在Widget重写的dragMoveEvent()函数中过滤建议的Action.但是,可以在dragEnterEvent()中接受所有的建议Action,并让用户稍后再决定要接受的Action.

 void DragWidget::dragEnterEvent(QDragEnterEvent *event)
  {
      event->acceptProposedAction();
  }

当一个拖放发生时,会调用dropEvent()函数.我们可以依次处理每个可能的Action.首先,我们处理同一Widget内部中拖放操作

void DragWidget::dropEvent(QDropEvent* event)
{
  if(event->source() == this && event->possibleActions() & Qt::MoveAction)
    return;
    

在这种情况下,我们拒绝处理移动操作。 我们接受的每种类型的拖放动作都是相应的检查和处理:

if(event->proposedAction() == Qt::MoveAction){
  event->acceptProposedAction();
  ...
}else if(event->proposedAction() == Qt::CopyAction){
  event->acceptProposedAction();
  ...
}else{
  // Ignore the drop.
  return;
}
...
}

请注意,我们在上面的代码中检查了单个的拖放操作。 如上面在覆盖建议拖放操作部分所述,有时需要覆盖建议的拖放操作,并从可能的拖放操作中选择一个不同的操作。 为此,您需要检查事件的possibleActions()提供的值中是否存在每个操作,使用setDropAction()设置拖放操作,然后调用accept()。

拖放矩形

dragMoveEvent()可以用来限制一个小部件可以接受拖放事件的区域,当光标位于指定区域时才接受建议拖放Action,可以达到此目的.例如,下面的代码只有在光标处在Window的子widget上时才接受拖放Action

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,SIGNAL(dataChanged()),this,SLOT(updateClipboard()));

可以在连接到此信号的槽函数中读取黏贴板中的数据

void ClipWindow::updateClipboard()
{
    QStringList formats = clipboard->mimeData()->formats();
    QByteArray data = clipboard->mimeData()->data(format);
    ...
}

selectionChanged()信号可用于X11上来监视鼠标选择。

与其他应用程序交互

在X11上,使用公共的XDND协议,而在Windows上,Qt使用OLE标准,而Qt for MacOS使用Cocoa Drag Manager。 在X11上,XDND使用MIME,所以不需要翻译。 不管平台如何,Qt API都是一样的。 在Windows上,支持MIME的应用程序可以使用MIME类型的剪贴板格式名称进行通信。 已经有一些Windows应用程序使用剪贴板格式的MIME命名约定。用于翻译专有剪贴板格式的自定义类可以通过在Windows上重新实现QWinMime或在macOS上重新实现QMacPasteboardMime来注册。

你可能感兴趣的:(Drag and Drop in QT)