拖放(Drag and Drop),一般会简称为 DnD,是现代软件开发中必不可少的一项技术。它提供了一种可以在应用程序内部甚至是应用程序之间进行信息交换的机制。操做系统与应用程序之间进行的剪贴板内容的交换,也能够被认为是拖放的一部分。函数
拖放实际上是由两部分组成的:拖动和释放。拖动是将被拖放对象进行移动,释放是将被拖放对象放下。前者是一个按下鼠标按键并移动的过程,后者是一个松开鼠标按键的过程;一般这两个操做之间的鼠标按键是被一直按下的。固然,这只是一种广泛的状况,其它状况仍是要看应用程序的具体实现。对于 Qt 而言,一个组件既能够做为被拖动对象进行拖动,也能够做为释放掉的目的地对象,或者两者都是。this
在下面的例子中(来自 C++ GUI Programming with Qt4, 2nd Edition),咱们将建立一个程序,将操做系统中的文本文件拖进来,而后在窗口中读取内容。url
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); private: bool readFile(const QString &fileName); QTextEdit *textEdit; };
注意到咱们须要重写dragEnterEvent()
和dropEvent()
两个函数。顾名思义,前者是拖放进入的事件,后者是释放鼠标的事件。操作系统
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { textEdit = new QTextEdit; setCentralWidget(textEdit); textEdit->setAcceptDrops(false); setAcceptDrops(true); setWindowTitle(tr("Text Editor")); } MainWindow::~MainWindow() { }
在构造函数中,咱们建立了QTextEdit
的对象。默认状况下,QTextEdit
能够接受从其它应用程序拖放过来的文本类型的数据。若是用户把一个文件拖到这面,默认会把文件名插入到光标位置。可是咱们但愿让MainWindow
读取文件内容,而不是仅仅插入文件名,因此咱们在MainWindow
中加入了拖放操做。首先要把QTextEdit
的setAcceptDrops()
函数置为 false,而且把MainWindow
的setAcceptDrops()
置为 true,这样咱们就可以让MainWindow
截获拖放事件,而不是交给QTextEdit
处理。.net
void MainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat("text/uri-list")) { event->acceptProposedAction(); } }
当用户将对象拖动到组件上面时,系统会回调dragEnterEvent()
函数。若是咱们在事件处理代码中调用acceptProposeAction()
函数,就能够向用户暗示,你能够将拖动的对象放在这个组件上。默认状况下,组件是不会接受拖放的。若是咱们调用了这个函数,那么 Qt 会自动以光标样式的变化来提示用户是否能够将对象放在组件上。在这里,咱们但愿告诉用户,窗口能够接受拖放,可是咱们仅接受某一种类型的文件,而不是所有文件。咱们首先检查拖放文件的 MIME 类型信息。MIME 类型由 Internet Assigned Numbers Authority (IANA) 定义,Qt 的拖放事件使用 MIME 类型来判断拖放对象的类型。关于 MIME 类型的详细信息,请参考 http://www.iana.org/assignments/media-types/。MIME 类型为 text/uri-list 一般用来描述一个 URI 列表。这些 URI 能够是文件名,能够是 URL 或者其它的资源描述符。若是发现用户拖放的是一个 text/uri-list 数据(即文件名),咱们便接受这个动做。翻译
void MainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urls = event->mimeData()->urls(); if (urls.isEmpty()) { return; } QString fileName = urls.first().toLocalFile(); if (fileName.isEmpty()) { return; } if (readFile(fileName)) { setWindowTitle(tr("%1 - %2").arg(fileName, tr("Drag File"))); } } bool MainWindow::readFile(const QString &fileName) { bool r = false; QFile file(fileName); QString content; if(file.open(QIODevice::ReadOnly)) { content = file.readAll(); r = true; } textEdit->setText(content); return r; }
当用户将对象释放到组件上面时,系统回调dropEvent()
函数。咱们使用QMimeData::urls()
来得到QUrl
的一个列表。一般,这种拖动应该只有一个文件,可是也不排除多个文件一块儿拖动。所以咱们须要检查这个列表是否为空,若是不为空,则取出第一个,不然当即返回。最后咱们调用readFile()
函数读取文件内容。这个函数的内容很简单,咱们前面也讲解过有关文件的操做,这里再也不赘述。如今能够运行下看看效果了。code
接下来的例子也是来自 C++ GUI Programming with Qt4, 2nd Edition。在这个例子中,咱们将建立左右两个并列的列表,能够实现两者之间数据的相互拖动。orm
class ProjectListWidget : public QListWidget { Q_OBJECT public: ProjectListWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void dragEnterEvent(QDragEnterEvent *event); void dragMoveEvent(QDragMoveEvent *event); void dropEvent(QDropEvent *event); private: void performDrag(); QPoint startPos; };
ProjectListWidget
是咱们的列表的实现。这个类继承自QListWidget
。在最终的程序中,将会是两个ProjectListWidget
的并列。对象
ProjectListWidget::ProjectListWidget(QWidget *parent) : QListWidget(parent) { setAcceptDrops(true); }
构造函数咱们设置了setAcceptDrops()
,使ProjectListWidget
可以支持拖动操做。继承
void ProjectListWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) startPos = event->pos(); QListWidget::mousePressEvent(event); } void ProjectListWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) performDrag(); } QListWidget::mouseMoveEvent(event); } void ProjectListWidget::performDrag() { QListWidgetItem *item = currentItem(); if (item) { QMimeData *mimeData = new QMimeData; mimeData->setText(item->text()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); drag->setPixmap(QPixmap(":/images/person.png")); if (drag->exec(Qt::MoveAction) == Qt::MoveAction) delete item; } }
mousePressEvent()
函数中,咱们检测鼠标左键点击,若是是的话就记录下当前位置。须要注意的是,这个函数最后须要调用系统自带的处理函数,以便实现一般的那种操做。这在一些重写事件的函数中都是须要注意的,前面咱们已经反复强调过这一点。
mouseMoveEvent()
函数判断了,若是鼠标在移动的时候一直按住左键(也就是 if 里面的内容),那么就计算一个manhattanLength()
值。从字面上翻译,这是个“曼哈顿长度”。首先来看看event.pos() - startPos
是什么。在mousePressEvent()
函数中,咱们将鼠标按下的坐标记录为 startPos,而event.pos()
则是鼠标当前的坐标:一个点减去另一个点,这就是一个位移向量。所谓曼哈顿距离就是两点之间的距离(按照勾股定理进行计算而来),也就是这个向量的长度。而后继续判断,若是大于QApplication::startDragDistance()
,咱们才进行释放的操做。固然,最后仍是要调用系统默认的鼠标拖动函数。这一判断的意义在于,防止用户由于手的抖动等因素形成的鼠标拖动。用户必须将鼠标拖动一段距离以后,咱们才认为他是但愿进行拖动操做,而这一距离就是QApplication::startDragDistance()
提供的,这个值一般是 4px。
performDrag()
开始处理拖放的过程。这里,咱们要建立一个QDrag
对象,将 this 做为 parent。QDrag
使用QMimeData
存储数据。例如咱们使用QMimeData::setText()
函数将一个字符串存储为 text/plain 类型的数据。QMimeData
提供了不少函数,用于存储诸如 URL、颜色等类型的数据。使用QDrag::setPixmap()
则能够设置拖动发生时鼠标的样式。QDrag::exec()
会阻塞拖动的操做,直到用户完成操做或者取消操做。它接受不一样类型的动做做为参数,返回值是真正执行的动做。这些动做的类型通常为Qt::CopyAction
,Qt::MoveAction
和Qt::LinkAction
。返回值会有这几种动做,同时还会有一个Qt::IgnoreAction
用于表示用户取消了拖放。这些动做取决于拖放源对象容许的类型,目的对象接受的类型以及拖放时按下的键盘按键。在exec()
调用以后,Qt 会在拖放对象不须要的时候释放掉。
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event) { ProjectListWidget *source = qobject_cast(event->source()); if (source && source != this) { event->setDropAction(Qt::MoveAction); event->accept(); } } void ProjectListWidget::dropEvent(QDropEvent *event) { ProjectListWidget *source = qobject_cast(event->source()); if (source && source != this) { addItem(event->mimeData()->text()); event->setDropAction(Qt::MoveAction); event->accept(); } }
dragMoveEvent()
和dropEvent()
类似。首先判断事件的来源(source),因为咱们是两个ProjectListWidget
之间相互拖动,因此来源应该是ProjectListWidget
类型的(固然,这个 source 不能是本身,因此咱们还得判断source != this
)。dragMoveEvent()
中咱们检查的是被拖动的对象;dropEvent()
中咱们检查的是释放的对象:这两者是不一样的。