Loading

Qt笔记 拖放原理

拖放原理

拖放的基本原理

  1. 拖放操作包括两个动作:拖动(drag)和放下(drop 或称为放置)。当被拖动时拖动的数据会 被存储为 MIME 类型的对象,MIME 类型使用 QMimeData 类来描述。MIME 类型通常由剪贴板和拖放系统使用,以识别不同类型的数据。

  2. 拖动点(drag site):拖动的起始位置。

  3. 放下点(drop site):被拖动的对象放下的位置,若部件不能接受拖动的对象,Qt 会改变光 标的形状(一个禁用形状)来向用户进行说明。

  4. 启动拖放:拖放通过调用 QDrag::exec()函数而启动,该函数是一个阻塞函数(但不会阻塞主事件循环),这意味着在拖放操作结束之前,不会返回该函数,调用 QDrag::exec() 函数后,Qt 拥有对拖动对象的所有权,并会在必要时将其删除。

  5. 结束拖放:当用户放下拖动或取消拖动操作时结束拖放。

拖放产生的过程及事件

  1. 启动拖放后,会使数据被拖动,这时需要按住鼠标按键才能拖动需要拖动的数据,松开鼠标按键时意味着拖动结束。
  2. 默认情况下,部件不接受放下事件。使用 QWidget::setAcceptDrops()函数可设置部件 是否接受放下事件(即,拖放完成时发送的事件)。只有在部件接受放下事件的情形下, 才会产生以下事件:
    • QDragEnterEvent:拖动进入事件。当拖动操作进入部件时,该事件被发送到部件, 忽略该事件,将会导至后续的拖放事件不能被发送。通常在该部件上光标会在外观上 显示为禁用的图形。
    • QDragMoveEvnet:拖动移动事件。当拖动操作正在进行时,以及当具有焦点时按下 键盘的修饰键(比如 Ctrl)时,发送该事件,要使部件能接收到该事件,则该部件必须接 受 QDragEnterEvent 事件。
    • QDropEvent:放下事件。在完成拖放操作时发送该事件,即当用户在部件上放下一个 对象时,发送此事件。要使部件能接收到该事件,则该部件必须接受 QDragEnterEvent 事件,且不能忽略 QDragMoveEvent 事件。
    • QLeaveEvent:当拖放操作离开部件时发送该事件,注意:要使部件能接收到该事件, 必须要使拖动先进入该部件(即产生 QDragEnterEvent 事件),然后再离开该部件,才会产生 QLeaveEvent 事件。因很少使用该事件,因此本文不做重点介绍。
  3. 注:必须接受是指必须重新实现该事件的处理函数并接受该事件,不能忽略是指在处件事理函数中不明确调用 ignore()函数忽略该事件,这意味着可以不必重新实现该事件的处理函数。
  4. 以上事件产生的顺序为:QDragEnterEventQDragMoveEvnetQDropEvent

image-20210911065547816

编写拖放程序的步骤

  1. 在需要接受放下数据的部件上调用 QWidget::setAcceptDrops()函数以使该部件能接受拖放事件。

    ProjectListWidget::ProjectListWidget(QWidget *parent)
        : QListWidget(parent)
    {
        setAcceptDrops(true);
    }
    
  2. 启动拖放:通常在 mousePressEvent()mouseMoveEvent()函数中启动拖放,记住启动拖放就是调用 QDrag 对象的 exec()函数,因此也可以在 keyPressEvent()等函数中启动拖放(因很少这样做,所以本文不介绍这种情况下的拖放)。在此步把需要拖动的数据保存在 QMimeData 对象中。

    // 可以单独写一个函数,在mouseMoveEvent中进行调用
    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;
        }
    }
    
    
  3. 重新实现需要接受放下数据的部件的 dragEnterEvent()事件处理函数。

    void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event)
    {
        ProjectListWidget *source =
                qobject_cast<ProjectListWidget *>(event->source());
        if (source && source != this) {
            event->setDropAction(Qt::MoveAction);
            event->accept();
        }
    }
    
  4. 根据需要重新实现 dragMoveEventdropEvent()函数。

    // dragMoveEvent内容基本和dragEnterEvent内容等同
    // 在dropEvent中记得调用QDropEvent的setDropAction和accept
    void ProjectListWidget::dropEvent(QDropEvent *event)
    {
        ProjectListWidget *source =
                qobject_cast<ProjectListWidget *>(event->source());
        if (source && source != this) {
            addItem(event->mimeData()->text());
            event->setDropAction(Qt::MoveAction);
            event->accept();
        }
    }
    

