上一章中,咱們的例子使用系統提供的拖放對象QMimeData
進行拖放數據的存儲。好比使用QMimeData::setText()
建立文本,使用QMimeData::urls()
建立 URL 對象等。可是,若是你但願使用一些自定義的對象做爲拖放數據,好比自定義類等等,單純使用QMimeData
可能就沒有那麼容易了。爲了實現這種操做,咱們能夠從下面三種實現方式中選擇一個:html
QByteArray
對象,使用QMimeData::setData()
函數做爲二進制數據存儲到QMimeData
中,而後使用QMimeData::data()
讀取QMimeData
,重寫其中的formats()
和retrieveData()
函數操做自定義數據QMimeData
,而後使用任意合適的數據結構進行存儲
這三種選擇各有千秋:第一種方法不須要繼承任何類,可是有一些侷限:便是拖放不會發生,咱們也必須將自定義的數據對象轉換成QByteArray
對象,在必定程度上,這會下降程序性能;另外,若是你但願支持不少種拖放的數據,那麼每種類型的數據都必須使用一個QMimeData
類,這可能會致使類爆炸。後兩種實現方式則不會有這些問題,或者說是可以減少這種問題,而且可以讓咱們有徹底的控制權。算法
下面咱們使用第一種方法來實現一個表格。這個表格容許咱們選擇一部分數據,而後拖放到另外的一個空白表格中。在數據拖動過程當中,咱們使用 CSV 格式對數據進行存儲。數據結構
首先來看頭文件:app
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
,但對於咱們的演示程序已經綽綽有餘。函數
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()
函數:性能
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()
函數指明,這裏的拖動操做接受兩種類型:複製和移動。當執行的是移動時,咱們將已選區域清除。this
須要注意一點,QMimeData
在建立時並無提供 parent 屬性,這意味着咱們必須手動調用 delete 將其釋放。可是,setMimeData()
函數會將其全部權轉移到QDrag
名下,也就是會將其 parent 屬性設置爲這個QDrag
。這意味着,當QDrag
被釋放時,其名下的全部QMimeData
對象都會被釋放,因此結論是,咱們實際是無需,也不能手動 delete 這個QMimeData
對象。url
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()
函數生成新的數據項。指針
幾個輔助函數的實現比較簡單:code
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
的一個簡單實現:
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
的方法來嘗試從新實現上面的功能。
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 數據對象支持的數據類型。這個列表是沒有前後順序的,可是最佳實踐是將「最適合」的類型放在第一位。對於支持多種類型的應用程序而言,有時候會直接選用第一個符合的類型存儲。
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
會自動將其轉換成所須要的類型:
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
相關數據,若是轉換失敗,咱們則使用通常的處理方式。這也是這類程序一般的處理方式:
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); }
因爲這部分代碼與前面的類似,感興趣的童鞋能夠根據前面的代碼補全這部分,因此這裏再也不給出完整代碼。