Model/View之子類化QAbstractItemModel實現QTreeView的複選框

引言

  • 先上效果圖:
  • 這裏寫圖片描述

  • 最近想要實現上圖所示的一個數據展示列表,最先使用的QTreeWidget組件進行展示,但是遇到了當數據量過大(10000以上),第一次點擊TabPage加載數據時,總是有很卡頓的感覺,得隔一段時間才能加載顯示出數據。汗!偷懶偷不成了,效果自己都不能忍,更何況別人。因此使用了Model/View框架,自己實現了數據項和數據模型,最後效果還算滿意。

  • 需求是這樣的,當點擊表頭時,可以全部選中或者全部不選中視圖中的數據,而點擊數據時,表頭能展示選中狀態的三態效果。配合鍵盤的Ctrl和Shift鍵,實現區域選中,多塊選中(效果圖見文章最後)

實現

一、 子類化QAbstractItemModel,自定義QTreeView的數據模型

QAbstractItemModel類是虛基類,子類化該類,得需要實現所有的純虛函數才能實例化自定義的數據模型類。

virtual int rowCount(const QModelIndex & parent = QModelIndex()) const;
virtual int columnCount(const QModelIndex & parent = QModelIndex()) const;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
virtual bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole);//設置數據
virtual Qt::ItemFlags flags(const QModelIndex & index) const;//返回Item項的可選,可用戶點擊等標識

virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;//返回每個數據項的index
virtual QModelIndex parent(const QModelIndex &index) const;//本需求樹只有一層,parent返回NULL

virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;//返回相應role的表頭數據

以上函數是爲了實現本需求,必須實現的方法。可以說是自定義QTreeView模型的核心。

  • 自定義Model頭文件
#ifndef TREEVIEWMODEL_H
#define TREEVIEWMODEL_H

#include <QAbstractItemModel>

struct FlashIndexData{
    FlashIndexData():is_be_checked(false){
    }
    bool is_be_checked;
    quint32 unix_time;
    quint16 addr;
};

class TreeViewModel:public QAbstractItemModel
{
    Q_OBJECT
public:
    explicit TreeViewModel(QObject *parent=NULL);
    void setFlashData(QList<FlashIndexData> &flash_data);
    void clear();
    void getSelectedFlashData(QMap<quint32,quint16> &selected_list);

    virtual int rowCount(const QModelIndex & parent = QModelIndex()) const;
    virtual int columnCount(const QModelIndex & parent = QModelIndex()) const;
    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
    virtual bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole);
    virtual Qt::ItemFlags flags(const QModelIndex & index) const;

    virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
    virtual QModelIndex parent(const QModelIndex &index) const;

    virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
signals:
    void stateChanged(Qt::CheckState state);
private slots:
    void slot_stateChanged(Qt::CheckState state);
private:
    QList<FlashIndexData> m_flash_index;    //flash 索引

    void onStateChanged();

    enum{
        CHECK_BOX_COLUMN = 0,
        UNIX_TIME_COLUMN,
        FLASH_ADDR_COLUMN
    };
};

#endif // TREEVIEWMODEL_H
  • 首先需要定義自己的數據容器,本需求的容器如下所示:
    struct FlashIndexData{
        FlashIndexData():is_be_checked(false){
        }
        bool is_be_checked;
        quint32 unix_time;
        quint16 addr;
    };
  • 返回數據源的個數,即 即將展示的數據的行數.列數固定,直接返回想要顯示的列數即可。
int TreeViewModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);

    return m_flash_index.count();
}
int TreeViewModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);

    return 3;
}
  • 獲取role的數據,設置role的數據
QVariant TreeViewModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())
        return QVariant();
    int row = index.row();
    int column = index.column();
    FlashIndexData index_data = m_flash_index.at(row);

    switch (role) {
    case Qt::DisplayRole:
        if(column == UNIX_TIME_COLUMN)
            return QDateTime::fromTime_t(index_data.unix_time).toString("yyyy-MM-dd hh:mm:ss");
        else if(column == FLASH_ADDR_COLUMN)
            return index_data.addr;
        return "";
        break;
    case Qt::CheckStateRole:
        if(column == CHECK_BOX_COLUMN)
            return index_data.is_be_checked?Qt::Checked:Qt::Unchecked;
        break;
    case Qt::TextAlignmentRole:
        if(column == CHECK_BOX_COLUMN)
            return QVariant(Qt::AlignLeft|Qt::AlignVCenter);
        else
            return Qt::AlignCenter;
        break;
    case Qt::TextColorRole:
        return QColor(Qt::black);
        break;
    case Qt::SizeHintRole:
        return QSize(100,30);
        break;
    case Qt::FontRole:
        return QFont("SimSun", 11);
        break;
    default:
        break;
    }
    return QVariant();
}

