从Qt的角度看MVC框架

从Qt的角度看MVC框架

最近开始看《设计模式之禅》,其中MVC框架是我最熟悉的,文中的MVC是以JAVA的代码来解释的,我对java只是略有接触也就不赘述书本里的描述和代码分析,这里就从Qt的角度来分析一下MVC框架和QTableView的代码实现。

MVC(Model-View-Controller)框架

其实从字面意思来说,只能了解各大概,view应该是视图的意思,那Model和Controller究竟是什么?
从Qt的角度看MVC框架_第1张图片
这个图大致解释了MVC框架的结构和功能(我们主要说Model2,也就是提倡视图和模型彻底分离的MVC框架):

MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input. Before MVC, user interface designs tended to lump these objects together. MVC decouples them to increase flexibility and reuse.
- Model: holds the application’s state and core functionality (Domain logic).
- View: visually renders the Model to the User (Presentation logic).
- Controller: mediates User actions on the GUI to drive modifications on the Model (Application logic).

  • Model 处理数据逻辑和程序运行状态
  • View 则只负责显示
  • Controller 通常负责处理用户交互的部分,从视图读取数据与用户输入,并向模型发送数据;这里顺便提一下,在Qt里面我们并没有Controller的概念,而是Delegate(委托),意义很明显:控制器委托模型来处理数据,模型委托控制器来做数据的交互。

这样的框架好处是很明显的:
- 高重用性:一个模型可以有多个视图,同样一个视图也可以对应多个模型
- 低耦合:因为模型与视图分离,所以可以独立的拓展和修改而不产生相互的影响
- 快速开发和便捷部署

当然,现在的MVC框架也有了很多发展,也需要提供更复杂的用户逻辑,此时一些简单的界面逻辑则无需controller和Model关注,直接由View自身来实现。

QTableView

下面是Qt 4.5.1 的帮助文档里面Model/View Achitechture的介绍,可以看到思路和MVC框架基本一致。
从Qt的角度看MVC框架_第2张图片

Qt主要支持三种模型:
- 列表模型(list)
- 表模型(table) —> 我们主要讨论这种模型,其余的模型类似
- 树模型(tree)

Model

接下来我们来看具体的代码分析QTableView是如何实现MVC(MVD)框架的,首先我们来看一下QAabstractItemModel的源代码:

class Q_CORE_EXPORT QAbstractItemModel : public QObject
{
    Q_OBJECT
public:
    explicit QAbstractItemModel(QObject *parent = 0);
    virtual ~QAbstractItemModel();

    virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
    virtual QModelIndex parent(const QModelIndex &child) const = 0;
    inline QModelIndex sibling(int row, int column, const QModelIndex &idx) const
        { return index(row, column, parent(idx)); }
    virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
    virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
    virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
    virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
    virtual bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole);
    ......
}

这是QAbstractItemModel的头文件源代码的一部分,省略号省略那些不需要我们来实现的部分,从上面的定义可以看到,有些函数是纯虚函数并没有实现,所以如果你要继承这个类是一定要自己去实现这些函数的:

virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
virtual QModelIndex parent(const QModelIndex &child) const = 0;
//< 下面两个函数的意义很明显,就是定义这个表格(或者是树、列表)的行数和列数
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;

相信大家也相信注释的内容,所以我们着重看一下其他三个函数的意义,该如何实现我们也可以让Qt源代码来指导我们,下面是QFileSystemModel中这些函数的实现

QVariant QFileSystemModel::data(const QModelIndex &index, int role) const
{
    Q_D(const QFileSystemModel);
    if (!index.isValid() || index.model() != this)
        return QVariant();

    switch (role) {
    case Qt::EditRole:
    case Qt::DisplayRole:
        switch (index.column()) {
        case 0: return d->displayName(index);
        case 1: return d->size(index);
        case 2: return d->type(index);
        case 3: return d->time(index);
        default:
            qWarning("data: invalid display value column %d", index.column());
            break;
        }
        break;
    case ...
    default ...
    }

    return QVariant();
}

先说这个返回类型QVariant,简单来说的话我们可以把它看做是任意一个已注册的数据类型,已注册就是非自定义的类和结构体,如这个函数就可以直接返回QString或者int都没有问题,但是如果返回我们自定义的一个结构体的话就有可能会报错,当然如果我们先全局注册了这个自定义的结构体也没问题,不再深究。
再说传参,QModelIndex & index为View和Delegate提供了model中的数据定位信息,与数据本身无关,QModelIndex index = model->index(row, column, …);而role字面意思就是(项)角色(如下表,要查看所有角色请查看Qt::ItemDataRole),最常用的是Qt::DisplayRole 和 Qt::EditRole.

