基于实例快速理解Qt模型视图的应用


Qt的开发过程中,经常会应用到它的模型视图结构,对于初学者来说,马上理解其原理,并且进行基础的应用,还是比较难的。另外,通过网络搜索查看别人发表有关模型视图的介绍,基本上都不能解决个人的一些疑惑。基于此,本文将结合实例来说明模型设计的应用,以便能够更加深刻的理解Qt模型视图的设计原理。

任何知识,如果想要能够应用自如,个人觉得首先需要对该知识,需要有一个整体的认识。所以,首先将对模型视图结构进行总体的说明,然后再结合实例来说明其应用,并且重点介绍代理的两种应用方式,最后再进行总结。

一、模型视图简介

Qt的模型视图与MVC设计模式类似,但是又不相同,MVC设计模式包括模型、视图和控制。模型表示数据,视图表示用户界面,控制定义了用户的操作。它能够有效将数据和显示分离,提高了代码的灵活性。Qt的模型视图同样有这样的效果。但是Qt的模型视图将视图和控制放在一起,以便简化框架。另外,为了能够更好的处理用户输入,Qt的模型视图加入的代理。通过代理能够自定义item的显示和编辑。

Qt的模型视图结构如下图所示,主要包含三个部分,分别为模型、视图和代理。模式与数据源通信,给其他部件提供接口。视图从模型中通过ModelIndex获取信息,代理用来显示和编辑item。

二、模型

模型的抽象基类为QAbstractItemModel,  下图显示了其继承关系。

QAbstractTableModel是表格的抽象基类,QAbstractListMode的列表的抽象基类。QAbstractProxyModel是代理的抽象基类。QDirModel是文件和目录的模型。QSqlQueryModel是有关数据库的模型。

模型的数据可以直接存在模型中,也可以由外部输入,由独立的类管理,还可以存在文件中,甚至是数据库。

模型中角色的功能是,根据不同的角色提供不同的数据。支持以下几种角色。

1、Qt::DisplayRole,   显示文字

2、Qt::DecorationRole,绘制数据

3、Qt::EditRole,  编辑器中的数据

4、Qt::ToolTipRole, 工具提示

5、Qt::StatusTipRole, 状态栏提示

6、Qt::SizeHintRole, 尺寸提示

7、Qt::FontRole, 字体

8、Qt::TextAlignmentRole,  对齐方式

9、Qt::BackgroundRole, 背景画刷

10、Qt::ForegroundRole, 前景画刷

11、Qt::CheckStateRole, 检查框状态

12、Qt::UseRole, 用户自定义数据的起始位置

三、视图

视图的抽象基类为QAbstractItemView,  它由五个基本视图类组成,分别是QTreeeView、QHeaderView、QListView、QColumnView和QTableVie w。为了用户简单方便的使用,还提供了三个模式视图集成的类,分别是QTreeWidget、QListWidget、QTableWidget。对于变动不大,并且简单的需求可以采用这三个类来快速开发。但是对于变动比较大的需求,就不建议使用这三个类,因为它们缺乏灵活性。

四、代理

代理的抽象基类为QAbstractItemDelegate,  如果需要改变item的显示或者item的编辑行为,那么可以考虑自定义代理类。

一般自定义代理类是继承QItemDelegate可以满足大部分的需求,如果直接继承QAbstractItemDelegate,则需要更多的开发工作量。

如果想要改变item的显示,那么可以通过继承QItemDelegate,然后重载paint。

如果想要改变item的编辑行为,同样的可以继承QItemDelegate,然后重载createEditor、setEditorData、setModelData和updateEditorGeometry。

下面的实例将详细介绍代理的这两种应用方式。

五、实例

首先实现模型QAbstractTableModel和表格QtableView的结合显示数据信息的实例。为了代码的清晰度,这里model直接存储了数据。

JWeaponModel模型的定义如下所示,rowCount返回行数,columnCount返回列数,data实现返回item的数据,headerData则是实现返回标题信息。flags和setData函数是为了支持代理而添加的,后面会讲解其作用。这里可以暂时不需要太关注。

class JWeaponModel : public QAbstractTableModel
{
public:
    JWeaponModel(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 orentation, int role) const;
    Qt::ItemFlags flags(const QModelIndex &index) const;

    bool setData(const QModelIndex &index, const QVariant &value, int role);

private:
    void InitData();

private:
    QVector<short>          m_army;         // 军种索引
    QVector<short>          m_weapon;       // 武器索引
    QMap<short, QString>    m_MapArmy;      // 军种映射表
    QMap<short, QString>    m_MapWeapon;    // 武器映射表
    QStringList             m_header;
};

