找回密码
 立即注册
收起左侧

Qt学习之路第53篇 自定义拖放数据

1
回复
10631
查看
[复制链接]
累计签到:3 天
连续签到:1 天
来源: 2013-9-10 10:09:43 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
版权声明

该文章原创于Qter开源社区(www.qter.org),作者devbean,博客www.devbean.net,转载请注明出处!


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



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


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



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



首先来看头文件:

  1. class DataTableWidget : public QTableWidget
  2. {
  3.     Q_OBJECT
  4. public:
  5.     DataTableWidget(QWidget *parent = 0);
  6. protected:
  7.     void mousePressEvent(QMouseEvent *event);
  8.     void mouseMoveEvent(QMouseEvent *event);
  9.     void dragEnterEvent(QDragEnterEvent *event);
  10.     void dragMoveEvent(QDragMoveEvent *event);
  11.     void dropEvent(QDropEvent *event);
  12. private:
  13.     void performDrag();
  14.     QString selectionText() const;

  15.     QString toHtml(const QString &plainText) const;
  16.     QString toCsv(const QString &plainText) const;
  17.     void fromCsv(const QString &csvText);

  18.     QPoint startPos;
  19. };
复制代码

这里,我们的表格继承自 QTableWidget。虽然这是一个简化的 QTableView,但对于我们的演示程序已经绰绰有余。

  1. DataTableWidget::DataTableWidget(QWidget *parent)
  2.     : QTableWidget(parent)
  3. {
  4.     setAcceptDrops(true);
  5.     setSelectionMode(ContiguousSelection);

  6.     setColumnCount(3);
  7.     setRowCount(5);
  8. }

  9. void DataTableWidget::mousePressEvent(QMouseEvent *event)
  10. {
  11.     if (event->button() == Qt::LeftButton) {
  12.         startPos = event->pos();
  13.     }
  14.     QTableWidget::mousePressEvent(event);
  15. }

  16. void DataTableWidget::mouseMoveEvent(QMouseEvent *event)
  17. {
  18.     if (event->buttons() & Qt::LeftButton) {
  19.         int distance = (event->pos() - startPos).manhattanLength();
  20.         if (distance >= QApplication::startDragDistance()) {
  21.             performDrag();
  22.         }
  23.     }
  24. }

  25. void DataTableWidget::dragEnterEvent(QDragEnterEvent *event)
  26. {
  27.     DataTableWidget *source =
  28.             qobject_cast<DataTableWidget *>(event->source());
  29.     if (source && source != this) {
  30.         event->setDropAction(Qt::MoveAction);
  31.         event->accept();
  32.     }
  33. }

  34. void DataTableWidget::dragMoveEvent(QDragMoveEvent *event)
  35. {
  36.     DataTableWidget *source =
  37.             qobject_cast<DataTableWidget *>(event->source());
  38.     if (source && source != this) {
  39.         event->setDropAction(Qt::MoveAction);
  40.         event->accept();
  41.     }
  42. }
复制代码

