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("\t");
            }
            QModelIndex index = itemModel->index(row, col);
            selectionString.append(index.data().toString()).append("\t");
        }
        selectionString = selectionString.trimmed();
        selectionString.append("\n");
    }
    return headerString.trimmed() + "\n" + 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("\t", "<td>");
    result.replace("\n", "\n<tr><td>");
    result.prepend("<table>\n<tr><td>");
    result.append("\n</table>");
    return result;
}
 
QString DataTableWidget::toCsv(const QString &plainText) const
{
    QString result = plainText;
    result.replace("\\", "\\\\");
    result.replace("\"", "\\\"");
    result.replace("\t", "\", \"");
    result.replace("\n", "\"\n\"");
    result.prepend("\"");
    result.append("\"");
    return result;
}
 
void DataTableWidget::fromCsv(const QString &csvText)
{
    QStringList rows = csvText.split("\n");
    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);
}
由于这部分代码与前面的相似,感兴趣的童鞋可以根据前面的代码补全这部分,所以这里不再给出完整代码。

Qt 学习之路 2(53):自定义拖放数据的更多相关文章

  1. Qt 学习之路 2(16):深入 Qt5 信号槽新语法

    Qt 学习之路 2(16):深入 Qt5 信号槽新语法  豆子  2012年9月19日  Qt 学习之路 2  53条评论 在前面的章节(信号槽和自定义信号槽)中,我们详细介绍了有关 Qt 5 的信号 ...

  2. Qt 学习之路 2(14):对话框数据传递

    Home / Qt 学习之路 2 / Qt 学习之路 2(14):对话框数据传递 Qt 学习之路 2(14):对话框数据传递  豆子  2012年9月15日  Qt 学习之路 2  53条评论 对话框 ...

  3. Qt 学习之路 2(13):对话框简介

    Qt 学习之路 2(13):对话框简介  豆子  2012年9月14日  Qt 学习之路 2  53条评论 对话框是 GUI 程序中不可或缺的组成部分.很多不能或者不适合放入主窗口的功能组件都必须放在 ...

  4. Qt 学习之路 2(49):自定义只读模型

    Qt 学习之路 2(49):自定义只读模型 豆子 2013年5月5日 Qt 学习之路 2 18条评论 model/view 模型将数据与视图分割开来,也就是说,我们可以为不同的视图,QListView ...

  5. Qt 学习之路 2(5):自定义信号槽

    Home / Qt 学习之路 2 / Qt 学习之路 2(5):自定义信号槽 Qt 学习之路 2(5):自定义信号槽  豆子  2012年8月24日  Qt 学习之路 2  131条评论 上一节我们详 ...

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

    Qt 学习之路 2(52):使用拖放 豆子 2013年5月21日 Qt 学习之路 2 17条评论 拖放(Drag and Drop),通常会简称为 DnD,是现代软件开发中必不可少的一项技术.它提供了 ...

  7. Qt 学习之路 2(50):自定义可编辑模型

    Home / Qt 学习之路 2 / Qt 学习之路 2(50):自定义可编辑模型 Qt 学习之路 2(50):自定义可编辑模型 豆子 2013年5月13日 Qt 学习之路 2 13条评论 上一章我们 ...

  8. Qt 学习之路 2(23):自定义事件

    Qt 学习之路 2(23):自定义事件  豆子  2012年10月23日  Qt 学习之路 2  21条评论 尽管 Qt 已经提供了很多事件,但对于更加千变万化的需求来说,有限的事件都是不够的.例如, ...

  9. Qt 学习之路 2(51):布尔表达式树模型

    Qt 学习之路 2(51):布尔表达式树模型 豆子 2013年5月15日 Qt 学习之路 2 17条评论 本章将会是自定义模型的最后一部分.原本打算结束这部分内容,不过实在不忍心放弃这个示例.来自于 ...

随机推荐

  1. UDP数据报

    服务器端:Server 函数: 1.inet_addr()://把IP地址转换为长整型2.inet_ntoa();//将长整型转换为IP地址3.socket的阻塞和非阻塞: 阻塞模式下: 在程序中,“ ...

  2. linux单用户模式修改密码

    Linux使用版本: Centos 7 救援模式: 1,在虚拟机设置里查看光驱是否开启启动,要保证设置状态里的两个选项都已选择. 2,先将Centos系统关机,然后在VMware左侧选中这台虚拟机并右 ...

  3. 简单的互斥同步方式——synchronized关键字详解

    目录 1. 关于synchronized关键字 2. synchronized的原理和实现细节 2.1 synchronized可以用在那些地方 2.2 synchronized是如何实现线程互斥访问 ...

  4. 主机不能访问虚拟机中的web服务【解决方案】

    百度了其它一些方法都不行,最后实在没辙,关了windows防火墙和Linux防火墙,居然能够访问了,我服. 总结一下,原来是Red Hat Linux 6.0防火墙没有开启端口80,开启的方法为(老版 ...

  5. php 读取和下载execl

    最近用到php 对excel 的操作 下来 小弟为大家 先贴一下自己的代码  有什么补充的 大神们请指点下.感激不尽. 我用的是yii2   yii2中有类 phpexcel 先说说下载吧. 首先下载 ...

  6. Tensorflow学习(练习)—下载骨骼图像识别网络inception数据集

    import tensorflow as tfimport osimport tarfileimport requests #inception模型下载地址inception_pretrain_mod ...

  7. SqlServer——神奇代码1之Update

    说明:一个带有update的循环的代码.很简单,但是在QQ群里问了,应该说是很少有人注意这个问题,也就是很少有人真的理解SQL中的Update. 代码如下: if object_id('tempdb. ...

  8. hrabs的数据库session的修改

    using System;using System.Data;using System.Collections;using System.Collections.Generic;using Syste ...

  9. JavaScript——Dom编程(2)

    ①.创建一个元素节点: var reference = document.createElement(element) createElement(): 按照给定的标签名创建一个新的元素节点. 方法只 ...

  10. httpd和apache的区别

    今天要配置集成服务器环境 apache + tomcat + php + jsp + mysql  + sqlserver 去下载apache 发现有: apache_2.2.14-win32-x86 ...