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来注册。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,327评论 0 17
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,054评论 25 707
  • 鱼那么喜欢水,水却煮了鱼,只为他心爱的姑娘,哪怕只是一眼就爱上的姑娘。 也许我们每一个人都曾飞蛾扑火般为爱执着,却...
    梨_涡阅读 204评论 0 0
  • 九 移动到滨松 2011年7月26日,今天的目的是抵达浜松(其实昨天经过了)。中午12:00才碰头,本来上午可以抽...
    明哥明说阅读 343评论 0 2