拖放
拖放提供了一种简单的可视化机制,可以被用来在多个应用程序间或单个应用程序中传递信息。(这里可以看作是“直接的操作模型”)拖拽功能与剪贴板的剪切和粘贴机制很类似。
· 配置
· 拖拽
· 释放
o 重载预设的动作
o 继承复杂的窗体部件
· 拖放动作
· 增加新的拖放类型
· 释放动作
· 释放矩形
· 剪贴板
· 实例
· 与其他应用程序的互操作
这篇文档描述了基本的拖放机制和概要地介绍了如何在用户自定义的窗体部件实现这项功能。Qt的item view和graphics view框架也支持拖放操作;相关的内容可以参阅在item view和graphics view框架中使用拖放。
配置
QApplication 对象提供了一些跟拖放相关的属性:
· QApplication::startDragTime 描述了在拖拽开始之前用户必须按下鼠标键的时长
· QApplication::startDragDistance指用户在鼠标保持按下状态必须移动多远才表示要实施拖拽。使用稍大的值可以避免用户只是想点击目标而造成的偶然拖拽。
如果你想在你的窗体部件支持拖放,这两个属性可以为你设置敏感性的默认值。
拖拽
创建一个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();
}
之后,在mousePressEvent()中我们可以决定是否开始拖拽,并构造拖拽对象处理操作:
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();
}
在这个例子中,我们接受预设的动作而没有去检查它。 在实际的应用程序中,如果动作是不相关的,可能有必要从dropEvent()函数返回而不接受预设的动作或者不对数据进行处理。例如,如果我们在我们的应用程序中不支持对外部资源的连接,我们可能选择忽略Qt::LinkAction动作。
重载预设的动作
我们可能忽略预设的动作,并对数据执行某些其他动作。为此我们要在调用accept()之前用希望的动作从Qt::DropAction调用事件对象的setDropAction()。这会确保替换的动作能够替代预设的动作被使用。
对于更多成熟的应用程序,重新实现dragMoveEvent() 和dragLeaveEvent()会使你的窗体部件对释放事件更灵敏并且能够在你得应用程序中给你更多对拖放操作的控制权。
继承复杂的窗体部件
某些标准的Qt窗体部件自身提供自己的拖放功能。在继承这些窗体部件的时候,可能有必要重新实现dragMoveEvent(), dragEnterEvent()以及 dropEvent()来避免继承基类的拖放操作,以便处理任何你感兴趣的特殊操作。
拖放动作
在这个最简的例子中,一个拖放动作的目标对象接收到被拖拽的数据的拷贝,源对象决定是否删除原始数据。CopyAction动作描述了这个操作。目标对象也可以选择处理其他动作,特别是MoveAction动作和LinkAction动作。如果源对象调用QDrag::exec() 并返回MoveAction,这样做的时候源对象要负责删除所有原始数据。由源窗体部件创建的QMimeData和QDrag对象不应该被删除,他们会被Qt删除。目标对象负责取消在拖放操作中发送的数据的所属关系;这样通常是通过保持对数据的引用来达到的。
如果目标对象理解LinkAction动作,它应该保存它自己对原始信息的引用;源对象不需要执行任何进一步的对数据的操作。拖放动作的最通常的用法是当在同一个窗体部件中执行移动操作;关于这个特征的详细信息见释放动作一节。
拖拽动作的其他主要用法是在使用一个例如text/uri-list引用类型的时候,该处拖拽的数据实际上是指向一个文件或者对象。
增加新的拖放类型
拖放不仅限于文本和图形。任何信息类型都能够在拖放操作中被传送。为了在应用程序间拖放信息,应用程序必须彼此能够识别它们能够接受和处理的数据格式。使用MIME类型来获得。QDrag对象由包含MIME类型列表的源目标构建,用来表示数据(按最适合到不适合顺序排列),释放的目标对象使用其中的一个来接受数据。对于最常见的数据类型,已有的方便函数能处理这些透明使用的MIME类型,但是自定义数据类型必须被显示地表述。
为了对QDrag的方便函数没有覆盖到的信息类型实现拖放动作,第一步也是最重要的一步是查看已有的合适格式。Internet号分配机构(IANA)在美国科学情报研究所(ISI)提供了一个MIME媒体类型的分级列表。使用标准MIME类型现在或将来能使你的应用程序和其他软件之间的互操作最大化。
为了支持一种额外的媒体类型,简单调用setData()函数在QMimeData对象中设置以完全支持MIME类型和一个包含相应格式的数据的QByteArray对象。下面的代码从一个标签中取图像,并在一个QMimeData对象中把它保存成一个流式网络图形格式文件(png文件):
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();
}
在这个例子中,我们拒绝处理移动操作。我们接受的每一种释放动作都要被检查并做相应处理:
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()也能被用来滚动窗口,或者任何其他合适的操作。
剪贴板
应用程序还能够通过在剪贴板中放入数据与其它程序进行交流。实现它需要你从QApoliction对象中得到一个QClipboard对象:
clipboard = QApplication::clipboard();
QMimeData类用来表达通过剪贴板传递的数据。你能够使用setText(),setImage() 和setPixMap()这些方便函数把通用数据类型的数据放入剪贴板。这些函数类似于QMineData类中的相应函数,除了那些带有额外控制数据存放位置的参数的函数:如果指定剪贴板,数据被放到剪贴板,如果指定选定,数据被放到鼠标选定的地方(仅限于X11)。默认情况下数据被放到剪贴板。
例如,我们能用下面的代码拷贝一个QLineEdit中的内容到剪贴板:
clipboard->setText(lineEdit->text(), QClipboard::Clipboard);
带不同MIME类型的数据也能够被放到剪贴板。构造一个QMimeData对象并用setData()函数用在之前小节中描述的方式设置数据;然后用setMineDate()函数把这个对象放入剪贴板。
QClipboard类能够在它的数据被改变的时候通过它的dataChanged()信号通知应用程序。例如,我们能够通过连接信号和窗体不见得一个槽来监视剪贴板:
connect(clipboard, SIGNAL(dataChanged()), this, SLOT(updateClipboard()));
连接到这个信号的槽能够用一个能表达剪贴板上的数据的MIME类型读取剪贴板上的数据:
void ClipWindow::updateClipboard()
{
QStringList formats = clipboard->mimeData()->formats();
QByteArray data = clipboard->mimeData()->data(format);
...
}
selectionChanged()信号能够用在X11上监视鼠标选取操作。
实例
· Draggable Icons
· Draggable Text
· Drop Site
· Fridge Magnets
· Drag and Drop Puzzle
与其他应用程序的互操作
在X11上用到了公用XDND协议,而在Windows上Qt使用OLE标准,而Qt/Mac使用Carbon拖拽管理器。在X11上,XDND使用MIME,无须转换。Qt API不管平台怎样,总是相同的。在Windows上,能识别MIME的应用程序能够通过使用剪贴板格式的MIME类型名称进行交流。某些应用程序已经使用MIME的剪贴板格式的命名规范。Qt内部使用QWindowsMime和QMacPasteboardMime 在专有的剪贴板格式和MIME类型之间转换。
在X11上,Qt也支持释放,也就是Modif拖放协议。这样的实现方法合并了某些最初由Daniel Dardailer编写并被Matt Koss koss@napri.sk和奇趣公司为Qt改写的代码。这儿有原始的版权声明:
版权 1996 Daniel Dardailler.
允许处于任何目的免费地使用,拷贝,修改,分发和销售这个软件,并在所有拷贝中提供上述的版权声明,并且版权声明和许可声明出现在支持文档中,名字Daniel Dardailer未经许可,不能被用作分法软件时的广告或者公众宣传。Daniel Dardailler makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.
修改版权 1999 Matt Koss,基于如上的相同许可证
注释:Modif拖放协议只允许接收者请求数据在一个QDropEvent中响应。如果你试图请求数据在例如一个QDragMoveEvent中相应,会返回空的QByteArray。