JWeaponModel模型的实现如下,columnCount默认写死显示三列。date根据角色Qt::DisplayRole来说显示不同数据条目的数据。headerData返回水平标题信息。

JWeaponModel::JWeaponModel(QObject *parent)
    : QAbstractTableModel(parent)
{
    m_MapArmy[1] = tr("海军");
    m_MapArmy[2] = tr("空军");

    m_MapWeapon[1] = tr("战斗机");
    m_MapWeapon[2] = tr("轰炸机");

    InitData();

}

int JWeaponModel::rowCount(const QModelIndex& parent) const
{
    return m_MapArmy.size();
}

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

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

    if (role == Qt::DisplayRole)
    {
        switch (index.column())
        {
            case 0:
            {
                return m_MapArmy[m_army[index.row()]];
            }
            case 1:
            {
                return m_MapWeapon[m_weapon[index.row()]];
            }
            default:
            {
                return QVariant();
            }
        }
    }
    return QVariant();
}

QVariant JWeaponModel::headerData(int section, Qt::Orientation orentation, int role) const
{
    if (role == Qt::DisplayRole && orentation == Qt::Horizontal)
    {
        return m_header[section];
    }

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

void JWeaponModel::InitData()
{
    m_header << tr("军种")  << tr("种类") << tr("部门") ;
    m_army << 1 << 2;
    m_weapon << 1 << 2;
}

// 需要添加ItemIsEditable属性,否则代理创建的部件显示不出来
Qt::ItemFlags JWeaponModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

bool JWeaponModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::EditRole)
    {
            int row = index.row();
            m_MapArmy[row+1] =  value.toString();
            //LOG(INFO) << "row :" << row;
            return true;
    }

    return false;
}

主程序添加如下代码,QTableView设置模式为JWeaponModel。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->resize(640, 480);
p_view->show();

编译运行之后的效果如下,显示的数据均来自模型JWeaponModel。

如果想要改变item的编辑行为,支持双击的时候,变成选择框QComboBox,那么考虑使用代码,其定义如下,createEditor创建控件,setEditorData设置控件初始数据,setModelData将编辑数据写入model,  则model需要实现setData(参加JWeaponModel类的实现),这样才能将数据显示到视图。updateEditorGeometry管理控件位置。

#include <QModelIndex>
#include <QVariant>
#include <QItemDelegate>


class JEditorDelegate : public QItemDelegate
{
    Q_OBJECT

public:
    JEditorDelegate(QObject *parent = 0);
    ~JEditorDelegate() override;

    QWidget *createEditor(QWidget *parent,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const override;

    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

    void updateEditorGeometry(QWidget *editor,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const override;
};

JEditorDelegate对应的实现如下,这里只针对表格第一列进行处理。

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

}

JEditorDelegate::~JEditorDelegate()
{

}

QWidget *JEditorDelegate::createEditor(QWidget *parent,
                      const QStyleOptionViewItem &option,
                      const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_editor = new QComboBox(parent);
        p_editor->addItem(tr("one"));
        p_editor->addItem(tr("two"));
        return p_editor;
    }
    else
    {
        return QItemDelegate::createEditor(parent, option, index);
    }
}

void JEditorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_combobox = qobject_cast<QComboBox*>(editor);
        if(p_combobox)
        {
            int i = p_combobox->findText("one");
            p_combobox->setCurrentIndex(i);
        }
    }
    else
    {
        return QItemDelegate::setEditorData(editor, index);
    }
}

void JEditorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_combobox = qobject_cast<QComboBox*>(editor);
        if(p_combobox)
        {
            model->setData(index, p_combobox->currentText());
        }
    }
    else
    {
        return QItemDelegate::setModelData(editor, model, index);
    }
}

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

主程序添加代理,进行如下所示的修改,调用QTableView的setItemDelegate来添加代理。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->setItemDelegate(new JEditorDelegate());
p_view->resize(640, 480);
p_view->show();

编译运行之后,双击第一列第一行的item, 则出现如下的效果

如果想要改变item的显示,那么也是通过代理的方式来支持,其代理类定义如下,继承QItemDelegate,并重载paint。

#include <QModelIndex>
#include <QItemDelegate>

class JArrowDelegate : public QItemDelegate
{
Q_OBJECT

public:
   JArrowDelegate(QObject* parent = 0);
   virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
};

对应的实现如下,paint实现再item中画一个黑色的竖线。

JArrowDelegate::JArrowDelegate(QObject *parent)
   : QItemDelegate(parent)
{
}

void JArrowDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    (void)index;
    //int i_row = index.row();
    int i_x = option.rect.x();
    int i_y = option.rect.y();
    int i_width = option.rect.width();
    int i_height = option.rect.height();
    QPen pen;
    pen.setWidth(2);
    pen.setColor(QColor(Qt::black));
    QStyleOptionViewItem my_option = option;

    QPainterPath path;
    QPoint point_one;
    QPoint point_two;
    point_one.setX(i_x + i_width / 2);
    point_one.setY(i_y);
    point_two.setX(i_x + i_width / 2);
    point_two.setY(i_y + i_height);

    path.moveTo(point_one);
    path.lineTo(point_two);

    painter->setPen(pen);
    painter->drawPath(path);
}

主程序添加代理JArrowDelegate,调用QTableView的接口setItemDelegateForColumn只对表格的第三列添加代理。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->setItemDelegate(new JEditorDelegate());
p_view->setItemDelegateForColumn(2, new JArrowDelegate());
p_view->resize(640, 480);
p_view->show();

编译运行效果如下,第三列显示了一条竖线。

六、总结

QT模型视图的原理以及使用说明的讲解就到这里。本文先说明了模型视图的结构,然后再依次说明模型、视图和代理的类层次结构。最后结合具体实例来说明其应用方式,从中可以看出模型提供数据给视图显示,而代理则可以改变数据条目的显示,并通知视图,代理还可以改变数据条目的编辑行为,并通知模型。

json其实不难,只需了解一点,就能轻松玩转它

工作过程中,经常需要使用json这种轻量级的数据交换格式,例如,通过它可以组织数据保存到配置文件,客户端和服务端通过json格式来进行通信等,但是,针对特定的需求场景,需要设计怎样的json格式比较合适呢,json到底可以支持多少种格式呢,有没有一种简单的概括,就能让使用者轻松使用它呢!

一般知识都有基本的理论结构,所以,本文首先将说明json的基本知识点,然后基于开源软件jsoncpp来说明如何构建/读写json,   再分享个人的使用心得,最后再总结json的关键要点,理解了这一点,玩转json不是问题。

一、Json简介

Json是轻量级的数据交换格式,它便于阅读和编写,它是完全独立于程序语言的文本格式。

二、Json结构

Json有两个结构, 分别是“名称/值”对的集合和值的有序列表。“名称/值”对的集合可以简单理解为对象集合,而值的有序列表可以理解为数组。

这里举一个“名称/值”对的集合的例子,它是以左大括号开始,以右大括号结束,中间是由多个“名称/值”对组成,各个“名称/值”对之间用逗号隔开。

{
    "cpu_name" : "special",
    "cpu_temp" : 40
}

举一个“值的有序列表”的例子,它是以左中括号开始,以右中括号结束,中间是由多个值组成,各个值之间用逗号隔开。

["apple", "pear", "banana "]

三、Json形式

Json主要由三种形式,分别为对象(object),  数组(array),  值(value)。

对象(object)是“名称/值”对集合,名称于值之间通过冒号隔开,另外对象是以左大括号开始,以右大括号结束。

数组(array)是值的有序集合,它是以左中括号开始,以右中括号结束。

值(value)可以是字符串(string)、数值(number)、对象(object)、数组(array)、true、false、null。这里我们会发现对象(object)里面有值(value),  数组(array)里面也有值(value),  而值(value)又包含有对象和数组,所以它们是可以嵌套的。

Json就是由上面简单的元素来组建复杂的信息元素。

四、Json例子

jsoncpp是C++语言编写的开源json库,通过该库,我们可以很容易的构建、读写json。接下来就基于jsoncpp来解释几个构建、读取json的例子。通过例子可以对json有更深的理解。jsoncpp最基本的对象就是Json::Value。

构建一个最简单的对象,然后输出整个json信息,最后读取json值,先调用isMember判断名称是否为root成员,如果是的话,那么就读取输出。

Json::Value root;
root["result"] = "true";

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root.isMember("result"))
{
    LOG(INFO) << "root[\"result\"] = " << root["result"];
}

输出的日志信息如下所示,大括号包含了一个“名称/值”对。

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "result" : "true"
}

2020-05-02 17:59:32,670 INFO  [default] root["result"] = "true"

构建嵌套对象,第一个根“名称/值”对中的“值”又是一个对象。