拖放动作(或称为放置动作)

  1. 拖放动作是指用户希望怎样处理拖放的数据,比如移动、复制、还是创建由目标到源的链 接等。拖放动作由 Qt::DropAction 枚举描述。

  2. 可能的拖放动作、实际的拖放动作以及建议的拖放动作 :

  • 可能的拖放动作:是指用户在拖放时可能会执行的拖放动作,用户在拖放时通常可由用户选择,比如可以选择移动、复制或链接等动作中的一种,这些动作都是可能的拖放动作。可能的拖放动作在QDrag::exec()函数的第 1 个参数中指定,同时该函数的返回值是最终的实际拖放动作。
  • 实际的拖放动作:是指拖放被最终放置时实际执行的动作,实际拖放动作在 dropEvent() 函数中使用 QDropEvent::setDropAction()函数(还需在之后调用 accept()函数)设置,该函数会影响 QDrag::exec()函数的返回值。
  • 建议的拖放动作:是指当用户执行拖动而不使用修饰键时的默认动作,建议拖放动作可在 QDrag::exec()函数的第 2 个参数中指定,该参数的设置会影响拖动时鼠标光标右下角的外观,另外 QDropEvent::acceptProposedAction()函数表示设置执行操作为建议操作并接受该事件。
  • 以上三种拖放动作常常相互关联,比如用户在拖动时通常可以执行移动、复制或链接等动作(可能的拖放动作)中的一种,然而应用程序在拖放被最终放置时并不知道用户 到底需要执行哪种操作,若用户未指定需要执行的可能的拖放动作中的哪一种动作时, 应用程序可以使用设置的建议动作,作为需要执行的动作。

3、各拖放动作之间的关系

  1. QDrag::exec()函数的规则
    • QDrag::exec()未指定建议拖放动作,则依顺序移动、复制、链接进行选择
    • QDrag::exec()函数在第 2 个参数上指定了建议拖放动作,但该动作不在可能的拖放动作组合之中,则使用默认的复制拖放动作。
  2. QDropEvent::setDropAction()函数的规则
    • 使用 setDropAction()函数设置拖放动作之后应使用 accept()函数,而不应使用 QDropEvent::acceptProposedAction()函数(因为该函数会重置拖放动作为建议拖放动作)
    • QDropEvent::setDropAction()函数设置的拖放动作不在可能的拖放动作组合之中,则使用建议拖放动作。
  3. dropEvent()函数的规则,该函数是否接受事件直接影响到 QDrag::exec()函数的返回值, 其规则如下 :
    • 若在该函数内调用 ignore(),则 exec()函数返回 Qt::IgnoreAction
    • 若在该函数内调用 accept(),则 exec()函数返回在该函数中使用 setDropAction() 函数设置的拖放动作。

