Qt Learning Road 53 Custom Drag and Drop Data
[Copy link]
In the previous chapter, our examples used the system-provided drag and drop object QMimeData to store drag and drop data. For example, use QMimeData::setText() to create text, use QMimeData::urls() to create URL objects, etc. However, if you want to use some custom objects as drag and drop data, such as custom classes, etc., it may not be so easy to use QMimeData alone. To achieve this operation, we can choose one of the following three implementation methods: - Store the custom data as a QByteArray object, use the QMimeData::setData() function as binary data in QMimeData, and then use QMimeData::Data() to read it
- Inherit QMimeData and rewrite the formats() and retrieveData() functions to operate custom data
- If the drag and drop operation only occurs in the same application, you can directly inherit QMimeData and then use any suitable data structure to store it
These three options have their own advantages: the first method does not require inheritance of any class, but there are some limitations: that is, drag and drop will not occur, and we must also convert custom data objects into QByteArray objects, which will reduce program performance to a certain extent; in addition, if you want to support many types of drag and drop data, each type of data must use a QMimeData class, which may cause class explosion. The latter two implementations will not have these problems, or can reduce this problem, and can give us full control. Below we use the first method to implement a table. This table allows us to select a part of the data and then drag and drop it into another blank table. During the data dragging process, we use the CSV format to store the data. First let’s look at the header file: - 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;
- };
[color=rgb(0, 204, 255) !important]Copy code Here, our table inherits from QTableWidget. Although this is a simplified QTableView, it is more than enough for our demonstration program.
- 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(event->source());
- if (source && source != this) {
- event->setDropAction(Qt::MoveAction);
- event->accept();
- }
- }
-
- void DataTableWidget::dragMoveEvent(QDragMoveEvent *event)
- {
- DataTableWidget *source =
- qobject_cast, Helvetica, sans-serif] The selection mode is set to continuous, which is to simplify our algorithm later. The four event response functions mousePressEvent(), mouseMoveEvent(), dragEnterEvent() and dragMoveEvent() are almost the same as before, so I will not repeat them here. Note that some of these functions do not call the parent class function of the same name. We have repeatedly emphasized this point in the previous chapters, but here we do not want the parent class implementation to be executed, so we completely block the parent class implementation. Let's take a look at the performDrag() function:
- 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();
- }
- }
[color=rgb(0, 204, 255) !important]Copy code Then we create a QMimeData object and set two data: HTML format and CSV format. Our CSV format is stored in the form of QByteArray. Then we create a QDrag object, use this QMimeData as the data required for dragging, and execute its exec() function. The exec() function indicates that the drag operation here accepts two types: copy and move. When moving is executed, we clear the selected area. It should be noted that QMimeData does not provide a parent property when it is created, which means that we must manually call delete to release it. However, the setMimeData() function transfers its ownership to the QDrag, that is, it sets its parent property to this QDrag. This means that when QDrag is released, all QMimeData objects under its name will be released, so the conclusion is that we actually do not need to and cannot manually delete the QMimeData object. - 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();
- }
- }
[color=rgb(0, 204, 255) !important]Copy code [p=22, null,left]The dropEvent() function is also very simple: if it is of CSV type, we take out the data, convert it into string form, and call the fromCsv() function to generate a new data item. ::color=rgb(102, 102, 102)] - 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", "");
- result.replace("\n", "\n");
- result.prepend("\nrightColumn(); 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", "");
- result.replace("\n", "\n");
- result.prepend("\nrightColumn(); 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", "");
- result.replace("\n", "\n");
- result.prepend("\n");
- 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('"', "")));
- }
- }
[color=rgb(0, 204, 255) !important]复制代码
虽然看起来很长,但是这几个函数都是纯粹算法,而且算法都比较简单。注意 toHtml() 中我们使用条件编译语句区分了一个 Qt4 与 Qt5 的不同函数。这也是让同一代码能够同时应用于 Qt4 和 Qt5 的技巧。fromCsv() 函数中,我们直接将下面表格的前面几列设置为拖动过来的数据,注意这里有一些格式上面的变化,主要用于更友好地显示。
最后是 MainWindow 的一个简单实现:
- 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");
- }
[color=rgb(0, 204, 255) !important]复制代码
这段代码没有什么新鲜内容,我们直接将其跳过。最后编译运行下程序,按下 shift 并点击表格两个单元格即可选中,然后拖放到另外的空白表格中来查看效果。
下面我们换用继承 QMimeData 的方法来尝试重新实现上面的功能。
- 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;
- };
[color=rgb(0, 204, 255) !important]复制代码
In order to avoid storing specific data, we store a pointer to the table and a pointer to the coordinates of the selected area; dataFormats indicates the data formats supported by this data object. This list of formats is returned by the formats() function, which means all data types supported by the MIME data object. This list is in no particular order, but the best practice is to put the "most suitable" type first. For applications that support multiple types, sometimes the first suitable type is directly selected for storage. - TableMimeData::TableMimeData(const QTableWidget *tableWidget,
- const QTableWidgetSelectionRange &range)
- {
- dataTableWidget = tableWidget;
- selectionRange = range;
- dataFormats << "text/csv" << "text/html";
- }
[color=rgb(0, 204, 255) !important]Copy code [p=22, null, The retrieveData() function returns the given MIME type as a QVariant. The value of the format parameter is usually one of the values returned by the formats() function, but we cannot assume that it must be one of these values, because not all applications check the MIME type through the formats() function. Some of the return functions, such as text(), html(), urls(), imageData(), colorData() and data() are actually implemented in the retrieveData() function of QMimeData. The second parameter preferredType gives which type of data we should store in the QVariant. Here, we simply ignore it, and in the else statement, we assume that QMimeData will automatically convert it to the required type: - 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);
- }
- }
[color=rgb(0, 204, 255) !important]Copy code In the dragEvent() function of the component, you need to choose according to the data type you define. We use the qobject_cast macro for type conversion. If successful, it means that the data comes from the same application, so we directly set the QTableWidget related data. If the conversion fails, we use the general processing method. This is also the usual way of handling this type of program: - void DataTableWidget::dropEvent(QDropEvent *event)
- {
- const TableMimeData *tableData =
- qobject_cast, Helvetica, SimSun, sans-serif] The value of the format parameter is usually one of the return values of the formats() function, but we cannot assume that it must be one of these values, because not all applications will check the MIME type through the formats() function. Some return functions, such as text(), html(), urls(), imageData(), colorData() and data() are actually implemented in the retrieveData() function of QMimeData. The second parameter preferredType gives what type of data we should store in the QVariant. Here, we simply ignore it, and in the else statement, we assume that QMimeData will automatically convert it to the required type:
- 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);
- }
- }
[color=rgb(0, 204, 255) !important]Copy code In the dragEvent() function of the component, you need to choose according to the data type you define. We use the qobject_cast macro for type conversion. If successful, it means that the data comes from the same application, so we directly set the QTableWidget related data. If the conversion fails, we use the general processing method. This is also the usual way of handling this type of program: - void DataTableWidget::dropEvent(QDropEvent *event)
- {
- const TableMimeData *tableData =
- qobject_cast, Helvetica, SimSun, sans-serif] The value of the format parameter is usually one of the return values of the formats() function, but we cannot assume that it must be one of these values, because not all applications will check the MIME type through the formats() function. Some return functions, such as text(), html(), urls(), imageData(), colorData() and data() are actually implemented in the retrieveData() function of QMimeData. The second parameter preferredType gives what type of data we should store in the QVariant. Here, we simply ignore it, and in the else statement, we assume that QMimeData will automatically convert it to the required type:
- 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);
- }
- }
[color=rgb(0, 204, 255) !important]Copy code In the dragEvent() function of the component, you need to choose according to the data type you define. We use the qobject_cast macro for type conversion. If successful, it means that the data comes from the same application, so we directly set the QTableWidget related data. If the conversion fails, we use the general processing method. This is also the usual way of handling this type of program: - void DataTableWidget::dropEvent(QDropEvent *event)
- {
- const TableMimeData *tableData =
- qobject_castsans-serif]The function retrieveData() returns the given MIME type as a QVariant. The value of the parameter format is usually one of the return values of the formats() function, but we cannot assume that it must be one of these values, because not all applications will check the MIME type through the formats() function. Some return functions, such as text(), html(), urls(), imageData(), colorData() and data() are actually implemented in the retrieveData() function of QMimeData. The second parameter preferredType gives what type of data we should store in the QVariant. Here, we simply ignore it, and in the else statement, we assume that QMimeData will automatically convert it to the required type:
- 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);
- }
- }
[color=rgb(0, 204, 255) !important]Copy code In the dragEvent() function of the component, you need to choose according to the data type you define. We use the qobject_cast macro for type conversion. If successful, it means that the data comes from the same application, so we directly set the QTableWidget related data. If the conversion fails, we use the general processing method. This is also the usual way of handling this type of program: - void DataTableWidget::dropEvent(QDropEvent *event)
- {
- const TableMimeData *tableData =
- qobject_castsans-serif]The function retrieveData() returns the given MIME type as a QVariant. The value of the parameter format is usually one of the return values of the formats() function, but we cannot assume that it must be one of these values, because not all applications will check the MIME type through the formats() function. Some return functions, such as text(), html(), urls(), imageData(), colorData() and data() are actually implemented in the retrieveData() function of QMimeData. The second parameter preferredType gives what type of data we should store in the QVariant. Here, we simply ignore it, and in the else statement, we assume that QMimeData will automatically convert it to the required type:
- 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);
- }
- }
[color=rgb(0, 204, 255) !important]Copy code In the dragEvent() function of the component, you need to choose according to the data type you define. We use the qobject_cast macro for type conversion. If successful, it means that the data comes from the same application, so we directly set the QTableWidget related data. If the conversion fails, we use the general processing method. This is also the usual way of handling this type of program: - void DataTableWidget::dropEvent(QDropEvent *event)
- {
- const TableMimeData *tableData =
- qobject_cast(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);
- }
[color=rgb(0, 204, 255) !important]Copy code Interested friends can complete this part based on the previous code, so the complete code is not given here.
|