Json::Value root;
Json::Value value;
value["cpu_name"] = "arm";
root["cpu_info"] = value;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["cpu_info"].isMember("cpu_name"))
{
    LOG(INFO) << "root[\"cpu_info\"][\"cpu_name\"] = " << root["cpu_info"]["cpu_name"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "cpu_info" : {
      "cpu_name" : "arm"
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["cpu_info"]["cpu_name"] = "arm"

构建三层嵌套对象,第一个根“名称/值”对中的“值”是一个对象,而该对象的“值”又是一个对象。依次类推,可以构建更多层的嵌套对象。

Json::Value root;
Json::Value value_01;
Json::Value value_02;

value_02["cell_number"]  = 255;
value_01["eye"] = value_02;
root["body"] = value_01;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["body"]["eye"].isMember("cell_number"))
{
    LOG(INFO) << "root[\"body\"][\"eye\"][\"cell_number\"]  = " << root["body"]["eye"]["cell_number"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "body" : {
      "eye" : {
         "cell_number" : 255
      }
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["body"]["eye"]["cell_number"]  = 255

构建简单的数组,jsoncpp中构建数组是通过append的接口来创建的。读取数组之前,先调用isArray来判断对象是否为数组,如果是的话,再读取输出。这里需要注意数组的个数。从防御式编程的角度看,读取数组值之前,需要判断数组索引是否在有效范围内。

Json::Value array;
array.append("one");
array.append("two");

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(array);
LOG(INFO) << "str_json: " << str_json.c_str();

if (array.isArray())
{
    LOG(INFO) << "array.size(): " << array.size();
    LOG(INFO) << "array[0]: " << array[0];
    LOG(INFO) << "array[1]: " << array[1];
}

输出的日志信息如下所示,从这里我们也可以确定数组是可以单独作为独立json串出现的。之前一直都有一个误区,就是认为json一定要用大括号包括起来。

2020-05-02 17:59:32,670 INFO  [default] str_json: [ "one", "two" ]

2020-05-02 17:59:32,671 INFO  [default] array.size(): 2
2020-05-02 17:59:32,671 INFO  [default] array[0]: "one"
2020-05-02 17:59:32,671 INFO  [default] array[1]: "two"

构建对象和数组组成的json。首先创建一个数组,然后将其作为对象的值。

Json::Value array;
array.append("one");
array.append("two");
array.append("three");

Json::Value root;
root["number"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["number"].isArray())
{
    LOG(INFO) << "root[\"number\"].size(): " << root["number"].size();
    LOG(INFO) << "root[\"number\"][0]: " << root["number"][0];
    LOG(INFO) << "root[\"number\"][1]: " << root["number"][1];
    LOG(INFO) << "root[\"number\"][2]: " << root["number"][2];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "number" : [ "one", "two", "three" ]
}

2020-05-02 17:59:32,671 INFO  [default] root["number"].size(): 3
2020-05-02 17:59:32,671 INFO  [default] root["number"][0]: "one"
2020-05-02 17:59:32,671 INFO  [default] root["number"][1]: "two"
2020-05-02 17:59:32,671 INFO  [default] root["number"][2]: "three"

最后再构建稍微复杂一点的json串,它是由对象、数组、对象来组成的,即对象的值是一个数组,而数组内部的值是由对象组成。

Json::Value root;
Json::Value array;
Json::Value value_01;
Json::Value value_02;

value_01["peripheral"] = 1;
value_01["patient"] = 2;

value_02["image"] = 3;
value_02["auto"] = 4;

array.append(value_01);
array.append(value_02);

root["department"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["department"].isArray())
{
    LOG(INFO) << "root[\"department\"].size(): " << root["department"].size();
    LOG(INFO) << "root[\"department\"][0][\"patient\"]: " << root["department"][0]["patient"];
    LOG(INFO) << "root[\"department\"][1][\"auto\"]: " << root["department"][1]["auto"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "department" : [
      {
         "patient" : 2,
         "peripheral" : 1
      },
      {
         "auto" : 4,
         "image" : 3
      }
   ]
}

2020-05-02 17:59:32,671 INFO  [default] root["department"].size(): 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][0]["patient"]: 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][1]["auto"]: 4

五、使用心得

  1. 读取json值之前,先判断其有效性,可以结合断言机制,调用isMember或者isArray来进行判断。
  2. 使用数组的时候,需要特别注意数组下标。

六、总结

json主要是由对象或数组创建而成,而它们的嵌套使用就可以创建复杂的json串,根据特定场景的需求来创建适用的json格式。