拖放动作及拖放的程序设计原则

  1. 若在 mouseMoveEvent()函数中启动拖放,则可以编写避免用户因为手握鼠标抖动而产生的拖动,这比在 mousePressEvent()函数中启动拖放效果更好。

    // 在类中定义一个QPoint成员变量startPos
    
    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); // 让父类正常处理事件
    }
    
  2. QDrag::exec()函数的参数中指定可能的拖放动作,比如在其中同时指定移动、复制、 链接等;但最终是否接受这些动作,由后续的事件处理函数进行判断,详见后文。另 外需要注意的是 QDrag::exec()函数虽是阻塞函数,但在执行完该函数(比如释放鼠标按钮完成拖放时)后程序会返回该函数,然后接着执行之后的语句,exec()函数返回的是用户实际执行的动作。

  3. dragEnterEvent()函数通常根据该部件或实际使用情况进行筛选,比如若该部件不接受图片数据,则忽略对该动作的接受,从而阻止事件被进一步传递。

  4. 若重新实现了 dragMoveEvent()函数,则还可以在该函数内进行进一步的设置,比如 默认为复制动作,若用户拖动的同时按下了 Shift 键,则设置为移动动作,若按下了 Ctrl 键,则为复制动作,若按下了 Alt 键则为链接动作等,在该函数中的设置可以影 响鼠标光标在外观上的显示,比如复制会在光标右下角显示一个“+”符号等。另外, 在该函数内还可以设置用户拖动到该部件中的范围,比如拖动到某矩形范围内时,该 部件才接受拖放,否则被忽略等。注意:若在 dragEnterEvent()函数内也设置了拖放动 作,同样会改变光标的外观但只会改变进入部件时的外观,光标最终的外观以 dragMoveEvent()函数设置的为准(因为该函数位于 dragEnterEvent()之后执行)。

    void dragMoveEvent(QDragMoveEvent *e)
    {
        /*以下设置会改变鼠标光标的外观。若拖动的同时按下了 CTRL、ALT、SHIFT 键,则把施放动作设置为复制、移动、链接,否则为复制。*/
        if (e->keyboardModifiers() == Qt::CTRL)
            e->setDropAction(Qt::CopyAction);
        else if (e->keyboardModifiers() == Qt::SHIFT)
            e->setDropAction(Qt::MoveAction);
        else if (e->keyboardModifiers() == Qt::ALT)
            e->setDropAction(Qt::LinkAction);
        else
            e->setDropAction(Qt::CopyAction);
        //若光标位于矩形 r 之内,则接受该事件,否则忽略该事件。
        QRect r(0, 0, 111, 33);
        if (r.contains(e->pos()))
        {
            e->accept();
        }
        else
            e->ignore();
    }
    
  5. dropEvent()函数内最终决定对拖放的数据的处理,以及用户实际执行的拖放动作, 因此该函数决定着 QDrag::exec()函数的返回值,这里要注意的是,对于移动动作,通常原始数据应由源部件(启动拖放的部件)进行删除,因此当 dropEvent()处理完数据之后,应把拖放动作设置为移动,QDrag::exec()函数会返回在 dropEvent()函数中设置的 动作,然后源部件根据 QDrag::exec()的返回值是否是移动动作,而作出是否删除原始数据的决定。注:dragEnterEvent()dragMoveEvent()对拖放动作的设置不会影响 QDrag::exec()的返回值。

    // 示例一中的dropEvent,根据辅助键设置拖放动作
    void DLabel::dropEvent(QDropEvent* e)
    {
        if(e->source()!=this)
        {
            setText(e->mimeData()->text());
            if(e->keyboardModifiers()==Qt::CTRL)
            {
                e->setDropAction(Qt::MoveAction);
            }
            else
            {
                e->setDropAction(Qt::CopyAction);
            }
            e->accept();
        }
    }
    
    // 示例二中的拖放执行函数,如果拖放动作为移动,删除原始数据
    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; // 如果是移动操作,删除原对象
        }
    }
    
    
  6. 注意:在源部件中创建的 QMimeDataQDrag 对象不应由程序员销毁,因为 Qt 会自动销毁,若程序员销毁了,则可能会出现多次 delete 同一个指针的错误。

  7. 以上只是通常在各函数中的做法,当然你也可以不按照这些步骤来实现,从之前的拖放示例可以看到,对拖放的处理完全是任意的。

  8. 注意:Qt为某些部件提供了一些标准的拖放支持,在继承这些部件实现拖放时需要重新实现 dragEnterEvent()dropEvent(),另外还可能需要重新实现 dragMoveEvent() 函数,以避免与标准实现的拖放支持相冲突或产生预料之外的结果。

使用拖放打开文件

拖放文件的基本步骤:文件需要使用 QFile 类来打开,然后才能读取或存入其内容,因此对 拖放的文件进行处理,其实就是获取文件的地址,而地址是使用 URL 来表示的,因此首先 需要判断拖放的数据是否含有 URL,然后读取出 URL 中保存的文件的地址,再打开文件, 然后读取文件的内容,有关流和文件的内容本章暂时不用深入了解,明白以下示例代码的作用即可。

// 示例:拖放文件
// m.h 文件的内容
#ifndef M_H
#define M_H
#include <QtWidgets>
class C : public QPushButton
{
    Q_OBJECT
public:
    C(QString t = "", QWidget *p = 0) : QPushButton(t, p) {}
    void dragEnterEvent(QDragEnterEvent *e)
    {
        //若拖动的数据包含一个 URL 则接受该事件,否则忽略该事件。
        if (e->mimeData()->hasUrls())
        {
            e->accept();
        }
        else
            e->ignore();
    }

