• Qt 学习之路 2(53):自定义拖放数据


    Qt 学习之路 2(53):自定义拖放数据 豆子  2013年5月26日  Qt 学习之路 2  13条评论
    上一章中,我们的例子使用系统提供的拖放对象QMimeData进行拖放数据的存储。比如使用QMimeData::setText()创建文本,使用QMimeData::urls()创建 URL 对象等。但是,如果你希望使用一些自定义的对象作为拖放数据,比如自定义类等等,单纯使用QMimeData可能就没有那么容易了。为了实现这种操作,我们可以从下面三种实现方式中选择一个:

    将自定义数据作为QByteArray对象,使用QMimeData::setData()函数作为二进制数据存储到QMimeData中,然后使用QMimeData::data()读取
    继承QMimeData,重写其中的formats()和retrieveData()函数操作自定义数据
    如果拖放操作仅仅发生在同一个应用程序,可以直接继承QMimeData,然后使用任意合适的数据结构进行存储

    这三种选择各有千秋:第一种方法不需要继承任何类,但是有一些局限:即是拖放不会发生,我们也必须将自定义的数据对象转换成QByteArray对象,在一定程度上,这会降低程序性能;另外,如果你希望支持很多种拖放的数据,那么每种类型的数据都必须使用一个QMimeData类,这可能会导致类爆炸。后两种实现方式则不会有这些问题,或者说是能够减小这种问题,并且能够让我们有完全的控制权。

    下面我们使用第一种方法来实现一个表格。这个表格允许我们选择一部分数据,然后拖放到另外的一个空白表格中。在数据拖动过程中,我们使用 CSV 格式对数据进行存储。

    首先来看头文件:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class DataTableWidget : public QTableWidget
    {
        Q_OBJECT
    public:
        DataTableWidget(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();
        QString selectionText() const;
     
        QString toHtml(const QString &plainText) const;
        QString toCsv(const QString &plainText) const;
        void fromCsv(const QString &csvText);
     
        QPoint startPos;
    };
    这里,我们的表格继承自QTableWidget。虽然这是一个简化的QTableView,但对于我们的演示程序已经绰绰有余。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    DataTableWidget::DataTableWidget(QWidget *parent)
        : QTableWidget(parent)
    {
        setAcceptDrops(true);
        setSelectionMode(ContiguousSelection);
     
        setColumnCount(3);
        setRowCount(5);
    }
     
    void DataTableWidget::mousePressEvent(QMouseEvent *event)
    {
        if (event->button() == Qt::LeftButton) {
            startPos = event->pos();
        }
        QTableWidget::mousePressEvent(event);
    }
     
    void DataTableWidget::mouseMoveEvent(QMouseEvent *event)
    {
        if (event->buttons() & Qt::LeftButton) {
            int distance = (event->pos() - startPos).manhattanLength();
            if (distance >= QApplication::startDragDistance()) {
                performDrag();
            }
        }
    }
     
    void DataTableWidget::dragEnterEvent(QDragEnterEvent *event)
    {
        DataTableWidget *source =
                qobject_cast<DataTableWidget *>(event->source());
        if (source && source != this) {
            event->setDropAction(Qt::MoveAction);
            event->accept();
        }
    }
     
    void DataTableWidget::dragMoveEvent(QDragMoveEvent *event)
    {
        DataTableWidget *source =
                qobject_cast<DataTableWidget *>(event->source());
        if (source && source != this) {
            event->setDropAction(Qt::MoveAction);
            event->accept();
        }
    }
    构造函数中,由于我们要针对两个表格进行相互拖拽,所以我们设置了setAcceptDrops()函数。选择模式设置为连续,这是为了方便后面我们的算法简单。mousePressEvent(),mouseMoveEvent(),dragEnterEvent()以及dragMoveEvent()四个事件响应函数与前面几乎一摸一样,这里不再赘述。注意,这几个函数中有一些并没有调用父类的同名函数。关于这一点我们在前面的章节中曾反复强调,但这里我们不希望父类的实现被执行,因此完全屏蔽了父类实现。下面我们来看performDrag()函数:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void DataTableWidget::performDrag()
    {
        QString selectedString = selectionText();
        if (selectedString.isEmpty()) {
            return;
        }
     
        QMimeData *mimeData = new QMimeData;
        mimeData->setHtml(toHtml(selectedString));
        mimeData->setData("text/csv", toCsv(selectedString).toUtf8());
     
        QDrag *drag = new QDrag(this);
        drag->setMimeData(mimeData);
        if (drag->exec(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction) {
             selectionModel()->clearSelection();
        }
    }
    首先我们获取选择的文本(selectionText()函数),如果为空则直接返回。然后创建一个QMimeData对象,设置了两个数据:HTML 格式和 CSV 格式。我们的 CSV 格式是以QByteArray形式存储的。之后我们创建了QDrag对象,将这个QMimeData作为拖动时所需要的数据,执行其exec()函数。exec()函数指明,这里的拖动操作接受两种类型:复制和移动。当执行的是移动时,我们将已选区域清除。

    需要注意一点,QMimeData在创建时并没有提供 parent 属性,这意味着我们必须手动调用 delete 将其释放。但是,setMimeData()函数会将其所有权转移到QDrag名下,也就是会将其 parent 属性设置为这个QDrag。这意味着,当QDrag被释放时,其名下的所有QMimeData对象都会被释放,所以结论是,我们实际是无需,也不能手动 delete 这个QMimeData对象。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    void DataTableWidget::dropEvent(QDropEvent *event)
    {
        if (event->mimeData()->hasFormat("text/csv")) {
            QByteArray csvData = event->mimeData()->data("text/csv");
            QString csvText = QString::fromUtf8(csvData);
            fromCsv(csvText);
            event->acceptProposedAction();
        }
    }
    dropEvent()函数也很简单:如果是 CSV 类型,我们取出数据,转换成字符串形式,调用了fromCsv()函数生成新的数据项。

    几个辅助函数的实现比较简单:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    QString DataTableWidget::selectionText() const
    {
        QString selectionString;
        QString headerString;
        QAbstractItemModel *itemModel = model();
        QTableWidgetSelectionRange selection = selectedRanges().at(0);
        for (int row = selection.topRow(), firstRow = row;
             row <= selection.bottomRow(); row++) {
            for (int col = selection.leftColumn();
                 col <= selection.rightColumn(); col++) {
                if (row == firstRow) {
                    headerString.append(horizontalHeaderItem(col)->text()).append(" ");
                }
                QModelIndex index = itemModel->index(row, col);
                selectionString.append(index.data().toString()).append(" ");
            }
            selectionString = selectionString.trimmed();
            selectionString.append(" ");
        }
        return headerString.trimmed() + " " + selectionString.trimmed();
    }
     
    QString DataTableWidget::toHtml(const QString &plainText) const
    {
    #if QT_VERSION >= 0x050000
        QString result = plainText.toHtmlEscaped();
    #else
        QString result = Qt::escape(plainText);
    #endif
        result.replace(" ", "<td>");
        result.replace(" ", " <tr><td>");
        result.prepend("<table> <tr><td>");
        result.append(" </table>");
        return result;
    }
     
    QString DataTableWidget::toCsv(const QString &plainText) const
    {
        QString result = plainText;
        result.replace("\", "\\");
        result.replace(""", "\"");
        result.replace(" ", "", "");
        result.replace(" ", "" "");
        result.prepend(""");
        result.append(""");
        return result;
    }
     
    void DataTableWidget::fromCsv(const QString &csvText)
    {
        QStringList rows = csvText.split(" ");
        QStringList headers = rows.at(0).split(", ");
        for (int h = 0; h < headers.size(); ++h) {
            QString header = headers.at(0);
            headers.replace(h, header.replace('"', ""));
        }
        setHorizontalHeaderLabels(headers);
        for (int r = 1; r < rows.size(); ++r) {
            QStringList row = rows.at(r).split(", ");
            setItem(r - 1, 0, new QTableWidgetItem(row.at(0).trimmed().replace('"', "")));
            setItem(r - 1, 1, new QTableWidgetItem(row.at(1).trimmed().replace('"', "")));
        }
    }
    虽然看起来很长,但是这几个函数都是纯粹算法,而且算法都比较简单。注意toHtml()中我们使用条件编译语句区分了一个 Qt4 与 Qt5 的不同函数。这也是让同一代码能够同时应用于 Qt4 和 Qt5 的技巧。fromCsv() 函数中,我们直接将下面表格的前面几列设置为拖动过来的数据,注意这里有一些格式上面的变化,主要用于更友好地显示。

    最后是MainWindow的一个简单实现:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent)
    {
        topTable = new DataTableWidget(this);
        QStringList headers;
        headers << "ID" << "Name" << "Age";
        topTable->setHorizontalHeaderLabels(headers);
        topTable->setItem(0, 0, new QTableWidgetItem(QString("0001")));
        topTable->setItem(0, 1, new QTableWidgetItem(QString("Anna")));
        topTable->setItem(0, 2, new QTableWidgetItem(QString("20")));
        topTable->setItem(1, 0, new QTableWidgetItem(QString("0002")));
        topTable->setItem(1, 1, new QTableWidgetItem(QString("Tommy")));
        topTable->setItem(1, 2, new QTableWidgetItem(QString("21")));
        topTable->setItem(2, 0, new QTableWidgetItem(QString("0003")));
        topTable->setItem(2, 1, new QTableWidgetItem(QString("Jim")));
        topTable->setItem(2, 2, new QTableWidgetItem(QString("21")));
        topTable->setItem(3, 0, new QTableWidgetItem(QString("0004")));
        topTable->setItem(3, 1, new QTableWidgetItem(QString("Dick")));
        topTable->setItem(3, 2, new QTableWidgetItem(QString("24")));
        topTable->setItem(4, 0, new QTableWidgetItem(QString("0005")));
        topTable->setItem(4, 1, new QTableWidgetItem(QString("Tim")));
        topTable->setItem(4, 2, new QTableWidgetItem(QString("22")));
     
        bottomTable = new DataTableWidget(this);
     
        QWidget *content = new QWidget(this);
        QVBoxLayout *layout = new QVBoxLayout(content);
        layout->addWidget(topTable);
        layout->addWidget(bottomTable);
     
        setCentralWidget(content);
     
        setWindowTitle("Data Table");
    }
    这段代码没有什么新鲜内容,我们直接将其跳过。最后编译运行下程序,按下 shift 并点击表格两个单元格即可选中,然后拖放到另外的空白表格中来查看效果。

    下面我们换用继承QMimeData的方法来尝试重新实现上面的功能。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class TableMimeData : public QMimeData
    {
        Q_OBJECT
    public:
        TableMimeData(const QTableWidget *tableWidget,
                      const QTableWidgetSelectionRange &range);
        const QTableWidget *tableWidget() const
        {
            return dataTableWidget;
        }
        QTableWidgetSelectionRange range() const
        {
            return selectionRange;
        }
        QStringList formats() const
        {
            return dataFormats;
        }
    protected:
        QVariant retrieveData(const QString &format,
                              QVariant::Type preferredType) const;
    private:
        static QString toHtml(const QString &plainText);
        static QString toCsv(const QString &plainText);
        QString text(int row, int column) const;
        QString selectionText() const;
     
        const QTableWidget *dataTableWidget;
        QTableWidgetSelectionRange selectionRange;
        QStringList dataFormats;
    };
    为了避免存储具体的数据,我们存储表格的指针和选择区域的坐标的指针;dataFormats 指明这个数据对象所支持的数据格式。这个格式列表由formats()函数返回,意味着所有被 MIME 数据对象支持的数据类型。这个列表是没有先后顺序的,但是最佳实践是将“最适合”的类型放在第一位。对于支持多种类型的应用程序而言,有时候会直接选用第一个符合的类型存储。


    1
    2
    3
    4
    5
    6
    7
    TableMimeData::TableMimeData(const QTableWidget *tableWidget,
                                 const QTableWidgetSelectionRange &range)
    {
        dataTableWidget = tableWidget;
        selectionRange = range;
        dataFormats << "text/csv" << "text/html";
    }
    函数retrieveData()将给定的 MIME 类型作为QVariant返回。参数 format 的值通常是formats()函数返回值之一,但是我们并不能假定一定是这个值之一,因为并不是所有的应用程序都会通过formats()函数检查 MIME 类型。一些返回函数,比如text(),html(),urls(),imageData(),colorData()和data()实际上都是在QMimeData的retrieveData()函数中实现的。第二个参数preferredType给出我们应该在QVariant中存储哪种类型的数据。在这里,我们简单的将其忽略了,并且在 else 语句中,我们假定QMimeData会自动将其转换成所需要的类型:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    QVariant TableMimeData::retrieveData(const QString &format,
                                         QVariant::Type preferredType) const
    {
        if (format == "text/csv") {
            return toCsv(selectionText());
        } else if (format == "text/html") {
            return toHtml(selectionText());
        } else {
            return QMimeData::retrieveData(format, preferredType);
        }
    }
    在组件的dragEvent()函数中,需要按照自己定义的数据类型进行选择。我们使用qobject_cast宏进行类型转换。如果成功,说明数据来自同一应用程序,因此我们直接设置QTableWidget相关数据,如果转换失败,我们则使用一般的处理方式。这也是这类程序通常的处理方式:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    void DataTableWidget::dropEvent(QDropEvent *event)
    {
        const TableMimeData *tableData =
                qobject_cast<const TableMimeData *>(event->mimeData());
     
        if (tableData) {
            const QTableWidget *otherTable = tableData->tableWidget();
            QTableWidgetSelectionRange otherRange = tableData->range();
            // ...
            event->acceptProposedAction();
        } else if (event->mimeData()->hasFormat("text/csv")) {
            QByteArray csvData = event->mimeData()->data("text/csv");
            QString csvText = QString::fromUtf8(csvData);
            // ...
            event->acceptProposedAction();
        }
        QTableWidget::mouseMoveEvent(event);
    }
    由于这部分代码与前面的相似,感兴趣的童鞋可以根据前面的代码补全这部分,所以这里不再给出完整代码。

  • 相关阅读:
    视图转换例子实践
    Xml序列化和反序列化对象使用MemoryStream实践
    iOS中控制器的实践和学习(2)认识XCode4模版(A1,A3,B2简易图)
    iOS中控制器的实践和学习(1)抛出UI问题
    拖动雪花视图实例学习
    <海量数据库解决方案>2011051901
    <海量数据库解决方案>2011051301
    提示(警告)视图的简单应用
    Delphi Prism Visual Studio Pascal For .NET
    Migrating Delphi Applications to .NET
  • 原文地址:https://www.cnblogs.com/okgogo2000/p/8393288.html
Copyright © 2020-2023  润新知