//可編輯,只提供可選不可選的編輯,不提供對數據源的編輯
bool TreeViewModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if(!index.isValid())
        return false;
    int column = index.column();
    FlashIndexData index_data = m_flash_index.at(index.row());
    switch (role) {
    case Qt::UserRole:  //根據表頭的複選框選擇
    case Qt::UserRole+1:    //根據鼠標點擊
        if(column == CHECK_BOX_COLUMN)
        {
            index_data.is_be_checked = (((Qt::CheckState)value.toInt()) == Qt::Checked);
            m_flash_index.replace(index.row(),index_data);

            emit dataChanged(index,index);

            if(role == Qt::UserRole+1)  //點擊鼠標,更新表頭複選框狀態
                onStateChanged();

            return true;
        }
        break;
    default:
        return false;
        break;
    }
    return false;
}
  • 使Item顯示覆選框
//可選
Qt::ItemFlags TreeViewModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return QAbstractItemModel::flags(index);

    Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren;
    if (index.column() == CHECK_BOX_COLUMN)
        flags |= Qt::ItemIsUserCheckable;
    return flags;
}
  • 設置(row,column)的index
QModelIndex TreeViewModel::index(int row, int column, const QModelIndex &parent) const
{
    if (row < 0 || column < 0 || column >= columnCount(parent))
        return QModelIndex();

    return createIndex(row,column);
}
  • 只有一層樹形結構,parent爲空
QModelIndex TreeViewModel::parent(const QModelIndex &index) const
{
    Q_UNUSED(index);
    return QModelIndex();
}
  • 返回表頭的一些數據
QVariant TreeViewModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    switch (role) {
    case Qt::DisplayRole:
        if(section == CHECK_BOX_COLUMN)
        {
            return QString("全選");
        }else if(section == UNIX_TIME_COLUMN)
        {
            return QString("時間");
        }else if(section == FLASH_ADDR_COLUMN)
        {
            return QString("地址");
        }
        return "";
        break;
    case Qt::FontRole:
        return QFont("SimSun", 12);
        break;
    case Qt::TextAlignmentRole:
        return Qt::AlignCenter;
        break;
    case Qt::TextColorRole:
        return QColor(Qt::black);
        break;
    case Qt::SizeHintRole:
        return QSize(100,40);
        break;
    case Qt::BackgroundRole:
        return QBrush(Qt::black);
        break;
    default:
        break;
    }
    return QVariant();
}
  • 用於與表頭HeaderView交互的信號和槽,可以協調錶頭和Item的複選框狀態
void TreeViewModel::slot_stateChanged(Qt::CheckState state)
{
   for(int i = 0;i < rowCount();++i)
   {
       setData(index(i,CHECK_BOX_COLUMN),state,Qt::UserRole);
   }
}

void TreeViewModel::onStateChanged()
{
    int select_total = 0;
    for(int i = 0;i < rowCount();++i)
    {
        if(m_flash_index.at(i).is_be_checked)
            ++select_total;
    }

    if(select_total == 0)
    {
        emit stateChanged(Qt::Unchecked);
    }else if(select_total < rowCount())
    {
        emit stateChanged(Qt::PartiallyChecked);
    }else
    {
        emit stateChanged(Qt::Checked);
    }
}
  • 用於更新Model和清空Model
TreeViewModel::TreeViewModel(QObject *parent):
    QAbstractItemModel(parent)
{
    m_flash_index.clear();
}

void TreeViewModel::setFlashData(QList<FlashIndexData> &flash_data)
{
    m_flash_index = flash_data;

    beginResetModel();
    endResetModel();

    emit stateChanged(Qt::Unchecked);
}

void TreeViewModel::clear()
{
    m_flash_index.clear();
    beginResetModel();
    endResetModel();

    emit stateChanged(Qt::Unchecked);
}

void TreeViewModel::getSelectedFlashData(QMap<quint32, quint16> &selected_list)
{
    selected_list.clear();
    for(int i = 0;i < rowCount();++i)
    {
        if(m_flash_index.at(i).is_be_checked)
        {
            selected_list.insert(m_flash_index.at(i).unix_time,m_flash_index.at(i).addr);
        }
    }
}

—————————————————————————–

二、 自定義QTreeView的HeaderView,實現表頭複選框

  • 頭文件
#ifndef ATHEADERVIEW_H
#define ATHEADERVIEW_H

#include <QHeaderView>