    void dropEvent(QDropEvent *e)
    {
        const QMimeData *pm = e->mimeData();
        QList<QUrl> u = pm->urls();          //读取出 URL 的地址列表。
        QString pth = u.at(0).toLocalFile(); //将地址转换为 QString
        if (!pth.isEmpty())
        {                    //判断地址 pth 是否为空
            QFile file(pth); //创建文件 file
            if (!file.open(QIODevice::ReadOnly))
                ;                  //以只读方式打开文件
            QTextStream in(&file); //创建流用于读取文件的内容。
            setText(in.readAll());
        }
    } //读出文件的内容,并设置为该部件的文本
};
class B : public QWidget
{
    Q_OBJECT public : B(QWidget *p = 0) : QWidget(p)
    {

        /*注:实际编程时只需把按钮替换为 QTexeEdit 之类的部件即可,此处为避免复杂性及明白其原理,使用简单的按钮就可以了。*/
        C *pb1 = new C("AAA", this);
        pb1->move(22, 22);
        pb1->setAcceptDrops(true);
    }
};
#endif // M_H

// m.cpp 文件的内容
#include "m.h"
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    B w;
    w.resize(444, 355);
    w.show();
    return app.exec();
}

拖放自定义类型数据

子类化 QMimeData

1、MIME 类型数据的存储 由 AAA 向 BBB 拖动时 拖动完成后按钮 BBB 的图标和文本都被成功修改。 MIME 类型不属于 Qt 类型或 C++类型,与这些类型关联的数据需要被存储,为此需要指 定一个存储 MIMI 类型的 Qt 类型(或 C++类型),并使用一个对象来存储该类型的数据, 比如对于 MIME 类型 XXX,与 XXX 关联的数据为两个字符串"SSS"和"TTT",那么"SSS" 和"TTT"需要被存储在应用程序中,比如可以使用 QByteArray 类型的对象 ba 来存储它们 (也可使用 QList 或其他类来存储),那么与 MIME 类型相关联的类型是 QByteArray 类型, MIME 类型的数据"SSS"和"TTT"被存储在 ba 中。

2、由以上原理可知,QMimeData 的本质就是用于把需要用于传输的数据保存在该类的对象中,因此最小的子类化 QMimeData 类只需在该类中定义一些保存数据的成员变量就可以了。并不需要重新实现 hasFormat()formats()retrieveData()等虚函数,但这样做的话, 没有任何对 MIME 类型的限制和指定,因此 MIME 类型就变得毫无意义了,见下面的示例

//示例:最小子类化 QMimeData(即 QMimeData 本质)
//m.h 文件的内容
#ifndef M_H
#define M_H
#include <QtWidgets>

//对于此类 MIME 类型毫无意义。只需声明以下 3 个成员变量来保存需要传输的数据即可。
class D : public QMimeData
{
public:
    QIcon c;
    QString s;
    int i;
};

class C : public QPushButton
{
    Q_OBJECT
public:
    C(QString t = "", QWidget *p = 0) : QPushButton(t, p) {}
    void mouseMoveEvent(QMouseEvent *e)
    {
        QDrag *dg = new QDrag(this);
        D *md = new D;
        //直接访问类 D 的成员变量,并把需要传输的数据直接保存在成员变量中。
        md->c = QIcon("F:/1i.png");
        md->s = "ZZZ";
        dg->setMimeData(md);
        dg->exec();
    }
    void dragEnterEvent(QDragEnterEvent *e) { e->acceptProposedAction(); }
    void dropEvent(QDropEvent *e)
    {
        const D *dg = (D *)e->mimeData(); //强制转换为子类 D
        //直接访问其成员变量
        setIcon(dg->c);
        setText(dg->s);
        e->accept();
    }
};

class B : public QWidget
{
    Q_OBJECT public : B(QWidget *p = 0) : QWidget(p)
    {
        C *pb1 = new C("AAA", this);
        pb1->move(22, 22);
        pb1->setIcon(QIcon("F:/1i.png"));
        C *pb2 = new C("BBB", this);
        pb2->move(99, 22);
        pb2->setAcceptDrops(true);
        pb1->setAcceptDrops(false);
    }
};
#endif // M_H

