拖放其实是由两部分组成的:拖动和释放。拖动是将被拖放对象进行移动,释放是将被拖放对象放下。前者是一个按下鼠标按键并移动的过程,后者是一个松开鼠标按键的过程;通常这两个操作之间的鼠标按键是被一直按下的。当然,这只是一种普遍的情况,其它情况还是要看应用程序的具体实现。对于 Qt 而言,一个组件既可以作为被拖动对象进行拖动,也可以作为释放掉的目的地对象,或者二者都是。

    在下面的例子中(来自 C++ GUI Programming with Qt4, 2nd Edition),我们将创建一个程序,将操作系统中的文本文件拖进来,然后在窗口中读取内容。

    注意到我们需要重写和dropEvent()两个函数。顾名思义,前者是拖放进入的事件,后者是释放鼠标的事件。

    1. : QMainWindow(parent)
    2. {
    3. textEdit = new QTextEdit;
    4. setCentralWidget(textEdit);
    5.  
    6. textEdit->setAcceptDrops(false);
    7. setAcceptDrops(true);
    8.  
    9. setWindowTitle(tr("Text Editor"));
    10. }
    11.  
    12. MainWindow::~MainWindow()
    13. {
    14. }

    在构造函数中,我们创建了QTextEdit的对象。默认情况下,QTextEdit可以接受从其它应用程序拖放过来的文本类型的数据。如果用户把一个文件拖到这面,默认会把文件名插入到光标位置。但是我们希望让MainWindow读取文件内容,而不是仅仅插入文件名,所以我们在MainWindow中加入了拖放操作。首先要把QTextEditsetAcceptDrops()函数置为 false,并且把MainWindowsetAcceptDrops()置为 true,这样我们就能够让MainWindow截获拖放事件,而不是交给QTextEdit处理。

    1. void MainWindow::dropEvent(QDropEvent *event)
    2. {
    3. QList<QUrl> urls = event->mimeData()->urls();
    4. if (urls.isEmpty()) {
    5. return;
    6. }
    7.  
    8. QString fileName = urls.first().toLocalFile();
    9. return;
    10. }
    11.  
    12. if (readFile(fileName)) {
    13. setWindowTitle(tr("%1 - %2").arg(fileName, tr("Drag File")));
    14. }
    15. }
    16.  
    17. bool MainWindow::readFile(const QString &fileName)
    18. {
    19. bool r = false;
    20. QFile file(fileName);
    21. QString content;
    22. if(file.open(QIODevice::ReadOnly)) {
    23. content = file.readAll();
    24. r = true;
    25. }
    26. textEdit->setText(content);
    27. return r;
    28. }

    当用户将对象释放到组件上面时,系统回调dropEvent()函数。我们使用QMimeData::urls()来获得QUrl的一个列表。通常,这种拖动应该只有一个文件,但是也不排除多个文件一起拖动。因此我们需要检查这个列表是否为空,如果不为空,则取出第一个,否则立即返回。最后我们调用函数读取文件内容。这个函数的内容很简单,我们前面也讲解过有关文件的操作,这里不再赘述。现在可以运行下看看效果了。

    接下来的例子也是来自 C++ GUI Programming with Qt4, 2nd Edition。在这个例子中,我们将创建左右两个并列的列表,可以实现二者之间数据的相互拖动。

    ProjectListWidget是我们的列表的实现。这个类继承自QListWidget。在最终的程序中,将会是两个ProjectListWidget的并列。

    1. ProjectListWidget::ProjectListWidget(QWidget *parent)
    2. : QListWidget(parent)
    3. setAcceptDrops(true);
    4. }

    构造函数我们设置了setAcceptDrops(),使ProjectListWidget能够支持拖动操作。

    mouseMoveEvent()函数判断了,如果鼠标在移动的时候一直按住左键(也就是 if 里面的内容),那么就计算一个manhattanLength()值。从字面上翻译,这是个“曼哈顿长度”。首先来看看event.pos() - startPos是什么。在mousePressEvent()函数中,我们将鼠标按下的坐标记录为 startPos,而event.pos()则是鼠标当前的坐标:一个点减去另外一个点,这就是一个位移向量。所谓曼哈顿距离就是两点之间的距离(按照勾股定理进行计算而来),也就是这个向量的长度。然后继续判断,如果大于QApplication::startDragDistance(),我们才进行释放的操作。当然,最后还是要调用系统默认的鼠标拖动函数。这一判断的意义在于,防止用户因为手的抖动等因素造成的鼠标拖动。用户必须将鼠标拖动一段距离之后,我们才认为他是希望进行拖动操作,而这一距离就是QApplication::startDragDistance()提供的,这个值通常是 4px。

    performDrag()开始处理拖放的过程。这里,我们要创建一个QDrag对象,将 this 作为 parent。QDrag使用存储数据。例如我们使用QMimeData::setText()函数将一个字符串存储为 text/plain 类型的数据。QMimeData提供了很多函数,用于存储诸如 URL、颜色等类型的数据。使用QDrag::setPixmap()则可以设置拖动发生时鼠标的样式。QDrag::exec()会阻塞拖动的操作,直到用户完成操作或者取消操作。它接受不同类型的动作作为参数,返回值是真正执行的动作。这些动作的类型一般为Qt::CopyActionQt::MoveActionQt::LinkAction。返回值会有这几种动作,同时还会有一个Qt::IgnoreAction用于表示用户取消了拖放。这些动作取决于拖放源对象允许的类型,目的对象接受的类型以及拖放时按下的键盘按键。在exec()调用之后,Qt 会在拖放对象不需要的时候释放掉。

    1. void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
    2. {
    3. ProjectListWidget *source =
    4. qobject_cast(event->source());
    5. if (source && source != this) {
    6. event->setDropAction(Qt::MoveAction);
    7. event->accept();
    8. }
    9. }
    10.  
    11. void ProjectListWidget::dropEvent(QDropEvent *event)
    12. {
    13. ProjectListWidget *source =
    14. qobject_cast(event->source());
    15. if (source && source != this) {
    16. addItem(event->mimeData()->text());
    17. event->setDropAction(Qt::MoveAction);
    18. event->accept();
    19. }
    20. }

    dragMoveEvent()dropEvent()相似。首先判断事件的来源(source),由于我们是两个ProjectListWidget之间相互拖动,所以来源应该是ProjectListWidget类型的(当然,这个 source 不能是自己,所以我们还得判断source != this)。dragMoveEvent()中我们检查的是被拖动的对象;dropEvent()中我们检查的是释放的对象:这二者是不同的。

    附件: