Skip to content

Qt中的MVC编程

官方文档 模型/视图编程 如有不解可参考奇牛 云对象浏览器

MVC(Model-View-Controller)包括了3个组件:模型(Model)是应用对象,用来表示数据;视图(View)是模型的用户界面,用来显示数据;控制(Controller)定义了用户界面对用户输入的反应方式。委托(Delegate)用于定制数据的渲染和编辑方式。

Qt中的模型/视图架构用来实现大量的数据存储、处理及显示。Qt 框架提供了 MVC 模式的实现方式:包括 QAbstractItemModel、QAbstractItemView 和 QAbstractItemDelegate 类。

模型Model

所有的模型都基于QAbstractItemModel类,该类提供了十分灵活的接口来处理各种视图,这些视图可以将数据的表现形式为表格(table)、列表(list)、树(tree)。

Qt提供了一些现成的模型来处理数据项:

  • QStringListModel 存储简单的QString项目列表;
  • QStandardItemModel管理复杂的属性结构数据项,每一个数据项可以包含任意的数据;
  • QFileSystemModel提供了本地文件系统中文件和目录信息;
  • QSqlQueryModel、QSqlTableModel和QSqlRelationTableModel用来访问数据库。

标准模型还无法满足需要时,可子类化QAbstractItemModel、QAbstractListModel或QAbstractTableModel来创建自定义的模型。 常见的3种模型为列表模型、表格模型、树模型,如下图所示:

为确保数据的表示与数据的获取相分离,Qt引入了模型索引的概念,输入和委托均可通过模型索引来请求数据并显示。只有模型需要知道怎样获取数据,被模型管理的数据类型可以被广泛的定义。模型索引包含一个指针,指向创建他们的模型,使用多个模型时可避免混淆。模型索引QModelIndex类提供对一块数据的临时引用,用来修改或检索模型中的数据,获取一个数据项的模型索引必须指定模型的3个属性:行号、列号和父项的模型索引。如:QModelIndex index = model->index(row,column,parent);也可通过模型指定的相关数据项对应的模型索引以及特定的角色来获取需要的类型数据,如:QVariant value = model->data(index,role);

常用的角色类型:

常量描述
Qt::DisplayRole数据被渲染为文本(数据为QString类型)
Qt::DecorationRole数据被渲染为图标等装饰 (数据为QColor,QIcon,或QPixmap类型)
Qt::EditRole数据可以在编辑器中进行编辑(数据为QString类型)
Qt::ToolTipRole数据显示在数据项的工具提示中(数据类型为QString)
Qt::StatusTipRole数据显示在状态栏中(数据为QString类型)
Qt::WhatsThisRole数据显示在数据项的“What’s This?”模式下(数据为QString类型)
Qt::SizeHintRole数据项的大小提示,将会应用到视图(数据为QSize类型)
Qt::FontRole默认代理的绘制使用的字体
Qt::TextAlignmentRole默认代理的对齐方式
Qt::BackgroundRole默认代理的背景画刷
Qt::ForegroundRole默认代理的前景画刷
Qt::CheckStateRole默认代理的检查框状态
Qt::UserRole用户自定义的数据的起始位置

视图View

Qt提供了QListView、QTableView视图、QTreeView视图分别实现列表、表格与树视图效果。QListView将数据项显示为一个列表;QTableView将模型中的数据显示在一个表格中;QTreeView将模型中的数据项显示在具有层次的列表中。QTableView和QTreeView在显示项目的时候同时还可以显示标头,通过QHeaderView类实现。自定义视图类是基于QAbstractItemView抽象基类,如实现条形图,饼状图等特殊显示方式。

视图类的选择行为

常量描述
QAbstractView::SelectItems选择单个项目
QAbstractView::SelectRows只选择行
QAbstractView::SelectColumns只选择列

视图类的选择模式

