【Qt筆記】自定義拖放數據

上一章中,咱們的例子使用系統提供的拖放對象QMimeData進行拖放數據的存儲。好比使用QMimeData::setText()建立文本,使用QMimeData::urls()建立 URL 對象等。可是,若是你但願使用一些自定義的對象做爲拖放數據,好比自定義類等等,單純使用QMimeData可能就沒有那麼容易了。爲了實現這種操做,咱們能夠從下面三種實現方式中選擇一個:html

  1. 將自定義數據做爲QByteArray對象,使用QMimeData::setData()函數做爲二進制數據存儲到QMimeData中,而後使用QMimeData::data()讀取
  2. 繼承QMimeData,重寫其中的formats()retrieveData()函數操做自定義數據
  3. 若是拖放操做僅僅發生在同一個應用程序,能夠直接繼承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()實際上都是在QMimeDataretrieveData()函數中實現的。第二個參數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);
}

因爲這部分代碼與前面的類似,感興趣的童鞋能夠根據前面的代碼補全這部分,因此這裏再也不給出完整代碼。

相關文章
相關標籤/搜索