#(53):自定義拖放數據
上一章中,我們的例子使用系統提供的拖放對象`QMimeData`進行拖放數據的存儲。比如使用`QMimeData::setText()`創建文本,使用`QMimeData::urls()`創建 URL 對象等。但是,如果你希望使用一些自定義的對象作為拖放數據,比如自定義類等等,單純使用`QMimeData`可能就沒有那么容易了。為了實現這種操作,我們可以從下面三種實現方式中選擇一個:
1. 將自定義數據作為`QByteArray`對象,使用`QMimeData::setData()`函數作為二進制數據存儲到`QMimeData`中,然后使用`QMimeData::data()`讀取
2. 繼承`QMimeData`,重寫其中的`formats()`和`retrieveData()`函數操作自定義數據
3. 如果拖放操作僅僅發生在同一個應用程序,可以直接繼承`QMimeData`,然后使用任意合適的數據結構進行存儲
這三種選擇各有千秋:第一種方法不需要繼承任何類,但是有一些局限:即是拖放不會發生,我們也必須將自定義的數據對象轉換成`QByteArray`對象,在一定程度上,這會降低程序性能;另外,如果你希望支持很多種拖放的數據,那么每種類型的數據都必須使用一個`QMimeData`類,這可能會導致類爆炸。后兩種實現方式則不會有這些問題,或者說是能夠減小這種問題,并且能夠讓我們有完全的控制權。
下面我們使用第一種方法來實現一個表格。這個表格允許我們選擇一部分數據,然后拖放到另外的一個空白表格中。在數據拖動過程中,我們使用 CSV 格式對數據進行存儲。
首先來看頭文件:
~~~
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()`函數指明,這里的拖動操作接受兩種類型:復制和移動。當執行的是移動時,我們將已選區域清除。
需要注意一點,`QMimeData`在創建時并沒有提供 parent 屬性,這意味著我們必須手動調用 delete 將其釋放。但是,`setMimeData()`函數會將其所有權轉移到`QDrag`名下,也就是會將其 parent 屬性設置為這個`QDrag`。這意味著,當`QDrag`被釋放時,其名下的所有`QMimeData`對象都會被釋放,所以結論是,我們實際是無需,也不能手動 delete 這個`QMimeData`對象。
~~~
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()`函數生成新的數據項。
幾個輔助函數的實現比較簡單:
~~~
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);
}
~~~
由于這部分代碼與前面的相似,感興趣的童鞋可以根據前面的代碼補全這部分,所以這里不再給出完整代碼。
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)