常量描述
QAbstractItemView::SigleSelection当用户选择一个项目时,所有已经选择的项目将成为未选择状态,而且用户无法在已经选择的项目上单击来取消选择
QAbstractView::ContiguousSelection用户单击一个项目的同时,按Shift键,则所有当前项目和单击项目之间的项目都将被选择或取消选择
QAbstractView::ExtendedSelection具有ContiguousSelection的特性,且可按Ctrl键进行不连续选择
QAbstractView::MultiSelection用户选择一个项目时不影响其他已经选择的项目
QAbstractView::NoSelection项目无法被选择

选择模型更新方式

QItemSelectionModel::SelectionFlags释义
QItemSelectionModel::NoUpdate不做选择
QItemSelectionModel::Clear选择被清除
QItemSelectionModel::Select选择指定索引
QItemSelectionModel::Deselect取消指定索引的选择
QItemSelectionModel::Toggle切换指定索引的选择
QItemSelectionModel::Current当前选择被更新
QItemSelectionModel::Rows索引扩展为跨行
QItemSelectionModel::Columns索引扩展为跨列
QItemSelectionModel::SelectCurrentSelect | Current组合
QItemSelectionModel::ToggleCurrentToggle | Current组合
QItemSelectionModel::ClearAndSelectClear | Select组合

委托Delegate

在模型/视图框架中,QAbstractItemDelegate是委托类的抽象基类,Qt默认的委托实现由QStyledItemDelegate类提供,这也被用作Qt标准视图的默认委托,选择 QStyledItemDelegate或QItemDelegate中其一来为视图中的项目绘制和提供编辑器。不同的是QStyledItemDelegate使用当前的样式来绘制项目,实现自定义委托建议使用QStyledItemDelegate作为基类。

Qt提供了项目试图的便捷类,这些类底层通过模型/视图框架实现。这些部件分别是QListWidget提供一个项目列表,QTreeWidget显示一个多层次的树结构,QTableWidget提供了一个以项目作为单元的表格。它们每一个类都继承了QAbstractItemView类的行为。之所以成为便捷因其用起来比较简单,使用于少量的数据的存储和显示。因没有将视图与模型分离,所以没有视图类灵活,不能和任意的模型一起使用。

模型视图使用示例

以查询本机文件系统为例。新建空项目testModelView,pro文件添加 QT += widgets,并添加main.cpp文件

main.cpp

cpp
#include <QApplication>
#include <QAbstractItemModel>
#include <QAbstractItemView>
// QT6已不可用,使用QFileSystemModel 创建model处须同步更改
#include <QItemSelectionModel>
// #include <QDirModel>
#include <QFileSystemModel>
#include <QTreeView>
#include <QListView>
#include <QTableView>
#include <QSplitter>
#include <QScreen>
#include <QDir>
#include <QDebug>

int main(int argc,char* argv[])
{
    QApplication app(argc,argv);

    //创建模型
    QFileSystemModel model;
    model.setRootPath(QDir::homePath());

    //创建树视图、列表视图、表格视图
    QTreeView tree;
    QListView list;
    QTableView table;

    //视图设置模型
    tree.setModel(&model);
    list.setModel(&model);
    table.setModel(&model);

    //设置视图对象的选择方式为多选,list/table选择方式同tree
    tree.setSelectionMode(QAbstractItemView::MultiSelection);
    list.setSelectionMode(tree.selectionMode());
    table.setSelectionMode(tree.selectionMode());

    //树视图双击信号发射后,列表及表格视图刷新内容
    QObject::connect(&tree,SIGNAL(doubleClicked(QModelIndex)),
                     &list,SLOT(setRootIndex(QModelIndex)));
    QObject::connect(&tree,SIGNAL(doubleClicked(QModelIndex)),
                     &table,SLOT(setRootIndex(QModelIndex)));

    QSplitter *splitter = new QSplitter;
    splitter->addWidget(&tree);
    splitter->addWidget(&list);
    splitter->addWidget(&table);
    splitter->setWindowTitle(QString("模型/视图"));
    QScreen *screen = app.primaryScreen();
    splitter->resize(screen->size());
    splitter->show();

    return app.exec();
}

运行效果

操作标准模型项

操作模型项需要使用 模型索引 通过模型获取的每条信息都由模型索引表示。视图和委托使用这些索引来请求显示数据项。

模型索引提供了对信息块的临时引用,可用于通过模型检索或修改数据。由于模型可能会不时重组其内部结构,模型索引可能会失效,因此不应存储。如果需要对某条信息进行长期引用,就必须创建一个持久的模型索引。它提供了对模型保持最新的信息的引用。临时模型索引由 QModelIndex 类提供,而持久模型索引则由 QPersistentModelIndex 类提供。

新建空项目testQModelIndex,pro文件添加 QT += widgets,并添加main.cpp文件

main.cpp

cpp
#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QDebug>

int main(int argc,char* argv[])
{
    QApplication a(argc,argv);

    //创建标准项模型
    QStandardItemModel model;

    //获取标准项模型的根项,根项是不可见的
    QStandardItem *parentItem = model.invisibleRootItem();

    //创建标准项item0,设置文本,设置图标,工具提示
    QStandardItem *item0 = new QStandardItem;
    item0->setText(QString("A"));

    QPixmap pixmap0(50,50);
    pixmap0.fill(Qt::red);

    item0->setIcon(QIcon(pixmap0));
    item0->setToolTip(QString("A的提示"));

    //将item0作为父项的子项
    parentItem->appendRow(item0);
    parentItem = item0;

    //创建item0的子项
    QStandardItem *item1 = new QStandardItem;
    item1->setText(QString("B"));

    QPixmap pixmap1(50,50);
    pixmap1.fill(Qt::blue);

    item1->setIcon(pixmap1);
    item1->setToolTip(QString("B项"));

    parentItem->appendRow(item1);

    QStandardItem *item2 = new QStandardItem;
    QPixmap pixmap2(50,50);
    pixmap2.fill(Qt::green);

    item2->setData("C",Qt::EditRole);
    item2->setData("indexC",Qt::ToolTipRole);
    item2->setData(QIcon(pixmap2),Qt::DecorationRole);

    parentItem->appendRow(item2);

    //在树视图中显示数据
    QTreeView view;
    view.setModel(&model);
    view.show();

    QModelIndex indexA = model.index(0,0,QModelIndex());
    qDebug() << "indexA row coutn:"
             << model.rowCount(indexA);

    QModelIndex indexB = model.index(0,0,indexA);
    qDebug() << "indexB text:"
             << model.data(indexB,Qt::EditRole).toString();
    qDebug() << "indexB toolTip:"
             << model.data(indexB,Qt::ToolTipRole).toString();

    return a.exec();
}

运行效果

自定义模型

以军事武器模型为例。新建空项目testCustomModel,pro文件添加QT+= widgets,并添加main.cpp文件

添加c++类WeaponModel继承自QAbstractTableModel

weaponmodel.h

cpp
#ifndef WEAPONMODEL_H
#define WEAPONMODEL_H
#include <QAbstractTableModel>

class WeaponModel : public QAbstractTableModel
{
public:
    WeaponModel(QObject* parent  = 0);

    virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
    virtual int columnCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index,int role) const;
    QVariant headerData(int section,Qt::Orientation orientation,int role) const;

private:
    QVector<short> army;                //军队
    QVector<short> weaponType;          //武器类型
    QMap<short,QString> armyMap;        //军队映射
    QMap<short,QString> weaponMap;      //武器映射

    QStringList weapon;     //武器
    QStringList header;     //表头
    void populateModel();   //表格数据的初始化
};

#endif // WEAPONMODEL_H

weaponmodel.cpp

cpp
#include "weaponmodel.h"

