首页 Qt 学习之路 2 Qt 学习之路 2(52):使用拖放

Qt 学习之路 2(52):使用拖放

22 2

拖放(Drag and Drop),通常会简称为 DnD,是现代软件开发中必不可少的一项技术。它提供了一种能够在应用程序内部甚至是应用程序之间进行信息交换的机制。操作系统与应用程序之间进行的剪贴板内容的交换,也可以被认为是拖放的一部分。

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

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

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中加入了拖放操作。首先要把QTextEditsetAcceptDrops()函数置为 false,并且把MainWindowsetAcceptDrops()置为 true,这样我们就能够让MainWindow截获拖放事件,而不是交给QTextEdit处理。

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()函数读取文件内容。这个函数的内容很简单,我们前面也讲解过有关文件的操作,这里不再赘述。现在可以运行下看看效果了。

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

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::CopyActionQt::MoveActionQt::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()中我们检查的是释放的对象:这二者是不同的。

附件:ProjectChooser

22 评论

Const_Lin 2014年3月23日 - 16:02

豆子你好:
这段代码:“QList urls = event->mimeData()->urls();“编译器报错了。QList 应改为 QList

回复
豆子 2014年3月24日 - 14:08

网页把尖括号省略掉了,已经改过来,多谢指出

回复
datde 2014年5月29日 - 07:48

豆子,拖动含有中文的txt有乱码,怎么才能搞定呢

回复
smalldy 2018年3月19日 - 11:34

content = QString::fromLocal8Bit( file.readAll());

回复
FCB 2014年7月6日 - 18:26

附件:ProjectListWidget中的程序编译不能通过,找不到"ui_projectdialog.h"
而且#include 要改成#include 同时#include 改成#include
(Qt Creator 3.1.2
Qt 5.3.1 (MSVC 2013, 32 bit))不知是怎么回事

回复
豆子 2014年7月9日 - 09:50

已经修改过来了,因为原文件没有更新到 Qt 5,请再试试新的附件

回复
zhangxian 2014年8月12日 - 13:05

请问豆子,我想整行拖动QTable中的数据以对不同行进行排序该怎么做?

回复
豆子 2014年8月16日 - 22:40

一般界面很少有按行拖动,应该需要自己实现。基本原则是,所有的修改都是针对模型数据本身的,view 只做显示用。

回复
Eli 2014年8月15日 - 17:46

on_rightButton_clicked 没有看到connect 任何 signal,它是如何被QToolButton触发的?

回复
豆子 2014年8月16日 - 22:44

on_rightButton_clicked() 这种函数是由 ui 文件生成的槽,Qt 会根据函数名的规律自动连接。这是信号槽的自动连接方式。不过,注意这种自动连接只能通过 ui 文件的方式实现,也就是说,如果你自己定义一个类似命名的函数,Qt 还是不会自动连接的。

回复
DS离心泵 2015年12月11日 - 18:05

豆子老师,用UI生成的信号槽函数可以实现双击移动项目,就是跟点箭头或拖动一样的效果。
我想请教的是如果要自己写这个信号槽的话是怎么个写法?
另外,那个UI设定的信号槽是在哪里定义的啊?能手工修改么?!
谢谢啊~

回复
豆子 2015年12月14日 - 21:54

QAbstractItemView 有 doubleClicked() 信号,QListWidget 之类的则由 itemDoubleClicked() 信号,如果不是这些类的话,需要重写 mouseDoubleClickEvent() 事件。

回复
DS离心泵 2015年12月15日 - 08:48

我看到您上面这句话:
不过,注意这种自动连接只能通过 ui 文件的方式实现,也就是说,如果你自己定义一个类似命名的函数,Qt 还是不会自动连接的。
我想请教的是:是不是说这种UI文件生成的槽必须而且只能是使用系统定义的函数名,不能自己定义或者修改?!
谢谢!

Kian 2015年12月11日 - 14:34

看完以后有个问题,代码中event->buttons() & Qt::LeftButton 和event->buttons() == Qt::LeftButton完全是等价的,平常也是基本采用后一种写法,文中这种写法是有什么优越性吗?

回复
LinWM 2015年12月13日 - 09:16

刚开始看,也不知道两者等不等价,查了帮助文档的枚举值之后,就知道这两种写法在功能上等价。对于这两种写法,个人看法是:1.功能上,二者完全相同;2.性能上:忽略编译器自动优化的话,位运算会比比较运算来的快;3.可读性上:比较运算来的直观。所以,在现代计算机高速发展的阶段,个人倾向于比较运算,当性能上相差不大时,可读性就应该起重要作用,当然,前提是功能上完全相同。

回复
豆子 2015年12月14日 - 21:28

event->buttons() & Qt::LeftButton 和 event->buttons() == Qt::LeftButton 在这里的效果一致,但这两个语句完全不等价(效果一致仅仅是一个特例)。event->buttons() 的返回值是 Qt::MouseButtons,这是一个 flag 类型,可以使用 | 进行按位或的操作。也就是说,event->buttons() 可能返回类似 Qt::LeftButton | Qt::RightButton (鼠标左右键同时按下),因此,要判断“正确的”左键是否按下,只能使用按位与运算。如果平时使用的是后面的写法,有可能存在一定的隐患(当然,后者确定只按下左键,不考虑多建一起按下的情况,也许是更常见的判断)。

回复
DS离心泵 2015年12月15日 - 08:43

叹服~学习就得豆子老师这样~知其然知其所以然才行啊~

回复
LinWM 2015年12月15日 - 08:53

嗯嗯,当时混淆了event->button()和event->buttons(),以为功能上相同,谢谢豆子老师纠正。

回复
SunnyDay 2019年7月15日 - 17:07

豆子老师您好。我想问,如果是反过来,从应用程序中拖动文件到资源管理器里,该如何做?我想要获取目标的路径,因为在移动和复制时我需要作一些处理,获取路径的话,我就可以控制这个过程。就像是在压缩软件中,将某个文件从压缩包里拖放到桌面一样。

回复
看粮的猪 2019年12月3日 - 18:59

QListWidget::mousePressEvent(event);
不懂就问,这个mousePressEvent(event)为什么可以运行,官方文档关于这个函数并没有声明为静态啊????
[virtual protected] void QWidget::mouseMoveEvent(QMouseEvent *event)
This event handler, for event event, can be reimplemented in a subclass to receive mouse move events for the widget.
If mouse tracking is switched off, mouse move events only occur if a mouse button is pressed while the mouse is being moved. If mouse tracking is switched on, mouse move events occur even if no mouse button is pressed.
QMouseEvent::pos() reports the position of the mouse cursor, relative to this widget. For press and release events, the position is usually the same as the position of the last mouse move event, but it might be different if the user's hand shakes. This is a feature of the underlying window system, not Qt.
If you want to show a tooltip immediately, while the mouse is moving (e.g., to get the mouse coordinates with QMouseEvent::pos() and show them as a tooltip), you must first enable mouse tracking as described above. Then, to ensure that the tooltip is updated immediately, you must call QToolTip::showText() instead of setToolTip() in your implementation of mouseMoveEvent().

回复
豆子 2019年12月5日 - 11:04

这里并不是说这个是静态函数,只是说这个函数是 QListWidget 类的函数。

回复
lwei2 2021年10月15日 - 18:06

豆子老师,您好,请教一个问题,就是用Qt实现了拖拽文件的功能,但是如果Qt编译后的应用程序以管理员权限运行则出现无法拖拽文件的功能,这个问题应该要如何解决呢?我在找了很多博客或官网文档,都没有找到有效的办法,不知道您是否知道怎么解决?

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2