//m.cpp 文件的内容。
#include "m.h"
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    B w;
    w.resize(444, 355);
    w.show();
    return app.exec();
}

重新实现 QMimeData 类中的虚函数

1、QMimeData 类中的虚函数 hasFormat()、formats()、retrieveData()主要用于对 MIME 类型进行判别,但并未提供存储数据的功能,因此仅仅实现这些虚函数是无法存储数据的,要使数据被存储,可以在 QMimeData 中声明成员变量,并把数据存储在其中,若想更完美, 则可以像 QMimeData 一样,再定义一些读取函数、设置函数和测试函数。

// 示例:重新实现 QMimeData 类中的虚函数
// m.h 文件的内容
#ifndef M_H #define M_H
#include <QtWidgets>

class E
{
public:
    QIcon c;
    QString s;
};
class D : public QMimeData
{
public:
    E me;           //使用自定义类型 E 存储自定义的 MIME 类型的数据
    QStringList st; //用于存储自定义的 MIME 类型
                    //存储 MIME 类型的数据
    void setXXX(QString m, E e)
    {
        if (!hasFormat(m))
            st.append(m); //若 MIME 类型 m 不存在于列表 st 之中,则追加到 st 中。
        me.c = e.c;
        me.s = e.s;
    } //存储数据
      //若 MIME 类型存在,则返回 me
    E XXX(QString m) const
    {
        if (hasFormat(m))
            return me;
        else
            return E();
    }
    QStringList formats() const { return st; } //返回自定义的 MIME 类型列表。
    bool hasFormat(const QString &m) const
    {
        if (st.contains(m))
            return true; //判断自定义类型 m 是否包含在 st 之中
        //调用父类的函数以处理 text/html 等常见 MIME 类型的数据。
        else
            return QMimeData::hasFormat(m);
    }

    //该函数通常由读取函数调用,本示例不需要调用此函数。
    QVariant retrieveData(const QString &m, QVariant::Type type) const
    {
        return QMimeData::retrieveData(m, type);
    }
};

class C : public QPushButton
{
    Q_OBJECT
public:
    C(QString t = "", QWidget *p = 0) : QPushButton(t, p) {}
    void mouseMoveEvent(QMouseEvent *e)
    {
        QDrag *dg = new QDrag(this);
        D *md = new D;
        E me;
        me.c = QIcon("F:/1i.png");
        me.s = "111";
        md->setXXX("XXX", me); //创建一个自定义的 MIME 类型 XXX,其包含的数据位于 me 之中。

        //创建两个自定义的 MIME 类型 CCC 和 DDD。由此可见,本示例可以一次创建多个 MIME 类型。

        md->st << "CCC"
               << "DDD";
        E me1;
        me1.c = QIcon("F:/1i.png");
        me1.s = "222";
        md->setXXX("CCC", me1); //自定义 MIME 类型包含的数据位于 me1 之中。
        dg->setMimeData(md);
        dg->exec();
    }

    void dragEnterEvent(QDragEnterEvent *e) { e->acceptProposedAction(); }

    void dropEvent(QDropEvent *e)
    {
        const D *dg = (D *)e->mimeData(); //强制转换为子类 D
        //若 CCC 是合格的自定义 MIME 类型,则把其图标和文本设置为该类型所关联的数据。
        //本例未使用自定义 MIME 类型 XXX 的数据。
        if (dg->hasFormat("CCC"))
        {
            setIcon(dg->XXX("CCC").c);
            setText(dg->XXX("CCC").s);
        }
        e->accept();
    }
};

class B : public QWidget
{
    Q_OBJECT public : B(QWidget *p = 0) : QWidget(p)
    {
        C *pb1 = new C("AAA", this);
        pb1->move(22, 22);
        pb1->setIcon(QIcon("F:/1i.png"));
        C *pb2 = new C("BBB", this);
        pb2->move(99, 22);
        pb2->setAcceptDrops(true);
        pb1->setAcceptDrops(false);
    }
};
#endif // M_H

//m.cpp 文件的内容。
#include "m.h"
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    B w;
    w.resize(444, 355);
    w.show();
    return app.exec();
}

posted @ 2021-09-17 09:25  橘崽崽啊  阅读(645)  评论(0编辑  收藏  举报