class ATHeaderView : public QHeaderView
{
    Q_OBJECT
public:
    explicit ATHeaderView(QWidget *parent = 0);
    ~ATHeaderView(){}
protected:
    virtual void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const;
    virtual void mousePressEvent(QMouseEvent *e);
    virtual void mouseReleaseEvent(QMouseEvent *e);
signals:
    void stateChanged(Qt::CheckState state);
private slots:
    void slot_stateChanged(Qt::CheckState state);
private:
    Qt::CheckState m_state;
    bool m_is_pressed;
};

#endif // ATHEADERVIEW_H
  • 實現表頭背景的繪製,Section文本的繪製和複選框的繪製
//默認水平表頭
ATHeaderView::ATHeaderView(QWidget *parent):
    QHeaderView(Qt::Horizontal,parent)
{
    m_state = Qt::Unchecked;
    m_is_pressed = false;

    setSectionsClickable(true);
}

void ATHeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
{
    painter->save();
    QHeaderView::paintSection(painter,rect,logicalIndex);//繪製其他Section
    painter->restore();

//繪製背景色
    painter->save();
    painter->setBrush(QBrush(Qt::gray));
    painter->setPen(Qt::NoPen);
    painter->drawRect(rect);
//繪製Section的Text
    painter->setFont(QFont("SimSun", 12));
    painter->setPen(QColor("#000000"));
    painter->drawText(rect, Qt::AlignCenter, model()->headerData(logicalIndex,Qt::Horizontal).toString());
    painter->restore();
//爲第一列繪製Checkbox
    if(logicalIndex == 0)
    {
        QStyleOptionButton option;
        option.initFrom(this);
        if(m_state == Qt::Unchecked)
        {
            option.state |= QStyle::State_Off;
        }
        else if(m_state == Qt::PartiallyChecked)
        {
            option.state |= QStyle::State_NoChange;
        }
        else if(m_state == Qt::Checked)
        {
            option.state |= QStyle::State_On;
        }
        option.iconSize = QSize(20, 20);
        option.rect = QRect(QPoint(rect.left()+5,rect.top()+(rect.height()-20)/2),QPoint(rect.left()+25,rect.bottom()-(rect.height()-20)/2));
        style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter);
    }
}

void ATHeaderView::mousePressEvent(QMouseEvent *e)
{
    int nColumn = logicalIndexAt(e->pos());
    if ((e->buttons() & Qt::LeftButton) && (nColumn == 0))
    {
        m_is_pressed = true;

        e->accept();
    }
    e->ignore();
}

void ATHeaderView::mouseReleaseEvent(QMouseEvent *e)
{
    if(m_is_pressed)
    {
        if(m_state == Qt::Unchecked)
        {
            m_state = Qt::Checked;
        }else
        {
            m_state = Qt::Unchecked;
        }
        updateSection(0);
        emit stateChanged(m_state); //狀態改變
    }
    m_is_pressed = false;
    e->accept();
}
//根據Item的複選框狀態,表頭複選框狀態更新
void ATHeaderView::slot_stateChanged(Qt::CheckState state)
{
    m_state = state;

    updateSection(0);
}

—————————————————————————–

三、使用Model和HeaderView,實現複選框的協調,同時選擇Item,Model和HeaderView作出響應

  • 初始化
    m_headerView = new ATHeaderView(this);
    m_treeModel = new TreeViewModel(this);

    ui->treeView_flashindex->setModel(m_treeModel);
    ui->treeView_flashindex->setHeader(m_headerView);
    ui->treeView_flashindex->setSelectionMode(QAbstractItemView::ExtendedSelection);
    ui->treeView_flashindex->setExpandsOnDoubleClick(false);
    ui->treeView_flashindex->setIndentation(5);
    ui->treeView_flashindex->setColumnWidth(0,150);
    ui->treeView_flashindex->setColumnWidth(1,400);
    ui->treeView_flashindex->header()->setStretchLastSection(true);

    connect(m_headerView,SIGNAL(stateChanged(Qt::CheckState)),m_treeModel,SLOT(slot_stateChanged(Qt::CheckState)));
    connect(m_treeModel,SIGNAL(stateChanged(Qt::CheckState)),m_headerView,SLOT(slot_stateChanged(Qt::CheckState)));
    connect(ui->treeView_flashindex->selectionModel(),SIGNAL(selectionChanged(QItemSelection,QItemSelection)),this,SLOT(slot_selectionChanged(QItemSelection,QItemSelection)));
  • 根據選中狀態,多選或者單選,CheckBox作出響應
void NodeObjectFlashExport::slot_selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
    for(int i = 0;i < selected.indexes().count();++i)
    {
        m_treeModel->setData(selected.indexes().at(i),Qt::Checked,Qt::UserRole+1);
    }

    for(int i = 0;i < deselected.indexes().count();++i)
    {
        m_treeModel->setData(deselected.indexes().at(i),Qt::Unchecked,Qt::UserRole+1);
    }
}

效果圖如圖所示:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章