QTableView表格控制元件區域選擇-自繪選擇區域

朝十晚八發表於2019-08-01

原文連結:QTableView表格控制元件區域選擇-自繪選擇區域

一、開心一刻

陪完客戶回到家,朦朧之中,看到我媽正在拖地,我掏出200塊塞到我媽手裡,說道:媽,給你點零花錢,別讓我媳婦知道。

我媽接過錢,大吼:你是不是又喝酒了?

我:噓,你怎麼知道的?

老媽:你看清楚了,我是你媳婦,還有。這200塊錢是哪來的,說!我:啊……

二、概述

最近優化了一個小功能,主要是模仿excel相關的操作,覺得還挺不錯的,因此在這裡進行了整理,分享給有需要的朋友。今天主要是說一下區域選擇這項功能,Qt自帶的表格控制元件是具有區域選擇功能的,但是他並不美觀,不能支援我們自定義邊框色和一些細節上的調整。

今天博主就來講解下自己是怎麼自定義這個區域選擇功能的。

主要使用的方式還是自繪,下面先來看下效果,是不是你想要的。

三、效果展示

如下圖所示,是一個自繪選擇區域的效果展示,除此之外demo中還有一些其他的效果,但不是本篇文章所要講述的內容。

本篇文章的重點就是講述怎麼實現區域選擇框繪製

QTableView表格控制元件區域選擇-自繪選擇區域

四、實現思路

看過效果圖之後,接下來開始分析怎麼繪製矩形選擇框。下面以問題的形式來進行分析,這樣更有利於理解。

那麼先來思考如下幾個很問題

  1. 怎麼確定繪製區域
  2. 怎麼確定繪製的邊框
  3. 誰去繪製更好

以上三個問題搞懂了,那麼今天的主要內容也就差不多了。

1、繪製區域

學習Qt的第一步便是看幫助文件,不得不說Qt的幫助文件那是做的相當好,非常齊全。既然如此那還等什麼,直接開啟Qt 助手看看如下幾個類都有哪些訊號把。

QTableView

//QAbstractItemView
void activated(const QModelIndex &index)
void clicked(const QModelIndex &index)
void doubleClicked(const QModelIndex &index)
void entered(const QModelIndex &index)
void iconSizeChanged(const QSize &size)
void pressed(const QModelIndex &index)
void viewportEntered()

QTableView是表格控制元件基類,我們的表格也是基於這個控制元件進行開發。再看這個類的包含的訊號(其中都是他的父視窗訊號),對於本小結開始提出的3個問題好像沒有特別大的作用。那麼我們繼續往下看,看看他的資料儲存類。

QStandardItemModel

void itemChanged(QStandardItem *item)

//parent QAbstractItemModel

void columnsAboutToBeInserted(const QModelIndex &parent, int first, int last)
void columnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationColumn)
void columnsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void columnsInserted(const QModelIndex &parent, int first, int last)
void columnsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int column)
void columnsRemoved(const QModelIndex &parent, int first, int last)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int> ())
void headerDataChanged(Qt::Orientation orientation, int first, int last)
void layoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex> (), QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint)
void layoutChanged(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex> (), QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint)
void modelAboutToBeReset()
void modelReset()
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
void rowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row)
void rowsRemoved(const QModelIndex &parent, int first, int last)

QStandardItemModel便是QTableView的資料模型了,一眼掃過好像都是模型資料發生變化了的一些訊號。這個時候發現M和V好像沒有我們需要的東西,Qt不會真這麼挫吧。答案當然是“否”,仔細翻閱Qt的幫助文件就會發現QAbstractItemView類可以返回一個selectionModel,看其名字好像是我們需要的東西。

QItemSelectionModel * selectionModel() const

隨繼續翻閱幫助文件,我們得到以下資訊

void currentChanged(const QModelIndex &current, const QModelIndex &previous)
void currentColumnChanged(const QModelIndex &current, const QModelIndex &previous)
void currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
void modelChanged(QAbstractItemModel *model)
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)

哈哈哈,果然找到了我們需要的訊號,看訊號名稱就知道,當前項發生變化時觸發,然後我們就可以去統計哪些項被選中。

到這裡,我們的第一個問題就算回答了,我們可以通過selectionModel的selectionChanged訊號來統計可能需要繪製border的單元格。

//連線訊號
connect(m_pVew->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ExcTableWidget::SelectionChanged);