WeaponModel::WeaponModel(QObject* parent)
    :QAbstractTableModel(parent)
{
    armyMap[1] = QString("空军");
    armyMap[2] = QString("海军");
    armyMap[3] = QString("陆军");
    armyMap[4] = QString("海军陆战队");

    weaponMap[1] = QString("轰炸机");
    weaponMap[2] = QString("战斗机");
    weaponMap[3] = QString("航空母舰");
    weaponMap[4] = QString("驱逐舰");
    weaponMap[5] = QString("直升机");
    weaponMap[6] = QString("坦克");
    weaponMap[7] = QString("两栖攻击舰");
    weaponMap[8] = QString("两栖战车");

    populateModel();
}

int WeaponModel::rowCount(const QModelIndex &parent) const
{
    return army.size();
}

int WeaponModel::columnCount(const QModelIndex &parent) const
{
    return 3;
}

//放回指定索引的数据,将数值映射成文字
QVariant WeaponModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())
        return QVariant();

    if(role == Qt::DisplayRole)
    {
        switch(index.column()){
        case 0:
            return armyMap[army[index.row()]];
            break;
        case 1:
            return weaponMap[weaponType[index.row()]];
            break;
        case 2:
            return weapon[index.row()];
        default:
            return QVariant();
        }
    }

    return QVariant();
}

QVariant WeaponModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(role == Qt::DisplayRole && orientation == Qt::Horizontal)
        return header[section];

    return QAbstractTableModel::headerData(section,
                                           orientation,
                                           role);
}

void WeaponModel::populateModel()
{
    header << QString("军种")
           << QString("种类")
           << QString("武器");
    army << 1 << 2 << 3 << 4 << 2 << 4 << 3 << 1;
    weaponType << 1 << 3 << 5 << 7 << 4 << 8 << 6 << 2;
    weapon << QString("B-2") << QString("尼尔兹极")
           << QString("阿帕奇") << QString("黄蜂级")
           << QString("比例伯克级") << QString("AAAV")
           << QString("M1A1") << QString("F-22");
}

main.cpp

cpp
#include <QApplication>
#include "weaponmodel.h"
#include <QTableView>


int main(int argc,char* argv[])
{
    QApplication app(argc,argv);

    WeaponModel model;
    QTableView view;
    view.setModel(&model);
    view.setWindowTitle(QString("表格视图"));
    view.resize(600,400);
    view.show();

    return app.exec();
}

运行效果

只读模型操作

新建空项目testModel,pro文件添加QT += widgets.并添加main.cpp文件

添加自定义类StringListModel继承自QAbstractListModel。

stringlistmodel.h

cpp
#ifndef STRINGLISTMODEL_H
#define STRINGLISTMODEL_H

#include <QAbstractListModel>

class StringListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    StringListModel(const QStringList &strings,QObject *parent =0)
        :QAbstractListModel(parent),m_stringList(strings){}

    //模型行数
    int rowCount(const QModelIndex &parent = QModelIndex()) const;

    //指定模型索引的数据项
    QVariant data(const QModelIndex &index,int role) const;

    //表头内容(数或表格)
    QVariant headerData(int section,Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const;

    //项目属性
    Qt::ItemFlags flags(const QModelIndex &index) const;

    //编辑数据
    bool setData(const QModelIndex &index,const QVariant &value,int role = Qt::EditRole);

    //插入行 参数(插入的位置,插入的行数,父项的模型索引)
    bool insertRows(int position,int rows,const QModelIndex &index = QModelIndex());

    //删除行
    bool removeRows(int position,int rows,const QModelIndex &index = QModelIndex());

private:
    QStringList m_stringList;
};

#endif // STRINGLISTMODEL_H

stringlistmodel.cpp

cpp
#include "stringlistmodel.h"

int StringListModel::rowCount(const QModelIndex &parent) const
{
    return m_stringList.count();
}

QVariant StringListModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())    return QVariant();

    if(index.row() == m_stringList.size())  return QVariant();

    if(role == Qt::DisplayRole || role == Qt::EditRole)
        return m_stringList.at(index.row());
    else
        return QVariant();
}

