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. SQL语句 表字段的操作 添加,删除,修改表的字段

    alter table 表名 drop constraint 约束名字   //删除字段的原有约束 alter table 表名 add constraint 约束名字 DEFAULT 默认值 for ...

  2. Python基本数据类型--列表、元组、字典、集合

    一.Python基本数据类型--列表(List) 1.定义:[ ]内以逗号分隔,按照索引,存放各种数据类型,每个位置代表一个元素. 2.列表的创建: # 方式一 list1 = ['name','ag ...

  3. eclipse egit(远程仓库)

    Git的强大之一体现在远程仓库,Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上.怎么分布呢?最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且 ...

  4. Asp.net Web Application 打开 SharePoint 2010 Site 错误 The Web application at could not be found

    解决办法如下: 1. 修改项目的.net framework 为3.5 2. Application Pool 选用 Sharepoint App pool 3. 修改 web.config如下: & ...

  5. Eclipse下使用Subversion(SVN工具)

    本文目的 让未使用过版本控制器软件或者未使用过subversion软件的人员尽快上手. subversion的使用技巧很多,这里只总结了最小使用集,即主要的基本功能,能够用来应付日常工作. 因此不涉及 ...

  6. mfs权威指南

    1. 我在性能测试中间遇到些问题,因为我时间有限,所以希望大家一起来测试解决,群策群力.有什么问题请大家及时指出来,因为我也处在一个不断摸索的阶段. 2. mfs不多做介绍,具体细节请参考本版mfs实 ...

  7. Servlet接口应用(开发servlet三种方式)

    参见 文库/java/javaEE全新学习教程2.2节 1.通过URL调用 2通过提交表单 3超链接 4 javascript写一个函数,调用这个函数 1,首先在工程的WebRoot文件夹下建立一个j ...

  8. Content 控件

    转自:http://www.cnblogs.com/superfang/archive/2008/06/29/1232158.html 创建一个服务器控件,该控件包含呈现到母版页中的 ContentP ...

  9. 268. Missing Number序列中遗失的数字

    [抄题]: Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, find the one that is ...

  10. 数据库sql 开窗函数

    --本文采用Oracle数据库测试,前4个查询为一组,后2个查询为一组,每组前面的查询是为了推出最后的查询 --创建表,为了简化处理,字段类型都采用varcharcreate table tb_sc( ...