2、繪製邊框

訊號連線上後,開始處理訊號。

思路大致是這樣的:

  1. 使用gridCell記錄所有的單元格
  2. 迴圈遍歷選中的單元格
  3. 判斷當前單元格哪個邊是需要繪製的
  4. 結果儲存於gridPosints結構中

判斷邏輯也比較簡單,邏輯比較簡單,可以直接看程式碼。這裡我舉一個例子,比如說是否需要繪製左border,那麼就是需要看這個cell左邊是否有cell,或者自己已經是第一列。

gridPosints是QMap<QModelIndex, QVector>型別,鍵儲存單元格索引,值儲存4個邊的狀態(是否需要繪製)

void ExcTableWidget::SelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
    QModelIndexList indexs = m_pVew->selectionModel()->selectedIndexes();

    qDebug() << indexs;

    int row = GetModel()->rowCount();
    int column = GetModel()->columnCount();

    QVector<QVector<bool>> gridCell(row, QVector<bool>(column));

    for each (const QModelIndex & index in indexs)
    {
        gridCell[index.row()][index.column()] = true;
    }

    QMap<QModelIndex, DrawTypes> datas;
    QMap<QModelIndex, QVector<GridPoint>> gridPosints;
    for each (const QModelIndex & index in indexs)
    {
        DrawTypes types;
        bool topLine = true, rightLine = true, bottomLine = true, leftLine = true;
        if (index.row() == 0)
        {
            types |= TOP;
        }
        else
        {
            int aboveCell = index.row() - 1;
            if (gridCell[aboveCell][index.column()] == false)
            {
                types |= TOP;
            }
            else
            {
                topLine = false;
            }
        }

        if (index.column() == GetModel()->columnCount() - 1)
        {
            types |= RIGHT;
        }
        else
        {
            int rightCell = index.column() + 1;
            if (gridCell[index.row()][rightCell] == false)
            {
                types |= RIGHT;
            }
            else
            {
                rightLine = false;
            }
        }

        if (index.row() == GetModel()->rowCount() - 1)
        {
            types |= BOTTOM;
        }
        else
        {
            int beloveCell = index.row() + 1;
            if (gridCell[beloveCell][index.column()] == false)
            {
                types |= BOTTOM;
            }
            else
            {
                bottomLine = false;
            }
        }

        if (index.column() == 0)
        {
            types |= LEFT;
        }
        else
        {
            int leftCell = index.column() - 1;
            if (gridCell[index.row()][leftCell] == false)
            {
                types |= LEFT;
            }
            else
            {
                leftLine = false;
            }
        }

        datas[index] = types;

        gridPosints[index].push_back({ TOP, topLine });
        gridPosints[index].push_back({ RIGHT, rightLine });
        gridPosints[index].push_back({ BOTTOM, bottomLine });
        gridPosints[index].push_back({ LEFT, leftLine });
    }

    m_pVew->SetCellDatas(gridPosints);
    SelectStyle * style = m_pVew->GetDelegate();
    style->SetCellDatas(datas);

    m_pVew->update();
}

到這裡,我們的第二個問題就算回答了,我們需要繪製邊框的單元格總算是計算出來了。

3、繪製

資料都有了,繪製還會遠嗎?

接下來繼續往下看,Qt提供的繪製邏輯機制還是很強大滴,我們可以通過以下方式重繪

1、重寫QStyledItemDelegate

QStyledItemDelegate是繪圖代理,大多數的繪製操作最終都會在這裡被執行,看引數就知道每一個cell繪製時都會來這裡。

virtual void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override;

但是這裡有一個問題,那就是這個函式可繪製的區域問題,只能在這個cell裡邊繪製,如果繪製在border上將會被覆蓋,不信看如下堆疊。

QTableView表格控制元件區域選擇-自繪選擇區域

繪圖代理QStyledItemDelegate的paint函式是被QTableView的paintEvent函式進行回撥。

既然繪圖代理中繪製cell項時不能繪製到cell外邊去,那麼剛好,我們可以在這裡進行選擇區域的填充

void SelectStyle::DrawSelected(QPainter * painter, const QRect & rect, const QModelIndex & index) const
{
    if (m_indexs.contains(index) == false)
    {
        return;
    }

    painter->save();

    QPen pen = painter->pen();
    pen.setWidth(1);
    pen.setColor(m_color);
    painter->setPen(pen);

    painter->fillRect(rect, QColor(100, 0, 0, 100));

    painter->restore();
}