构造函数中,由于我们要针对两个表格进行相互拖拽,所以我们设置了 setAcceptDrops() 函数。选择模式设置为连续,这是为了方便后面我们的算法简单。mousePressEvent(),mouseMoveEvent(),dragEnterEvent() 以及 dragMoveEvent() 四个事件响应函数与前面几乎一摸一样,这里不再赘述。注意,这几个函数中有一些并没有调用父类的同名函数。关于这一点我们在前面的章节中曾反复强调,但这里我们不希望父类的实现被执行,因此完全屏蔽了父类实现。下面我们来看 performDrag() 函数:

  1. void DataTableWidget::performDrag()
  2. {
  3.     QString selectedString = selectionText();
  4.     if (selectedString.isEmpty()) {
  5.         return;
  6.     }

  7.     QMimeData *mimeData = new QMimeData;
  8.     mimeData->setHtml(toHtml(selectedString));
  9.     mimeData->setData("text/csv", toCsv(selectedString).toUtf8());

  10.     QDrag *drag = new QDrag(this);
  11.     drag->setMimeData(mimeData);
  12.     if (drag->exec(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction) {
  13.          selectionModel()->clearSelection();
  14.     }
  15. }
复制代码

首先我们获取选择的文本(selectionText() 函数),如果为空则直接返回。然后创建一个 QMimeData 对象,设置了两个数据:HTML 格式和 CSV 格式。我们的 CSV 格式是以 QByteArray 形式存储的。之后我们创建了 QDrag 对象,将这个 QMimeData 作为拖动时所需要的数据,执行其 exec() 函数。exec() 函数指明,这里的拖动操作接受两种类型:复制和移动。当执行的是移动时,我们将已选区域清除。



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

  1. void DataTableWidget::dropEvent(QDropEvent *event)
  2. {
  3.     if (event->mimeData()->hasFormat("text/csv")) {
  4.         QByteArray csvData = event->mimeData()->data("text/csv");
  5.         QString csvText = QString::fromUtf8(csvData);
  6.         fromCsv(csvText);
  7.         event->acceptProposedAction();
  8.     }
  9. }
复制代码

dropEvent() 函数也很简单:如果是 CSV 类型,我们取出数据,转换成字符串形式,调用了 fromCsv() 函数生成新的数据项。

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

  1. QString DataTableWidget::selectionText() const
  2. {
  3.     QString selectionString;
  4.     QString headerString;
  5.     QAbstractItemModel *itemModel = model();
  6.     QTableWidgetSelectionRange selection = selectedRanges().at(0);
  7.     for (int row = selection.topRow(), firstRow = row;
  8.          row <= selection.bottomRow(); row++) {
  9.         for (int col = selection.leftColumn();
  10.              col <= selection.rightColumn(); col++) {
  11.             if (row == firstRow) {
  12.                 headerString.append(horizontalHeaderItem(col)->text()).append("\t");
  13.             }
  14.             QModelIndex index = itemModel->index(row, col);
  15.             selectionString.append(index.data().toString()).append("\t");
  16.         }
  17.         selectionString = selectionString.trimmed();
  18.         selectionString.append("\n");
  19.     }
  20.     return headerString.trimmed() + "\n" + selectionString.trimmed();
  21. }

  22. QString DataTableWidget::toHtml(const QString &plainText) const
  23. {
  24. #if QT_VERSION >= 0x050000
  25.     QString result = plainText.toHtmlEscaped();
  26. #else
  27.     QString result = Qt::escape(plainText);
  28. #endif
  29.     result.replace("\t", "<td>");
  30.     result.replace("\n", "\n<tr><td>");
  31.     result.prepend("<table>\n<tr><td>");
  32.     result.append("\n</table>");
  33.     return result;
  34. }

  35. QString DataTableWidget::toCsv(const QString &plainText) const
  36. {
  37.     QString result = plainText;
  38.     result.replace("\", "\\\");
  39.     result.replace(""", "\\"");
  40.     result.replace("\t", "", "");
  41.     result.replace("\n", ""\n"");
  42.     result.prepend(""");
  43.     result.append(""");
  44.     return result;
  45. }

  46. void DataTableWidget::fromCsv(const QString &csvText)
  47. {
  48.     QStringList rows = csvText.split("\n");
  49.     QStringList headers = rows.at(0).split(", ");
  50.     for (int h = 0; h < headers.size(); ++h) {
  51.         QString header = headers.at(0);
  52.         headers.replace(h, header.replace('"', ""));
  53.     }
  54.     setHorizontalHeaderLabels(headers);
  55.     for (int r = 1; r < rows.size(); ++r) {
  56.         QStringList row = rows.at(r).split(", ");
  57.         setItem(r - 1, 0, new QTableWidgetItem(row.at(0).trimmed().replace('"', "")));
  58.         setItem(r - 1, 1, new QTableWidgetItem(row.at(1).trimmed().replace('"', "")));
  59.     }
  60. }
复制代码

虽然看起来很长,但是这几个函数都是纯粹算法,而且算法都比较简单。注意 toHtml() 中我们使用条件编译语句区分了一个 Qt4 与 Qt5 的不同函数。这也是让同一代码能够同时应用于 Qt4 和 Qt5 的技巧。fromCsv() 函数中,我们直接将下面表格的前面几列设置为拖动过来的数据,注意这里有一些格式上面的变化,主要用于更友好地显示。



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

  1. MainWindow::MainWindow(QWidget *parent) :
  2.     QMainWindow(parent)
  3. {
  4.     topTable = new DataTableWidget(this);
  5.     QStringList headers;
  6.     headers << "ID" << "Name" << "Age";
  7.     topTable->setHorizontalHeaderLabels(headers);
  8.     topTable->setItem(0, 0, new QTableWidgetItem(QString("0001")));
  9.     topTable->setItem(0, 1, new QTableWidgetItem(QString("Anna")));
  10.     topTable->setItem(0, 2, new QTableWidgetItem(QString("20")));
  11.     topTable->setItem(1, 0, new QTableWidgetItem(QString("0002")));
  12.     topTable->setItem(1, 1, new QTableWidgetItem(QString("Tommy")));
  13.     topTable->setItem(1, 2, new QTableWidgetItem(QString("21")));
  14.     topTable->setItem(2, 0, new QTableWidgetItem(QString("0003")));
  15.     topTable->setItem(2, 1, new QTableWidgetItem(QString("Jim")));
  16.     topTable->setItem(2, 2, new QTableWidgetItem(QString("21")));
  17.     topTable->setItem(3, 0, new QTableWidgetItem(QString("0004")));
  18.     topTable->setItem(3, 1, new QTableWidgetItem(QString("Dick")));
  19.     topTable->setItem(3, 2, new QTableWidgetItem(QString("24")));
  20.     topTable->setItem(4, 0, new QTableWidgetItem(QString("0005")));
  21.     topTable->setItem(4, 1, new QTableWidgetItem(QString("Tim")));
  22.     topTable->setItem(4, 2, new QTableWidgetItem(QString("22")));

  23.     bottomTable = new DataTableWidget(this);

  24.     QWidget *content = new QWidget(this);
  25.     QVBoxLayout *layout = new QVBoxLayout(content);
  26.     layout->addWidget(topTable);
  27.     layout->addWidget(bottomTable);

  28.     setCentralWidget(content);

  29.     setWindowTitle("Data Table");
  30. }
复制代码

这段代码没有什么新鲜内容,我们直接将其跳过。最后编译运行下程序,按下 shift 并点击表格两个单元格即可选中,然后拖放到另外的空白表格中来查看效果。



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

  1. class TableMimeData : public QMimeData
  2. {
  3.     Q_OBJECT
  4. public:
  5.     TableMimeData(const QTableWidget *tableWidget,
  6.                   const QTableWidgetSelectionRange &range);
  7.     const QTableWidget *tableWidget() const
  8.     {
  9.         return dataTableWidget;
  10.     }
  11.     QTableWidgetSelectionRange range() const
  12.     {
  13.         return selectionRange;
  14.     }
  15.     QStringList formats() const
  16.     {
  17.         return dataFormats;
  18.     }
  19. protected:
  20.     QVariant retrieveData(const QString &format,
  21.                           QVariant::Type preferredType) const;
  22. private:
  23.     static QString toHtml(const QString &plainText);
  24.     static QString toCsv(const QString &plainText);
  25.     QString text(int row, int column) const;
  26.     QString selectionText() const;

  27.     const QTableWidget *dataTableWidget;
  28.     QTableWidgetSelectionRange selectionRange;
  29.     QStringList dataFormats;
  30. };
复制代码

为了避免存储具体的数据,我们存储表格的指针和选择区域的坐标的指针;dataFormats 指明这个数据对象所支持的数据格式。这个格式列表由 formats() 函数返回,意味着所有被 MIME 数据对象支持的数据类型。这个列表是没有先后顺序的,但是最佳实践是将“最适合”的类型放在第一位。对于支持多种类型的应用程序而言,有时候会直接选用第一个符合的类型存储。

  1. TableMimeData::TableMimeData(const QTableWidget *tableWidget,
  2.                              const QTableWidgetSelectionRange &range)
  3. {
  4.     dataTableWidget = tableWidget;
  5.     selectionRange = range;
  6.     dataFormats << "text/csv" << "text/html";
  7. }
复制代码

函数 retrieveData() 将给定的 MIME 类型作为 QVariant 返回。参数 format 的值通常是 formats() 函数返回值之一,但是我们并不能假定一定是这个值之一,因为并不是所有的应用程序都会通过 formats() 函数检查 MIME 类型。一些返回函数,比如 text(),html(),urls(),imageData(),colorData() 和 data() 实际上都是在 QMimeData 的 retrieveData() 函数中实现的。第二个参数 preferredType 给出我们应该在 QVariant 中存储哪种类型的数据。在这里,我们简单的将其忽略了,并且在 else 语句中,我们假定 QMimeData 会自动将其转换成所需要的类型:

  1. QVariant TableMimeData::retrieveData(const QString &format,
  2.                                      QVariant::Type preferredType) const
  3. {
  4.     if (format == "text/csv") {
  5.         return toCsv(selectionText());
  6.     } else if (format == "text/html") {
  7.         return toHtml(selectionText());
  8.     } else {
  9.         return QMimeData::retrieveData(format, preferredType);
  10.     }
  11. }
复制代码

在组件的 dragEvent() 函数中,需要按照自己定义的数据类型进行选择。我们使用 qobject_cast 宏进行类型转换。如果成功,说明数据来自同一应用程序,因此我们直接设置 QTableWidget 相关数据,如果转换失败,我们则使用一般的处理方式。这也是这类程序通常的处理方式:

  1. void DataTableWidget::dropEvent(QDropEvent *event)
  2. {
  3.     const TableMimeData *tableData =
  4.             qobject_cast<const TableMimeData *>(event->mimeData());

  5.     if (tableData) {
  6.         const QTableWidget *otherTable = tableData->tableWidget();
  7.         QTableWidgetSelectionRange otherRange = tableData->range();
  8.         // ...
  9.         event->acceptProposedAction();
  10.     } else if (event->mimeData()->hasFormat("text/csv")) {
  11.         QByteArray csvData = event->mimeData()->data("text/csv");
  12.         QString csvText = QString::fromUtf8(csvData);
  13.         // ...
  14.         event->acceptProposedAction();
  15.     }
  16.     QTableWidget::mouseMoveEvent(event);
  17. }
复制代码

由于这部分代码与前面的相似,感兴趣的童鞋可以根据前面的代码补全这部分,所以这里不再给出完整代码。





回复

使用道具 举报

尚未签到

2017-11-28 14:55:23 显示全部楼层
在fromCsv  中,这样写更好。
for(int c=0;c<row.count();c++){
            setItem(r-1,c,new QTableWidgetItem(row.at(c).trimmed().replace('"',"")));
}
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了