QVariant StringListModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(role != Qt::DisplayRole)
        return QVariant();

    //水平表头
    if(orientation == Qt::Horizontal)
        return QString("Column %1").arg(section);
    else
        return QString("Row %1").arg(section);
}

Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const
{
    if(!index.isValid())
        return Qt::ItemIsEnabled;
    return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
}

bool StringListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    //检测索引有效且项目可编辑
    if(index.isValid() && role == Qt::EditRole)
    {
        m_stringList.replace(index.row(),value.toString());
        emit dataChanged(index,index);
        return true;
    }
    return false;
}

bool StringListModel::insertRows(int position, int rows, const QModelIndex &index)
{
    //告知其他组件指定的行开始插入操作
    beginInsertRows(QModelIndex(),position, position + rows -1);
    for(int row = 0; row < rows; ++row)
        m_stringList.insert(position,QString("来自星星的你"));
    //告知其他组件完成操作
    endInsertRows();
    return true;
}

bool StringListModel::removeRows(int position, int rows, const QModelIndex &index)
{
    //告知其他组件指定的行开始删除操作
    beginRemoveRows(QModelIndex(),position,position + rows - 1);
    for(int row = 0; row < rows; ++row){
        m_stringList.removeAt(position);
    }
    //告知其他组件完成操作
    endRemoveRows();
    return true;
}

main.cpp

cpp
#include <QApplication>
#include "stringlistmodel.h"
#include <QListView>
#include <QTableView>

int main(int argc,char* argv[])
{
    QApplication app(argc,argv);

    QStringList list;
    list << QString("太阳") << QString("地球")
         << QString("月亮") << QString("木星");

    StringListModel model(list);    //创建模型

    model.insertRows(3,2);
    model.removeRows(5,1);

    QListView listView;         //创建列表视图
    listView.setModel(&model);  //视图设置模型
    listView.show();            //视图显示

    QTableView tableView;       //创建表格视图
    tableView.setModel(&model); //视图设置模型
    tableView.show();           //视图显示

    return app.exec();
}

运行结果

操作选择模型

新建桌面应用程序testSelectionModel,基类QMainWindow,勾选产生界面文件。

mainwindow.h

cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class QTableView;
class QItemSelection;
class QModelIndex;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

public slots:
    void getCurrentItemData();      //当前选择
    void toggleSection();           //切换选择
    //更新选择,selected表新的选择,deselected表以前的选择
    void updateSelection(const QItemSelection &selected,
                         const QItemSelection &deselected);

    //改变当前模型索引
    void changeCurrent(const QModelIndex &current,
                       const QModelIndex &previous);
private:
    Ui::MainWindow *ui;
    QTableView *m_tableView;
    QTableView *m_tableView2;
};
#endif // MAINWINDOW_H

mainwindow.cpp

cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStandardItemModel>
#include <QTableView>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //创建标准项模型,7行4列
    QStandardItemModel *model = new QStandardItemModel(7,4,this);
    for(int row = 0; row < 7; ++row)
        for(int column = 0; column < 4; ++column)
        {
            QStandardItem *item = new QStandardItem(QString("%1")
                                                    .arg(row * 4 + column));
            //标准模型设置数据项
            model->setItem(row,column,item);
        }

    m_tableView = new QTableView;
    m_tableView->setModel(model);
    setCentralWidget(m_tableView);  //设置主窗口的中心部件为表格视图

    //获取视图的选择模式
    QItemSelectionModel *selectionModel = m_tableView->selectionModel();

    QModelIndex topLeft;        //左上角模型索引
    QModelIndex bottomRight;    //右下角模型索引
    topLeft = model->index(1,1);    //1行1列
    bottomRight = model->index(5,2);//5行2列

    //创建模型选择
    QItemSelection selection(topLeft,bottomRight);
    //以选择的方式来选择项目
    selectionModel->select(selection,QItemSelectionModel::Select);

    //添加动作addAction(动作文本,响应者,槽方法)
    ui->menubar->addAction(QString("当前项目"),this,
                           &MainWindow::getCurrentItemData);
    ui->menubar->addAction(QString("切换选择"),this,
                           &MainWindow::toggleSection);

    //关联选择模型的选择改变、当前项改变的信号
    connect(selectionModel,&QItemSelectionModel::selectionChanged,
            this,&MainWindow::updateSelection);
    connect(selectionModel,&QItemSelectionModel::currentChanged,
            this,&MainWindow::changeCurrent);

    m_tableView2 = new QTableView;
    m_tableView2->setWindowTitle("tableview2");
    m_tableView2->resize(400,300);
    m_tableView2->setModel(model);
    m_tableView2->setSelectionModel(selectionModel);
    m_tableView2->show();
}

