在上一章里演示了一个很基础的数据库显示程序,但这个程序存在这不少问题,比如点击一个表名,右侧就会显示对应的表的全部数据,那如果表的数据比较多的话,这个操作就会比较耗时,而在程序读取数据库内容的时候程序会处于卡死状态,无法响应用户的操作,在这个数据爆炸的时代,一张表有个几千万条数据是件很正常的事情,而程序要把这几千万条数据全部读出来就是一项非常浩大的工程了,很多时候读取这么大的数据是个非常不明智的选择甚至是无法完成的任务,同时显示这些数据也有些麻烦,以我的系统为例,QTableView显示的数据超过十万行时就会崩溃。。。问题的根源在于上一章中模型QSqlTableModel使用了select()函数来读取数据,而这个函数里会使用SELECT语句查询指定表的全部数据,解决这个问题的方法是继承QSqlTableModel类,提供一个额外的函数,可以查询指定列的数据(比如只查询1-10行的数据)。
这个问题要解决不是很困难,接下来先看一个程序,这个程序在功能上比上一章的“简单”。为了演示这个例子,需要读者在自己的电脑里创建一个sqlite3的文件(数据库),里面包含一张表
CREATE TABLE Patient (name TEXT , sex TEXT , age INTEGER , checkDate TEXT);
然后在这张表里插入诺干数据。
这章的程序很简单,显示表(只读模式),并且是的用户可以编辑表(读写模式);
表的结构也很简单,该表用于记录产科病人的基本信息,总共有四列,姓名,性别,年龄,上次检查日期。如果按照使用上一章的内容,会遇到一些问题。除了上面提到的,数据条目比较大(假设一个大型医院检查的人次大于十万),那全部显示就不太明智了,这里需要只显示十条数据,然后提供一个翻页的功能。然后由于这是产科病人的信息记录,所以用户希望表的第二列为只读(显然产科病人不会有男的),为了显示第二列的特殊性,需要让他的字体大小和表格的颜色于其他列有区别,这样方便医生的操作。
如果使用上一章的QSqlTableModel,这似乎是个无法完成的任务,主要原因在于QSqlTableModel跟多面向普遍的数据类型,而对于这种具有一定特殊要求的数据类型,不止QSqlTableModel,很多Qt提供的模型都会有些力不从心,遇到这种情况,就需要使用自定义模型了。
Qt主要提供了QAbctractListModel,QAbctractTableModel和QAbctractProxyModel这三个抽象模型用于自定义模型,而这三个抽象模型都继承自QAbctractItemModel。根据数据结构,这里使用QAbctractTableModel模型。
先看下自定义模型的头文件(该模型将用于代替上一章的QSqlTableModel)
#include <QAbstractTableModel>
class PatientInfo : public QAbstractTableModel
{
Q_OBJECT
private:
QList<QString> title_List;
QList<QString> name_List;
QList<QString> sex_List;
QList<int> age_List;
QList<QString> date_List; //注释1
int dataCount_int;
public:
explicit PatientInfo(QObject *parent = 0);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; //注释2
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
void searchDataFromDatabase(int beginIndex = 0); //注释3
};
注释1 :程序中表有四列,这里使用四个链表来储存每次显示的十条数据。下面的变量dataCount_int用于记录每次显示的数据的数量(默认为10);
注释2:要完成一个(只读)自定义模型,必须重新实现这四个函数,其中除headerData()函数外,其他三个是必须重新实现的纯虚函数,这些函数会被视图(这个例子里将会使用QtableView)调用,rowCount()和columnCount()将向视图返回行数于列数,而data()函数则向视图返回每个数据。headerData()用于控制水平以及垂直表头,如果想使用默认表头,这个函数可以不用重新实现。
注释3:该自定义函数用于查询数据,作用类似上一章里QSqlTableModel的select()函数,不过该函数将对查询的条目做显示,避免一口气查询整张表的情况。
然后是该模型的实现,首先是构造函数
PatientInfo::PatientInfo(QObject *parent)
: QAbstractTableModel(parent)
{
title_List.append(tr("Name"));
title_List.append(tr("Sex"));
title_List.append(tr("Age"));
title_List.append(tr("CheckDate")); //注释1
dataCount_int = 10; //注释2
searchDataFromDatabase();
}
注释1:这里将水平表头放置一个链表里,这样做的原因参加下面
注释2 :默认每次值显示(查询)十条数据,而searchDataFromDatabase()函数用于查询数据。
int PatientInfo::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent)
return name_List.count(); //注释1
}
int PatientInfo::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent)
return title_List.count(); //注释2
}
注释1:这两个函数用于向视图提供表格的行数与列数,其中函数的参数parent用不到,所以使用Q_UNUSED宏来避免编译器发出一些警告信息,该宏不会对程序产生任何作用。表格的列数等于水平表头的数量,所以这里只要返回表头链表的数量。
注释2:该类总共有四个链表来储存表的四列数据,每个链表的数量都一样,这里返回任意一个链表的count()都可以。
接下来是比较关键的data()函数了,先看下代码
QVariant PatientInfo::data(const QModelIndex& index, int role) const
{
if(!(index.isValid()))
return QVariant(); //注释1
if(role == Qt::DisplayRole && index.isValid()) //注释2
{
int columns = index.column();
int rows = index.row();
if(columns == 0)
return QVariant(name_List.at(rows));
if(columns == 1)
return QVariant(sex_List.at(rows));
if(columns == 2)
return QVariant(age_List.at(rows));
if(columns == 3)
return QVariant(date_List.at(rows));
}
if(role == Qt::TextAlignmentRole) //注释3
return QVariant(Qt::AlignCenter);
if(role == Qt::BackgroundColorRole && index.column() == 1) //注释4
return QVariant(QColor(150,150,150));
if(role == Qt::FontRole && index.column() == 1)
{
QFont ft;
ft.setPixelSize(20);
return QVariant(ft);
}
return QVariant();
}
注释1:首先要判断下索引是否可用,不可用的话该函数什么都不做
注释2:这里有个比较重要的概念,前面说过,该函数是向视图返回(某个索引)的数据,就这个例子而言,可以理解为返回表格里指定格子的数据。但这个数据,并不等同于表格上显示的数据(比如年龄这一列数据有24,25,31等),还包括了每个数据显示是,字体大小,对其方式,背景颜色等等。而data()函数的参数role的功能就是指定数据类型。data()函数文档上虽然把这个值写成int类型,但起始是个枚举类型Qt::ItemDataRole,这个枚举值确定了数据类型,这个例子里会用到Qt::AlignmentRole,Qt::BackgroundColorRole以及Qt::FontRole,该枚举量的全部值比较多,这里就不全部列出了,有需要的可以查询文档。
当数据类型为Qt::DisplayRole的时候,说明需要向视图返回的是显示的数据,通过index的row()和column()函数可以知道需要哪一行的哪一列数据,然后把数据以QVariant的类型返回。
注释3:当数据类型为Qt::AlignmentRole的时候说明处理的是表格数据的对其方式,这里返回的是Qt::AlignCenter,当也可以根据具体需要返回Qt::AlignLeft,Qt::AlignRight或者其他的对其方式,注意返回类型必须是QVariant.
注释4:这里就处理前面提到的把第二行的背景演示和字体大小区别于其他列,以背景颜色为例,由于只需要处理第二列,所以除里数据类型为Qt::BackgroundColorRole外,索引index的column()值必须为1(即第二列)。
接下来是设置表头的函数
QVariant PatientInfo::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole) //注释1
return QVariant(title_List.at(section));
else if(orientation == Qt::Vertical && role == Qt::DisplayRole)
return QVariant(QString::number(section+1));
return QVariant();
}
注释1:表头函数处理数据是同样有“数据类型”,该值和上面data()函数的完全一样,所以如果需要更改表头的字体,背景演示等等,可以参照data()的方法。
最后是查询(改变)数据的函数
void PatientInfo::searchDataFromDatabase(int beginIndex)
{
QSqlQuery sqlWrite;
QString dataCount = QString::number(dataCount_int); //注释1
QString sqlStr = tr("SELECT name,sex,age,checkDate FROM Patient LIMIT ") + dataCount + tr(" OFFSET ") + QString::number(beginIndex) + tr(";");
if(sqlWrite.exec(sqlStr) == false)
{
qDebug()<<"SQL is Fail...";
return;
}
beginResetModel(); //注释2
name_List.clear();
sex_List.clear();
age_List.clear();
date_List.clear();
while(sqlWrite.next())
{
name_List.append(sqlWrite.value(tr("name")).toString());
sex_List.append(sqlWrite.value(tr("sex")).toString());
age_List.append(sqlWrite.value(tr("age")).toInt());
date_List.append(sqlWrite.value(tr("checkDate")).toString());
}
endResetModel();
}
注释1:该函数的参数为查询的起始索引,默认为0,即从第1行开始查询总共十条数据。
注释2:这里有对组合函数beginResetModel()和endResetModel(),这两个函数的作用是分别告诉视图,数据马上要开始改变了和数据改变已经完成。这样视图就会“刷新”显示的数据,如果没有这两个函数的组合,视图不会知道,模型的数据什么时候改变了,以及什么时候改变完成,这样视图就不会改变显示的数据。
到这里为止,我们完成了一个只读的模型,接下来要做的是和一个视图配合。下面是程序的头文件代码
class PatientRecord : public QDialog
{
Q_OBJECT
private:
QPushButton* pageUp_PushButton;
QPushButton* pageDown_PushButton;
QTableView* info_TableView;
PatientInfo* info_Model;
public:
PatientRecord(QWidget *parent = 0);
~PatientRecord();
};
然后是实现文件代码
const QString DATABASE_FILE_PATH = "/home/vim1024/sqliteFile/obs.db"; //注释1
PatientRecord::PatientRecord(QWidget *parent)
: QDialog(parent)
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(DATABASE_FILE_PATH);
db.open();
pageUp_PushButton = new QPushButton(tr("PageUP"));
pageDown_PushButton = new QPushButton(tr("PageDownn"));
info_TableView = new QTableView;
info_Model = new PatientInfo;
info_TableView->setFixedSize(500,500);
info_TableView->setModel(info_Model);
info_TableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
info_TableView->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);
QHBoxLayout* button_Layout = new QHBoxLayout;
button_Layout->addStretch();
button_Layout->addWidget(pageUp_PushButton);
button_Layout->addStretch();
button_Layout->addWidget(pageDown_PushButton);
button_Layout->addStretch();
QVBoxLayout* main_Layout = new QVBoxLayout;
main_Layout->addWidget(info_TableView);
main_Layout->addLayout(button_Layout);
setLayout(main_Layout);
main_Layout->setSizeConstraint(QLayout::SetFixedSize);
}
注释1:这个是我的数据库文件的位置,请改成自己的数据库文件的位置。
到这里位置这个程序仍然是只读的,而按照我们的需求,第二列(性别)是只读的,其他列均是可读写,要想实现读写的功能,还必须实现模型的另外两个虚函数,他们分别是
bool setData(const QModelIndex &index, const QVariant &value, int role);
Qt::ItemFlags flags(const QModelIndex &index) const;
setData()函数很好理解,他的作用就是从视图获取(用户输入的)数据,并将改动的数据保存至模型,setData()和前面的data()函数构成了和视图交换数据的两座桥梁。而flag()函数则用于控制模型是否允许编辑数据。
首先看下setDate()函数的实现
bool PatientInfo::setData(const QModelIndex &index, const QVariant &value, int role)
{
if(!(index.isValid()))
return false;
if(role == Qt::EditRole)
{
int columns = index.column();
int rows = index.row();
if(columns == 0)
name_List[rows] = value.toString();
if(columns == 2)
{
bool onLevel = false;
int v = value.toInt(&onLevel);
if(onLevel)
age_List[rows] = v; //注释1
}
if(columns == 3)
date_List[rows] = value.toString();
emit dataChanged(index,index); //注释2
return true;
}
return false;
}
注释1:表的第三列为整数,所以当用户输入的时候需要考虑用户有可能输入了错误的值,比如输入了一个非数字,这里需要做下判断,在读写模型中最大的问题就是需要考虑用户可能出现的错误操作进而引起对数据的破坏,这里就是一个简单的例子,关于如何减少用户的输入错误,下一章委托会有更详细的介绍。另外这里更改的数据没有第二列,前面说过这一列不需要编辑。
注释2:当数据成功修改后,需要发射一个信号,这个信号是setData()函数更改数据后会发射的信号,我们重新实现了setData()函数,当成功修改数据后,仍然需要发射这个信号,确保与这个信号相连的槽能正常工作。
然后是flag()函数的实现
Qt::ItemFlags PatientInfo::flags(const QModelIndex &index) const
{
Qt::ItemFlags fg = QAbstractItemModel::flags(index);
if(index.column() == 1) //注释1
return fg;
else
return fg|Qt::ItemIsEditable; //注释2
}
注释1:第二行性别不需要编辑,而模型默认是不接受编辑的,所以这里第二行直接使用默认的flag即可
注释2:其他行需要编辑,需要添加上Qt::ItemIsEditable这个量,Qt::ItemFlags这个枚举值用于记录模型的某个item的属性,除了这里的允许编辑(Qt::ItemIsEditable)外,还包括Qt::ItemIsSelected,Qt::ItemIsDropEnable,Qt::ItemDrapEnable等等。