填充完選擇區域後,接下來便是繪製選擇區域的border。

2、重寫paintEvent
看了函式呼叫堆疊後,大家心裡應該也比較清楚QTableView是怎麼繪製的了吧。既然繪製代理不能完成需求,那麼我們就只能在paintEvent這座大山中進行繪製。

這裡需要注意一點就是,我們需要先試用QTableView本身的paintEvent把原有的繪製走一遍,保證介面上的資訊都是全的,然後在執行我們自己的定製程式碼。

如下圖所示,父類的paintEvent函式執行完畢後,我們繪製了border邊線



QTableView表格控制元件區域選擇-自繪選擇區域


之前在selectionModel的selectionChanged訊號中,我們已經獲取到了需要繪製border的cell資訊,下面繪製時只需要根據快取資料繪製即可,看這程式碼很長,但速度槓槓滴。

void FreezeTableView::paintEvent(QPaintEvent * event)
{
    QTableView::paintEvent(event);

    //繪製網格線
    QPainter painter(viewport());
    painter.save();
    QPen pen = painter.pen();
    pen.setWidth(1);
    pen.setColor(m_pSelectBorder->GetLineColor());
    painter.setPen(pen);

    for (auto iter = m_indexs.begin(); iter != m_indexs.end(); ++iter)
    {
        QModelIndex index = iter.key();
        QVector<GridPoint> cellTyeps = iter.value();
        QRect rect = visualRect(index);
        QRect tmpRect = rect;
        tmpRect.adjust(-1, -1, 1, 1);
        if (index.column() == 0)
        {
            tmpRect.adjust(1, 0, 0, 0);
        }
        if (index.row() == 0)
        { 
            tmpRect.adjust(0, 1, 0, 0);
        }

        for (int i = 0; i < cellTyeps.size(); ++i)
        {
            const GridPoint & point = cellTyeps.at(i);

            if (point.type == TOP && point.line)
            {
                painter.drawLine(tmpRect.topLeft(), tmpRect.topRight());
            }
            if (point.type == RIGHT && point.line)
            {
                painter.drawLine(tmpRect.topRight(), tmpRect.bottomRight());
            }
            if (point.type == BOTTOM && point.line)
            {
                painter.drawLine(tmpRect.bottomLeft(), tmpRect.bottomRight());
            }
            if (point.type == LEFT && point.line)
            {
                painter.drawLine(tmpRect.topLeft(), tmpRect.bottomLeft());
            }
        }
    }

    for (auto iter = m_indexsBorder.begin(); iter != m_indexsBorder.end(); ++iter)
    {
        QModelIndexList indexs = iter.key();
        for each (const QModelIndex & index in indexs)
        {
            QRect rect = visualRect(index);
            rect.adjust(-1, -1, 0, 0);
            if (index.column() == 0)
            {
                rect.adjust(1, 0, 0, 0);
            }
            if (index.row() == 0)
            {
                rect.adjust(0, 1, 0, 0);
            }
            painter.setPen(iter.value());
            painter.drawRect(rect);
        }
    }

    painter.restore();
}

有了以上核心程式碼,自繪選擇區域的功能基本上也就可以實現了。

五、相關文章

  1. Qt實現表格控制元件-支援多級列表頭、多級行表頭、單元格合併、字型設定等

  2. Qt高仿Excel表格元件-支援凍結列、凍結行、內容自適應和合並單元格

  3. 屬性瀏覽器控制元件QtTreePropertyBrowser編譯成動態庫(設計師外掛)

  4. 超級實用的屬性瀏覽器控制元件--QtTreePropertyBrowser

  5. Qt之表格控制元件螞蟻線

  6. QRowTable表格控制元件-支援hover整行、checked整行、指定列排序等

  7. QRowTable表格控制元件(二)-紅漲綠跌


值得一看的優秀文章:

  1. 財聯社-產品展示
  2. 廣聯達-產品展示
  3. Qt定製控制元件列表
  4. 牛逼哄哄的Qt庫





如果您覺得文章不錯,不妨給個打賞,寫作不易,感謝各位的支援。您的支援是我最大的動力,謝謝!!!








QTableView表格控制元件區域選擇-自繪選擇區域 QTableView表格控制元件區域選擇-自繪選擇區域






很重要--轉載宣告

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用連結的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。


相關文章