MainWindow::~MainWindow()
{
    delete ui;
    delete m_tableView2;
}

void MainWindow::getCurrentItemData()
{
    qDebug() << QString("当前项数据:")
             << m_tableView->selectionModel()->currentIndex().data().toString();
}

void MainWindow::toggleSection()
{
    //左上角模型索引
    QModelIndex topLeft = m_tableView->model()->index(0,0,QModelIndex());

    //右下角模型索引
    QModelIndex bottomRight = m_tableView->model()->index(
                m_tableView->model()->rowCount(QModelIndex()) - 1,
                m_tableView->model()->columnCount(QModelIndex()) - 1,
                QModelIndex());

    //项选择
    QItemSelection curSelection(topLeft,bottomRight);
    m_tableView->selectionModel()->select(curSelection,
                                          QItemSelectionModel::Toggle);
}

void MainWindow::updateSelection(const QItemSelection &selected, const QItemSelection &deselected)
{
    QModelIndex index;

    //indexes()返回所有选择项的模型索引
    QModelIndexList list = selected.indexes();

    //给现在选择的项目填充数据
    foreach(index,list)
    {
        QString text = QString("%1,%2")
                        .arg(index.row())
                        .arg(index.column());
        m_tableView->model()->setData(index,text);
    }

    //清空上一次选择的内容
    list = deselected.indexes();
    foreach(index,list)
        m_tableView->model()->setData(index,"");
}

void MainWindow::changeCurrent(const QModelIndex &current, const QModelIndex &previous)
{
    qDebug() << QString("从(%1,%2)到(%3,%4)")
                .arg(previous.row()).arg(previous.column())
                .arg(current.row()).arg(current.column());
}

自定义委托

上面选择模型操作示例testSelectionModel项目中添加自定义委托类SpinBoxDelegate继承自QItemDelegate

spinboxdelegate.h

cpp
#ifndef SPINBOXDELEGATE_H
#define SPINBOXDELEGATE_H
#include <QItemDelegate>

class SpinBoxDelegate : public QItemDelegate
{
    Q_OBJECT
public:
    SpinBoxDelegate(QObject* parent = 0);

    //创建编辑器
    QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    //设置编辑器数据
    void setEditorData(QWidget *editor, const QModelIndex &index) const override;

    //更新编辑器几何属性
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

#endif // SPINBOXDELEGATE_H

spinboxdelegate.cpp

cpp
#include "spinboxdelegate.h"
#include <QSpinBox>

SpinBoxDelegate::SpinBoxDelegate(QObject* parent):QItemDelegate(parent)
{

}

QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QSpinBox *editor = new QSpinBox(parent);
    editor->setMinimum(0);
    editor->setMaximum(100);
    return editor;
}

void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    int value = index.model()->data(index,Qt::EditRole).toInt();

    //类型转换:QWidget*转QSpinBox*
    QSpinBox* spinBox = static_cast<QSpinBox*>(editor);

    //编辑器设置数据
    spinBox->setValue(value);
}

void SpinBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    editor->setGeometry(option.rect);
}

主窗口构造函数最后添加如下代码

cpp
SpinBoxDelegate *delegate = new SpinBoxDelegate(this);
// 视图设置自定义委托
m_tableView->setItemDelegate(delegate);