Constant value Description
Qt::DisplayRole 0 The key data to be rendered in the form of text.
Qt::DecorationRole 1 The data to be rendered as a decoration in the form of an icon.
Qt::EditRole 2 The data in a form suitable for editing in an editor.

所以在上面的函数实现中,我们看到返回值是数据对应的不同列的值,如displayName,size,type,time等,这其实就是FileSystem(文件系统)里可以看到的信息(如下图),所有数据都可由data函数来访问。
从Qt的角度看MVC框架_第3张图片

QModelIndex QFileSystemModel::index(int row, int column, const QModelIndex &parent) const
{
    Q_D(const QFileSystemModel);
    //< 先判断输入的行列号是否合法或是否可以去到上层model的数据,如果不能则直接返回一个空的ModelIndex
    if (row < 0 || column < 0 || row >= rowCount(parent) || column >= columnCount(parent))
        return QModelIndex();

    // get the parent node
    QFileSystemModelPrivate::QFileSystemNode *parentNode = (d->indexValid(parent) ? d->node(parent) : const_cast(&d->root));
    Q_ASSERT(parentNode);

    // now get the internal pointer for the index
    QString childName = parentNode->visibleChildren[d->translateVisibleLocation(parentNode, row)];
    const QFileSystemModelPrivate::QFileSystemNode *indexNode = parentNode->children.value(childName);
    Q_ASSERT(indexNode);
    //< 返回相应的节点信息(行列号以及该数据节点的指针)
    return createIndex(row, column, const_cast(indexNode));
}

这个函数提供了通过行列号获取数据的方法,最后parent:

QModelIndex QFileSystemModel::parent(const QModelIndex &index) const
{
    Q_D(const QFileSystemModel);
    if (!d->indexValid(index))
        return QModelIndex();

    QFileSystemModelPrivate::QFileSystemNode *indexNode = d->node(index);
    Q_ASSERT(indexNode != 0);
    QFileSystemModelPrivate::QFileSystemNode *parentNode = (indexNode ? indexNode->parent : 0);
    //< 如果父节点是空或者是根节点(根节点一般没有数据),则返回空的index
    if (parentNode == 0 || parentNode == &d->root)
        return QModelIndex();

    //< 通过祖父节点获取父节点的行列号
    QFileSystemModelPrivate::QFileSystemNode *grandParentNode = parentNode->parent;
    Q_ASSERT(grandParentNode->children.contains(parentNode->fileName));
    int visualRow = d->translateVisibleLocation(grandParentNode, grandParentNode->visibleLocation(grandParentNode->children.value(parentNode->fileName)->fileName));
    if (visualRow == -1)
        return QModelIndex();
    return createIndex(visualRow, 0, parentNode);
}

不难看出这种结构是一种树形结构的找当前节点父节点的方式,更直观的可以看看Windows的文件系统,而相对的,表格的结构中其实并没有父节点相对应的概念,所以我们暂时不讨论!
从Qt的角度看MVC框架_第4张图片

所以到目前为止,我们了解了要实现一个model所必需实现的函数,当然还有一些跟表格实际用处相关的函数也是需要实现的,如headerData用来定义表头的信息,flags用来定义每个单元格的标记(可编辑属性、可选属性等)等,后面我会给出一整套简单的实现一个表格的代码,里面有这些函数的具体实现!

Delegate(Controller)

Delegate(委托)对应的就是MVC框架中Controller的角色,model将数据传给delegate,委托它进行一些处理后转发给View进行显示,这些处理当然包括一些数据类型的转换、可读写属性的过滤和判定、绘制样式的控制等等。我们首先来看一段Qt源代码里的Delegate的函数实现:

QString QItemDelegatePrivate::valueToText(const QVariant &value, const QStyleOptionViewItemV4 &option)
{
    QString text;
    switch (value.userType()) {
        case QMetaType::Float:
            text = option.locale.toString(value.toFloat(), 'g');
            break;
        case QVariant::Double:
            text = option.locale.toString(value.toDouble(), 'g', DBL_DIG);
            break;
        case QVariant::Int:
        case QVariant::LongLong:
            text = option.locale.toString(value.toLongLong());
            break;
        case QVariant::UInt:
        case QVariant::ULongLong:
            text = option.locale.toString(value.toULongLong());
            break;
        case QVariant::Date:
            text = option.locale.toString(value.toDate(), QLocale::ShortFormat);
            break;
        case QVariant::Time:
            text = option.locale.toString(value.toTime(), QLocale::ShortFormat);
            break;
        case QVariant::DateTime:
            text = option.locale.toString(value.toDateTime().date(), QLocale::ShortFormat);
            text += QLatin1Char(' ');
            text += option.locale.toString(value.toDateTime().time(), QLocale::ShortFormat);
            break;
        default:
            text = replaceNewLine(value.toString());
            break;
    }
    return text;
}

很明显,该函数是将上面Model中data函数返回的QVariant数据全部转换成QString,而转换的结果最终会提供给界面显示,毕竟界面显示只有字符型。第二个传参在函数里是并没有使用的,我们只用了解QStyleOptionViewItemV4是为所有可视Item提供样式的,而真正来绘制这些Item的是另外一个函数:

void DetQTableDelegate::paint(QPainter *painter, 
    const QStyleOptionViewItem &option, 
    const QModelIndex &index) const
{
    Q_ASSERT(index.isValid());
    QStyleOptionViewItemV4 opt = option;

    int row    = index.row();
    int column = index.column();
    painter->setPen(QColor(0, 0, 0));
    //< 这里为了方便大家看我改成了数字,也就是表格中的 0, 2, 4 三列输入框的样式由我控制,剩余的由默认的QItemDelegate控制,建议大家不要直接使用数字而是使用枚举或者宏来定义列号,否则代码可读性会很差
    //< 指定第0,2,4三列的颜色和别的列不同
    if( column == 0 || column == 2 || column == 4 )
        painter->fillRect(option.rect, QBrush(QColor(255,255,255)));
    else
        painter->fillRect(option.rect, QBrush(QColor(210,210,210)));

    if ((option.state & QStyle::State_Selected)
        && (option.state & QStyle::State_Active))
    {
        painter->setPen(QColor(255, 255, 255));
        painter->fillRect(option.rect, QBrush(QColor(102, 102, 153)));       
    }
    //< 最终将所有的字符都画在item里面
    //< 表格其中两列的字体,对齐方式等等属性与别的列不同,分开画
    if(column == 7 || column == 8 )
        opt.widget->style()->drawItemText(painter, opt.rect, Qt::AlignCenter, opt.palette, true, index.data().toString());
    else
        opt.widget->style()->drawItemText(painter, opt.rect, Qt::AlignRight | Qt::AlignVCenter, opt.palette, true, index.data().toString());
}

我们无需关注函数里面的判断逻辑,整个的操作流程就是对某个特定的index对应的界面上的Item的样式做了调整,然后画出来(draw),所以如果我们要控制表格的样式,paint这个函是一定要自己重载的,不重写就是默认的样式,QItemDelegate这个类是没有纯虚函数的,不重载则直接使用默认格式。
另外,我们有时会要求表格不仅用于显示,还要可以编辑数据,这就要求在我们编辑时,表格相应单元格创建出一个输入框来供我们输入,这个输入可以是LineEdit,也可以是别的像CheckBox或者是SpinBox:

QWidget *QItemDelegate::createEditor(QWidget *parent, 
    const QStyleOptionViewItem &, 
    const QModelIndex &index) const
{
    Q_D(const QItemDelegate);
    if (!index.isValid())
        return 0;
    QVariant::Type t = static_cast(index.data(Qt::EditRole).userType());
    const QItemEditorFactory *factory = d->f;
    if (factory == 0)
        factory = QItemEditorFactory::defaultFactory();
    return factory->createEditor(t, parent);
}

这里我们在自己实现时通常会有一些特定的需求,而不是QItemEditorFactor这样的笼统的没指定类型的输入方式。比如我们这里需要一个简单的输入框,但是我们要求在输入时有一些特殊的样式,我们可以这样实现:

QWidget *DetQTableDelegate::createEditor(QWidget *parent, 
    const QStyleOptionViewItem &option, 
    const QModelIndex &index) const
{
    if (!index.isValid())
        return 0;
    int column = index.column();
    if( column == 0 || column == 2 || column == 4 )
    {
        //< 这里我们要新建一个LineEdit来指定输入的控件是一个行输入框
        QLineEdit *editor = new QLineEdit(parent);
        editor->setAlignment(Qt::AlignCenter);
        editor->setValidator(new QRegExpValidator(QRegExp("^-?[0-9]{0,2}\\.[0-9]{0,4}$"), editor));
        //< 设置样式
        editor->setStyleSheet("QLineEdit{ \
                              font-size: 15px; \
                              font-family: SIMHEI; \
                              font-weight: bold; \
                              color:blue; \
                              border-width: 2px; \
                              border-color: #0000FF; \
                              border-style: solid; \
                              background-color: #00FF00; \
                              selection-background-color: Transparent; \
                              selection-color:#0000FF; \
                              }");
        return editor;
    }
    else
        return QItemDelegate::createEditor(parent, option, index);
}

这样我们终于可以在表格中创建我们自己可以样式的输入框了,但是怎样才能把表格中输入的数据真正的写到Model的数据中去呢?QItemDelegate的实现并没有给我们太多的信息,这里看看我自己的实现:

void DetQTableDelegate::setEditorData(QWidget *editor, 
    const QModelIndex &index) const
{
    //< 这个函数的目的其实是把当前这个新建出的LineEdit的数值真正返回给Delegate内部的编辑器
    if (!index.isValid())
        return 0;
    int column = index.column();
    if( column == 0 || column == 2 || column == 4 )
    {
        QString string(index.model()->data(index, Qt::EditRole).toString());

        QAbstractItemModel *model = const_cast(index.model());
        model->setData(index, "", Qt::EditRole);
        QLineEdit *lineEdit = static_cast(editor);
        lineEdit->selectAll();
        lineEdit->setText(string);
    }
    else
        QItemDelegate::setEditorData(editor, index);
}

void DetQTableDelegate::setModelData(QWidget *editor, 
    QAbstractItemModel *model, 
    const QModelIndex &index) const
{
    //< 这个函数的目的是将输入框的值设置到Model的数据相应index中去
    QLineEdit *lineEdit = static_cast(editor);
    QString text = lineEdit->text();
    if ( text.isNull() || text.isEmpty() )
        return;

    int row = index.row();
    int column = index.column();
    if( row < 0 || column < 0 )
        return;
    switch( column )
    {
    case 0:
        g_QTableExtraDataMap[row].QMicroAdjust.x = text.toDouble();
        break;
    case 2:
        g_QTableExtraDataMap[row].QMicroAdjust.y = text.toDouble();
        break;
    case 4:
        g_QTableExtraDataMap[row].QMicroAdjust.z = text.toDouble();
        break;
    default:
        QItemDelegate::setModelData(editor, model, index);
        break;
    }
}

至此,我们表格的delegate所需要实现的基本功能就结束了,基本上View和Model的代理工作在这几个函数( paint, createEditor, setEditorData, setModelData )里面都可以完成!

View

视图其实是这三者中最简单的,我们只需要实例化一个QTableView,然后用setModel / setItemDelegate 即可将我们刚刚实现的那些Model和Delegate的操作带入到表格中去,表格的显示可以调整的由表格的显示风格,表格的行高列宽,表头的显示与否,表格多选或单选等等,这些都可以通过QTableView的已有接口来控制,下面有一段简单的实例代码,具体可以查阅QtAssistant里的接口介绍,这里就不多做赘述了:

    //< 设置数据model
    m_sourceModel = new DetQTableModel(this);
    m_proxyModel  = new QSortFilterProxyModel(this);
    m_proxyModel->setSourceModel(m_sourceModel);
    this->setModel(m_proxyModel);
    //< 设置委托
    m_itemDelegate = new DetQTableDelegate(this);
    this->setItemDelegate( m_itemDelegate );
    //< 设置竖表头隐藏
    this->verticalHeader()->hide();
    //< 设置表格只能单选
    this->setSelectionMode( QAbstractItemView::SingleSelection );
    //< 表头最后一格自适应拉伸
    this->horizontalHeader()->setStretchLastSection(true);
    this->verticalHeader()->setStretchLastSection(true);
    //< 设置表格样式
    this->setStyleSheet("... \
                        ");
    ...

总结

之前我也翻了一些网上关于MVC框架的博文,大多数都是针对Web开发,所以可能MVC框架确实在Web开发中用的会相对多一些而且也在不断地发展出新的特性,我对此确实不太了解,只有在QT中接触到这么一小部分理念很相似的设计,想跟大家分享一下。
这是我的第一篇Blog,其实也相当于是学习笔记,有一些并没有研究透彻的问题也希望大家能够提出来,我们共同进步!

你可能感兴趣的:(Qt-C++)