【Qt】《Qt 5.9 C++开发指南》笔记-Chapter11-数据库

第11章 数据库

Qt SQL模块提供数据库编程的支持,Qt支持多种常见的数据库,如MySQL、Oracle、MS SQL Server、SQLite等。Qt SQL模块包括多个类,可以实现数据库连接、SQL语句执行、数据获取与界面显示等功能,数据与界面之间使用Model/View架构,从而可以方便地实现数据的界面显示和操作。

11.1 Qt SQL模块概述

要在项目中使用Qt SQL模块,需在项目配置文件中增加下面一条设置语句:

Qt += sql

在头文件或源程序文件中使用Qt SQL模块中的类,可以使用包含语句:

#include <QtSql>

这样会将Qt SQL模块中的所有类都包含进去,如果只使用其中的某些类,为避免冗余可以单独包含某个类。

11.1.1 Qt SQL支持的数据库

Qt SQL提供了一些常见数据库的驱动,包括网络型数据库,如Oracle、MS SQL Server等,也包括简单的单机型数据库,如SQLite。Qt SQL提供的数据库驱动见表11-1。

表11-1 Qt SQL支持的数据库

驱动名数据库
QDB2IBM DB2(7.1及以上版本)数据库
QIBASEBorland InterBase数据库
QMYSQLMySQL数据库
QOCIOracle 调用接口驱动(Oracle Call Interface Driver)
QODBCOpen Database Connectivity(ODBC),Microsoft的SQL Server数据库,以及其他支持ODBC接口的数据库,如Access
QPSQLPostgreSQL(7.3及以上版本)数据库
QSQLITE2SQLite 2数据库
QSQLITESQLite 3数据库
QTDSSybase Adaptive Server,注意:从Qt 4.7开始已过时

11.1.2 SQLite数据库

本书假设读者对数据库的基本概念和SQL语句已经有基础的知识,为了用实例研究Qt SQL的数据库编程功能,本章采用SQLite数据库作为数据库实例。

SQLite是一种无需服务器、无需进行任何配置的数据库,所有的数据表、索引等数据库元素全都存储在一个文件里,在应用程序里使用SQLite数据库就完全可以当作一个文件来使用。SQLite是可以跨平台使用的数据库,在不同平台之间可以随意复制数据库。SQLite的驱动库文件很小,包含完整功能的驱动可以小到只有500 KB。

SQLite是一种开源免费使用的数据库,可以从其官网下载最新版本的数据库驱动安装文件。

SQLite Expert是SQLite数据库可视化管理工具,可以从其官网下载最新的安装文件,SQLite Expert安装文件带有SQLite数据库驱动,所以安装SQLite Expert后无需再下载安装SQLite数据库驱动。

在SQLite Expert软件里建立一个数据库demodb.db3,在此数据库里建立4个表,本章的Qt SQL编程实例都采用这个数据库文件作为数据库实例。

SQLite Expert软件进行数据库字段设计的界面如原书的图11-1所示。

(1)employee数据表是一个员工信息表,用于在实例samp11_1里演示通过QSqlTableModel获取、显示和编辑数据表的内容,employee的字段定义见表11-2。

表11-2 employee数据表的字段定义

序号字段名类型描述说明
1EmpNoINT员工编号主键,非空
2NameVARCHAR(20)姓名非空
3GenderVARCHAR(4)性别缺省值=“男”
4HeightFLOAT身高缺省值=1.71
5BirthdayDATE出生日期
6MobileVARCHAR(18)手机号
7ProvinceVARCHAR(20)省份
8CityVARCHAR(20)城市
9DepartmentVARCHAR(30)工作部门
10EducationVARCHAR(16)教育程度
11SalaryCURRENCY工资缺省值=3500
12PhotoBLOB照片BLOB字段可存储任何二进制内容
13MemoMEMO备注MEMO字段可存储任意长度普通文本

(2)departments数据表是一个学院信息表,记录学院编号和学院名称。其字段定义见表11-3。

表11-3 departments数据表的字段定义

序号字段名类型描述说明
1departIDINT学院编号主键,非空
2departmentsVARCHAR(40)学院名称非空

(3)majors数据表是专业信息表,记录各专业的信息。其字段定义见表11-4。

表11-4 majors数据表的字段定义

序号字段名类型描述说明
1majorIDINT专业编号主键,非空
2majorVARCHAR(40)专业名称非空
3departIDINT所属学院编号非空,等于departments表中某个学院的departID

departments和majors构成一个Master/Detail关系数据表,majors表里的departID字段记录了这个专业属于哪个学院,departments表里的一条记录关联majors表中的多条记录。

(4)studInfo是一个记录学生信息的数据表。其字段定义见表11-5。

表11-5 studInfo数据表的字段定义

序号字段名类型描述说明
1studIDINT学号主键,非空
2nameVARCHAR(10)姓名非空
3genderVARCHAR(4)性别
4departIDINT学院编号关联departments表中的记录
5majorIDINT专业编号关联majors表中的记录

studInfo表中记录学生的所在学院采用了代码字段departID,具体的学院名称需要通过查询departments表中相同的departID的记录获得;majorID记录了专业代码,具体的专业名称需要查找majors表中的记录获取。这两个字段都是代码字段,只存储代码,具体的意义需要查询关联数据表的相应记录获得,在实际的数据库设计中经常用到这种设计方式。

Qt SQL中使用QSqlRelationalTableModel可以很方便地实现这种代码型数据表的显示与编辑,实例samp11_4显示了QSqlRelationalTableModel的使用方法。

11.1.3 Qt SQL模块的主要类

Qt SQL提供的主要类的简要功能描述见表11-6。

表11-6 Qt SQL模块包含的主要类的功能

类名称功能描述
QSqlDatabase用于建立与数据库的连接
QSqlDriver用于访问具体的SQL数据库的底层抽象类
QSqlDriverCreator为某个具体的数据库驱动提供SQL驱动的模板类
QSqlDriverCreatorBase所有SQL驱动器的基类
QSqlDriverPlugin用于定制QSqlDriver插件的抽象基类
QSqlErrorSQL数据库错误信息,可以用于访问上一次出错的信息
QSqlField操作数据表或视图(view)的字段的类
QSqlIndex操作数据库的索引的类
QSqlQuery执行各种SQL语句的类
QSqlQueryModelSQL查询结果数据的只读数据模型(data model),用于SELECT查询结果数据记录的只读显示
QSqlRecord封装了数据记录操作的类
QSqlRelation用于存储SQL外键信息的类,用于QSqlRelationalTableModel数据源中设置代码字段与关联数据表的关系
QSqlRelationalDelegate用于QSqlRelationalTableModel的一个代码字段的显示和编辑代理组件,一般是一个QComboBox组件,下拉列表中自动填充代码表的代码字段对应的实际内容
QSqlRelationalTableModel用于一个数据表的可编辑的数据模型,支持代码字段的外键
QSqlResult访问SQL数据库的抽象接口
QSqlTableModel编辑一个单一数据表的数据模型类
QDataWidgetMapper用于界面组件与字段之间实现映射,实现字段内容自动显示的类

QSqlDatabase用于建立与数据库的连接,一般是先加载需要的数据库驱动,然后设置数据库的登录参数,如主机地址、用户名、登录密码等,如果是单机型数据库,如SQLite,只需设置数据库文件即可。

数据库的操作一般需要将数据库的内容在界面上进行显示和编辑,Qt采用 Model/View结构进行数据库内容的界面显示。QTableView是常用的数据库内容显示视图组件,用于数据库操作的数据模型类有QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel,这几个类的继承关系见原书的图11-2。

QSqlQueryModel通过设置SELECT语句查询获取数据库的内容,但是QSqlQueryModel的数据是只读的,不能进行编辑。

QSqlTableModel直接设置一个数据表的名称,可以获取数据表的全部记录,其结果是可编辑的,设置为界面上的QTableView组件的数据模型后就可以显示和编辑数据。

QSqlRelationalTableModel编辑一个数据表,并且可以将代码字段通过关系与代码表关联,将代码字段的编辑转换为直观的内容选择编辑。

QSqlQuery是另外一个经常使用的类,它可以执行任何SQL语句,特别是没有返回记录的语句,如UPDATE、INSERT、DELETE等,通过SQL语句对数据库直接进行编辑修改。

11.2 QSqlTableModel的使用

11.2.1 实例功能

实例samp11_1使用QSqlTableModel显示实例数据库demodb中employee数据表的内容,实现编辑、插入、删除记录的操作,实现数据的排序和记录过滤,还实现BLOB类型字段Photo中存储照片的显示、导入等操作,运行界面如原书的图11-3所示。

在图11-3的中,左侧数据表格是一个QTableView组件,设置一个QSqlTableModel类的变量作为数据模型后就可以显示数据表的内容。右侧的一些编辑框、下拉列表框等。界面组件通过QDataWidgetMapper类的实例设置为与某个字段关联,自动显示字段的内容。但是没有现成的组件可以通过字段映射显示Photo字段中的照片。为此使用一个QLabel组件,通过其pixmap属性显示图片,所以照片的显示、导入和清除需要单独操作,并且与数据模块的记录移动保持同步,如同将此界面组件与Photo字段映射了一样。

QTableView显示内容有缺省的代理组件,一般是自动使用QLineEdit组件。但是对于某些字段期望通过下拉列表框来选择输入,例如“性别”和“部门”这两个字段。为此,还设计了自定义数据代理类,“性别”和“部门”两个字段使用QComboBox进行编辑。

工具栏上的按钮根据当前状态自动可用或禁用,特别是“保存”和“取消”两个按钮在数据表的内容被修改后自动变为可用,当保存或取消修改后又变为不可用。

11.2.2 主窗口设计

实例samp11_1是一个主窗口继承自QMainWindow的应用程序,使用UI设计器可视化设计界面。设计的Action如原书的图11-4所示,使用Action创建工具栏按钮,其他界面组件和布局不再详述。

以下是主窗口类MainWindow的定义(一些通用的部分,自动生成的界面组件的槽函数省略了)。

#include   <QtSql>
#include   <QDataWidgetMapper>
class MainWindow : public QMainWindow
{
   Q_OBJECT
private:
   QSqlDatabase  DB;                    //数据库连接
   QSqlTableModel  *tabModel;           //数据模型
   QItemSelectionModel *theSelection;   //选择模型
   QDataWidgetMapper   *dataMapper;     //数据映射
   QWComboBoxDelegate   delegateSex;    //自定义数据代理,性别
   QWComboBoxDelegate   delegateDepart; //自定义数据代理,部门
   void   openTable();                  //打开数据表
   void   getFieldNames();              //获取字段名称,填充“排序字段”的comboBox
public:
   explicit MainWindow(QWidget *parent = 0);
   ~MainWindow();
private slots:
    void on_currentChanged(const QModelIndex &current, const QModelIndex &previous);
    void on_currentRowChanged(const QModelIndex &current, const QModelIndex &previous);
}

MainWindow类中定义了几个私有变量。

  • QSqlDatabase DB,用于加载数据库驱动和建立与数据库之间的连接。

  • QSqlTableModel *tabModel,用于指定某一个数据表,作为数据表的数据模型。

  • QItemSelectionModel *theSelection,作为tabModel的选择模型,提供currentChanged()、currentRowChanged()等信号,在tabModel选择的字段发生变化、当前记录发生变化时发射信号,以便程序进行响应。例如在currentChanged()信号发射时,检查tabModel是否有数据被修改,从而更新界面上“保存”和“取消”两个按钮的使能状态。

  • QDataWidgetMapper *dataMapper用于实现界面组件与tabModel的字段之间的映射。例如界面上的QLineEdit类型的dbEditName组件与数据表的Name字段映射,当前记录变化时会自动更新显示当前记录的Name字段的数据。

  • QWComboBoxDelegate是自定义的基于QComboBox的代理类,delegateSex和delegateDepart用作tableView中“性别”和“部门”字段的代理组件。

私有函数openTable()用于打开数据表,getFieldNames()用于获取数据表employee的所有字段的名称,并填充界面上“排序字段”后的ComboBox组件。

定义了两个槽函数,功能如下。

  • on_currentChanged()用于检查数据表内容是否有修改,从而更新“保存”和“取消”两个按钮的使能状态。

  • on_currentRowChanged()用于在当前记录发生变化时,从新的当前记录里提取Photo字段的内容,并作为界面上的QLabel类型的dblabPhoto组件的pixmap显示出来。

MainWindow的构造函数代码如下,主要是对tableView的一些显示属性的设置。

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
   ui->setupUi(this);
   this->setCentralWidget(ui->splitter);
//   tableView显示属性设置
   ui->tableView->setSelectionBehavior(QAbstractItemView::SelectItems);
   ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
   ui->tableView->setAlternatingRowColors(true);
}

11.2.3 打开数据表

工具栏上的“打开”按钮由actOpenDB生成,单击此按钮时将添加SQLite数据库驱动、打开数据库文件、连接employee数据表并设置显示属性,并且创建tableView显示的代理组件,设置数据源与界面组件的映射等。其响应代码如下:

void MainWindow::on_actOpenDB_triggered()
{//打开数据库
   QString aFile=QFileDialog::getOpenFileName(this,"选择数据库文件","",
                      "SQLite数据库(*.db *.db3)");
   if (aFile.isEmpty())
     return;
   DB=QSqlDatabase::addDatabase("QSQLITE"); //添加 SQLITE数据库驱动
   DB.setDatabaseName(aFile); //设置数据库名称
   if (!DB.open())   //打开数据库
   {
      QMessageBox::warning(this, "错误", "打开数据库失败",
                    QMessageBox::Ok,QMessageBox::NoButton);
      return;
   }
   openTable();//打开数据表
}

由打开文件对话框选择需要打开的SQLite数据库文件,本实例中需要打开demodb.db3数据库,此文件在第11章实例程序总目录下。

私有变量DB是QSqlDatabase类。QSqlDatabase用于数据库的操作,包括建立数据库连接,设置登录数据库的参数,打开数据库等。下面的一行语句使用QSqlDatabase的静态函数addDatabase()添加SQLite数据库的驱动。

DB=QSqlDatabase::addDatabase("QSQLITE");

然后需要使用QSqlDatabase的几个函数设置数据库登录参数,setDatabaseName()设置数据库名称,对于SQLite数据库,就设置为数据库文件。如果是网络型数据库,如Oracle、MS SQL Server等,还需要使用setHostName()设置数据库主机名,setUserName()设置数据库用户名,setPassword()设置数据库登录密码。对于SQLite数据库,只需用setDatabaseName()设置数据库文件即可。

数据库连接与登录参数设置后,调用QSqlDatabase::open()函数打开数据库。如果成功打开数据库,再调用自定义函数openTable()打开数据表employee,并进行相应的操作。openTable()函数代码如下:

void MainWindow::openTable()
{//打开数据表
   tabModel=new QSqlTableModel(this,DB);//数据表
   tabModel->setTable("employee"); //设置数据表
   tabModel->setSort(tabModel->fieldIndex("EmpNo"),Qt::AscendingOrder); 
   tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
   if (!(tabModel->select()))//查询数据
   {
      QMessageBox::critical(this, "错误", "打开数据表错误,错误信息\n"
                +tabModel->lastError().text(),
               QMessageBox::Ok,QMessageBox::NoButton);
      return;
   }
//设置字段显示名
   tabModel->setHeaderData(tabModel->fieldIndex("EmpNo"), Qt::Horizontal,  "工号"); 
   tabModel->setHeaderData(tabModel->fieldIndex("Name"),  Qt::Horizontal,  "姓名");
   tabModel->setHeaderData(tabModel->fieldIndex("Gender"),Qt::Horizontal, "性别");
   tabModel->setHeaderData(tabModel->fieldIndex("Height"),Qt::Horizontal, "身高");
   tabModel->setHeaderData(tabModel->fieldIndex("Birthday"),Qt::Horizontal,"出生日期");
   tabModel->setHeaderData(tabModel->fieldIndex("Mobile"), Qt::Horizontal, "手机");
   tabModel->setHeaderData(tabModel->fieldIndex("Province"), Qt::Horizontal, "省份");
   tabModel->setHeaderData(tabModel->fieldIndex("City"), Qt::Horizontal, "城市");
   tabModel->setHeaderData(tabModel->fieldIndex("Department"), Qt::Horizontal, "部门");
   tabModel->setHeaderData(tabModel->fieldIndex("Education"), Qt::Horizontal, "学历");
   tabModel->setHeaderData(tabModel->fieldIndex("Salary"),  Qt::Horizontal,  "工资");
   tabModel->setHeaderData(tabModel->fieldIndex("Memo"),    Qt::Horizontal,  "备注"); 
   tabModel->setHeaderData(tabModel->fieldIndex("Photo"),   Qt::Horizontal,  "照片"); 
   theSelection=new QItemSelectionModel(tabModel);//选择模型
   connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),
         this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
  connect(theSelection,SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
         this,SLOT(on_currentRowChanged(QModelIndex,QModelIndex)));

   ui->tableView->setModel(tabModel);//设置数据模型
   ui->tableView->setSelectionModel(theSelection); //设置选择模型
   ui->tableView->setColumnHidden(tabModel->fieldIndex("Memo"),true);
   ui->tableView->setColumnHidden(tabModel->fieldIndex("Photo"),true);
//tableView上为“性别”和“部门”两个字段设置自定义代理组件
   QStringList strList;
   strList<<"男"<<"女";
   bool isEditable=false;
   delegateSex.setItems(strList,isEditable);
   ui->tableView->setItemDelegateForColumn(
         tabModel->fieldIndex("Gender"), &delegateSex); 

   strList.clear();
   strList<<"销售部"<<"技术部"<<"生产部"<<"行政部";
   isEditable=true;
   delegateDepart.setItems(strList,isEditable);
   ui->tableView->setItemDelegateForColumn( 
         tabModel->fieldIndex("Department"),&delegateDepart); 

//创建界面组件与数据模型的字段之间的数据映射
   dataMapper= new QDataWidgetMapper();
   dataMapper->setModel(tabModel);//设置数据模型
   dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
//界面组件与tabModel的具体字段之间的联系
   dataMapper->addMapping(ui->dbSpinEmpNo,tabModel->fieldIndex("EmpNo"));
   dataMapper->addMapping(ui->dbEditName,tabModel->fieldIndex("Name"));
   dataMapper->addMapping(ui->dbComboSex,tabModel->fieldIndex("Gender"));
dataMapper->addMapping(ui->dbSpinHeight,tabModel->fieldIndex("Height"));
dataMapper->addMapping(ui->dbEditBirth,tabModel->fieldIndex("Birthday"));
dataMapper->addMapping(ui->dbEditMobile,tabModel->fieldIndex("Mobile"));
   dataMapper->addMapping(ui->dbComboProvince,
                 tabModel->fieldIndex("Province"));
   dataMapper->addMapping(ui->dbEditCity,tabModel->fieldIndex("City"));
   dataMapper->addMapping(ui->dbComboDep,
                 tabModel->fieldIndex("Department"));
dataMapper->addMapping(ui->dbComboEdu,tabModel->fieldIndex("Education"));
dataMapper->addMapping(ui->dbSpinSalary,tabModel->fieldIndex("Salary"));
   dataMapper->addMapping(ui->dbEditMemo,tabModel->fieldIndex("Memo"));
   dataMapper->toFirst();//移动到首记录

   getFieldNames();//获取字段名称列表,填充ui->groupBoxSort组件
//更新actions和界面组件的使能状态
   ui->actOpenDB->setEnabled(false);
   ui->actRecAppend->setEnabled(true);
   ui->actRecInsert->setEnabled(true);
   ui->actRecDelete->setEnabled(true);
   ui->actScan->setEnabled(true);
   ui->groupBoxSort->setEnabled(true);
   ui->groupBoxFilter->setEnabled(true);
}

函数openTable()主要是创建QSqlTableModel类型的私有变量tabModel,指定需要打开的数据表为employee,设置与界面显示组件的关联等。QSqlTableModel设置一个数据表名称后,作为数据表的数据模型,可以方便地编辑一个数据表。

使用的核心的类是QSqlTableModel,其主要的函数功能见表11-7(省略了函数中的const关键字,省略了缺省参数)。

表11-7 QSqlTableModel类的主要函数

函数功能描述
QSqlDatabase database()返回其数据库连接
void setTable(QString &tableName)设置数据表名称,不立即读取记录,但会提取字段信息
QString tableName()返回设置的数据表名称
void setFilter(QString &filter)设置记录过滤条件
void setSort(int column, Qt::SortOrder order)设置排序字段和排序规则,需调用select()才生效
void sort(int column, Qt::SortOrder order)按列号column和排序规则立即进行排序并获取数据
void setEditStrategy(EditStrategy strategy)设置编辑策略
bool setHeaderData(int section, Qt::Orientation orientation, QVariant &value,)设置表头,一般用于设置字段的显示名称
bool isDirty()若有未更新到数据库的修改,就返回true;否则返回false
int fieldIndex(QString &fieldName)根据字段名称返回其在模型中的字段序号,若字段不存在则返回-1
QSqlIndex primaryKey()返回数据表的主索引
int rowCount()返回记录条数
bool select()查询数据表的数据,并使用设置的排序和过滤规则
bool selectRow(int row)刷新获取指定行号的记录
void clear()清除数据模型,释放所有获取的数据
QSqlRecord record()返回一条空记录,只有字段名,可用来获取字段信息
QSqlRecord record(int row)返回行号为row的一条记录,包含记录的数据
bool setRecord(int row, QSqlRecord &values)更新一条记录的数据到数据模型,源和目标之间通过字段名称匹配,而不是按位置匹配
bool insertRecord(int row, QSqlRecord &record)在行号row之前插入一条记录
bool insertRows(int row, int count)在行号row之前插入count空行,编辑策略为OnFieldChange 或OnRowChange时只能插入一行
bool removeRows(int row, int count)从行号row开始,删除count行。若编辑策略为OnManualSubmit,需调用submitAll()才从数据表里删除
void revertRow(int row)取消行号为row的记录的修改
void revert()编辑策略为OnRowChange或OnFieldChange时,取消当前行的修改,对 OnManualSubmit编辑策略无效
void revertAll()取消所有未提交的修改
bool submit()提交当前行的修改到数据库,对OnManualSubmit编辑策略无效
bool submitAll()提交所有未更新的修改到数据库,若成功则返回true;否则返回false,错误的详细信息可以从lastError()获取

函数openTable()的代码主要包括以下几个部分的功能。

1.数据模型创建与属性设置

首先创建QSqlTableModel类型的私有变量tabModel,并且在创建时指定了数据库连接,就是前面设置的私有变量DB,然后用setTable()函数指定数据表。

setSort()函数设置排序字段和排序方式,其函数原型为:

void QSqlTableModel::setSort(int column, Qt::SortOrder order)

第1个参数column是排序字段的列号;第2个参数order是枚举类型Qt::SortOrder,表示排序方式,取值Qt::AscendingOrder表示升序,Qt::DescendingOrder表示降序。程序中设置为根据EmpNo字段升序排序,即:

tabModel->setSort(tabModel->fieldIndex("EmpNo"),Qt::AscendingOrder); 

QSqlTableModel::setEditStrategy()函数用于设置编辑策略,参数是枚举类型QSqlTableModel::EditStrategy,各取值的意义如下。

  • QSqlTableModel::OnFieldChange,字段值变化时立即更新到数据库。

  • QSqlTableModel:: OnRowChange,当前行(就是记录)变换时更新到数据库。

  • QSqlTableModel:: OnManualSubmit,所有修改暂时缓存,手动调用submitAll()保存所有修改,或调用revertAll()函数取消所有未保存修改。

程序里设置编辑策略为QSqlTableModel::OnManualSubmit,在修改数据后并不直接提交更新,只是让工具栏上的“保存”和“取消”按钮可用,用户手动提交或取消修改。

对tabModel设置这些属性后,调用select()函数打开数据表,如果打开失败,可以通过QSqlTableModel::lastError()函数获取上一错误信息的文本说明。

2.表头设置

QSqlTableModel::setHeaderData()函数用于设置每个字段的表头数据,主要是设置显示标题。如果不进行表头设置,在TableView里显示时将显示字段名作为表头。为此,将每个字段的显示设置为相应的中文标题。例如,设置“Name”字段的显示标题为“姓名”的代码是:

tabModel->setHeaderData(tabModel->fieldIndex("Name"), Qt::Horizontal,  "姓名");

setHeaderData()函数的第1个参数是字段的序号,通过fieldIndex()函数可以获取某个字段名在数据模型里的序号,避免直接使用数字时不便于理解和修改的问题。

3.选择模型及其信号的作用

为数据模型创建一个选择模型的代码如下:

theSelection=new QItemSelectionModel(tabModel);

选择模型的作用是当用户在TableView组件上操作时,获取当前选择的行、列信息,并且在选择的单元格变化时发射currentChanged()信号,在当前行变化时发射currentRowChanged()信号。

为currentChanged()信号编写槽函数on_currentChanged(),获取tabModel->isDirty()函数的值,根据是否有未提交的修改,更新工具栏按钮“保存”或“修改”的使能状态。槽函数on_currentChanged()的代码如下:

void MainWindow::on_currentChanged(const QModelIndex &current, const QModelIndex &previous)
{//更新actSubmit和actRevert的状态
   Q_UNUSED(current);
   Q_UNUSED(previous);
   ui->actSubmit->setEnabled(tabModel->isDirty()); //有未保存修改时可用
   ui->actRevert->setEnabled(tabModel->isDirty());
}

为currentRowChanged()信号编写槽函数on_currentRowChanged(),用于在当前行变化时,从新的记录里提取Photo字段的内容,并将图片在QLabel组件中显示出来。on_currentRowChanged()的代码如下:

void MainWindow::on_currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
{
   Q_UNUSED(previous);
// 行切换时的状态控制
   ui->actRecDelete->setEnabled(current.isValid());
   ui->actPhoto->setEnabled(current.isValid());
   ui->actPhotoClear->setEnabled(current.isValid());
   if (!current.isValid())
   {
      ui->dbLabPhoto->clear();  //清除图片显示
      return;
   }

   dataMapper->setCurrentIndex(current.row());   //更新数据映射的行号
   int curRecNo=current.row();//获取行号
   QSqlRecord  curRec=tabModel->record(curRecNo); //获取当前记录
   if (curRec.isNull("Photo"))  //图片字段内容为空
      ui->dbLabPhoto->clear();
   else
   {
      QByteArray data=curRec.value("Photo").toByteArray();
      QPixmap pic;
      pic.loadFromData(data);
      ui->dbLabPhoto->setPixmap(pic.scaledToWidth( 
                  ui->dbLabPhoto->size().width()));
   }
}

槽函数on_currentRowChanged()传递的参数current是行切换后新的当前行的模型索引,首先根据current是否有效,更新3个Action的使能状态。若current是有效的,更新数据映射dataMapper的当前行,即:

dataMapper->setCurrentIndex(current.row());

这将使界面上的编辑框、下拉列表框等与字段关联的界面组件显示当前记录的内容。

由于没有现成的界面组件可以通过数据映射显示BLOB字段内的图片,在此槽函数里通过编程获取Photo字段的数据,并将其显示为一个QLabel组件的pixmap。

用下面两行代码获取当前行的记录:

int curRecNo=current.row();//获取行号
QSqlRecord  curRec=tabModel->record(curRecNo); //获取当前记录

curRec是QSqlRecord类型,返回了当前记录的数据,可以获取当前记录的每个字段的内容。程序中它获取Photo字段的内容,并将其转换为图片后作为ui->dbLabPhoto组件的pixmap来显示。

4.表示记录的类QSqlRecord

QSqlRecord类记录了数据表的字段信息和一条记录的数据内容,QSqlTableModel有两种参数的函数record()可以返回一条记录。

  • QSqlRecord QSqlTableModel::record(),不带参数的record()函数,返回的一个QSqlRecord对象只有记录的字段定义,但是没有数据。这个函数一般用于获取一个数据表的字段定义。

  • QSqlRecord QSqlTableModel::record(int row),返回行号为row的记录,包括记录的字段定义和数据。

QSqlRecord类封装了对记录的字段定义和数据的操作,其主要函数见表11-8(省略了函数参数中的const关键字,对于具有不同参数的同名函数,只列出一种参数形式的函数)。

表11-8 QSqlRecord类的主要函数

函数原型功能描述
void clear()清除记录的所有字段定义和数据
void clearValues()清除所有字段的数据,将字段数据内容设置为null
bool contains(QString &name)判断记录是否含有名称为name的字段
bool isEmpty()若记录里没有字段,返回true;否则返回false
int count()返回记录的字段个数
QString fieldName(int index)返回序号为index的字段的名称
int indexOf(QString &name)返回字段名称为name的字段的序号,如果字段不存在,返回-1
QSqlField field(QString &name)返回字段名称为name的字段对象
QVariant value(QString &name)返回字段名称为name的字段的值
void setValue(QString &name, QVariant &val)设置字段名称为name的字段的值为val
bool isNull(const QString &name)判断字段名称为name的字段数据是否为null
void setNull(const QString &name)设置名称为name的字段的值为null

QSqlRecord用于字段操作的函数一般有两种参数形式的同名函数,用字段序号或字段名表示一个字段,如value()函数返回一个字段的值,有如下两种参数形式的函数:

  • QVariant value(int index),返回序号为index的字段的值;

  • QVariant value(QString &name),返回字段名称为name的字段的值。

5.表示字段的类QSqlField

QSqlRecord的field()函数返回某个字段,返回数据类型是QSqlField。QSqlField封装了字段定义信息和数据。字段的定义一般在设计数据表时就固定了,不用在QSqlRecord里修改。QSqlRecord用于字段数据读写的函数见表11-9(省略了函数参数中的const关键字)。

表11-9 QSqlField类的主要函数

接口函数功能描述
void clear()清除字段数据,设置为NULL。如果字段是只读的,则不清除
bool isNull()判断字段值是否为NULL
bool setReadOnly(bool readOnly)设置一个字段为只读,只读的字段不能用setValue()函数设置值,也不能用clear()函数清除值
QVariant value()返回字段的值
void setValue(QVariant &value)设置字段的值
6.tableView的设置

界面上用一个QTableView组件显示tabModel的表格数据内容,设置其数据模型和选择模型,并且将Memo和Photo两个字段的列设置为隐藏,因为在表格里难以显示备注文字和图片。

为在tableView中显示和编辑“性别”和“部门”两个字段,设计了基于QComboBox的自定义代理组件类QWComboBoxDelegate。自定义代理组件QWComboBoxDelegate的设计方法在5.5节有详细介绍,这里只是稍作改变,增加了一个setItems()函数,用于初始化下拉列表框和设置是否可以编辑,这样一个QWComboBoxDelegate可以创建多个代理组件实例,分别用于“性别”和“部门”两个字段。

下面是相对于5.5节的QWComboBoxDelegate修改或增加的部分的代码,其余相同的部分没有列出。

class QWComboBoxDelegate : public QStyledItemDelegate
{
   Q_OBJECT
private:
   QStringList  m_ItemList;//选择列表
   bool   m_isEdit; //是否可编辑
public:
   void   setItems(QStringList items, bool isEdit);//初始化设置列表内容
   QWidget  *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE;
};

下面是两个函数的实现代码:

void QWComboBoxDelegate::setItems(QStringList items, bool isEdit)
{
   m_ItemList=items;
   m_isEdit=isEdit;
}
QWidget *QWComboBoxDelegate::createEditor(QWidget *parent,
      const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   Q_UNUSED(option);
   Q_UNUSED(index);
   QComboBox *editor = new QComboBox(parent);
   for (int i=0;iaddItem(m_ItemList.at(i));
   editor->setEditable(m_isEdit); //是否可编辑
   return editor;
}
7.数据映射

QDataWidgetMapper用于建立界面组件与数据模型之间的映射,可以将界面的QLineEdit、QCombobox等组件与数据模型的一个字段关联起来。

创建QDataWidgetMapper类的变量dataMapper后,用setModel()设置关联的数据模型,setSubmitPolicy()函数设置数据提交策略,有自动(AutoSubmit)和手动(ManualSubmit)两种方式。addMapping()用于设置界面组件与数据模型的列的映射,程序在界面上的各编辑组件与数据表的各字段之间建立了映射关系。Memo字段可以与一个QPlainTextEdit的组件映射,但是Photo是一个BLOB字段,没有组件可以直接显示其内容。

QDataWidgetMapper还有toFirst()、toPrevious()、toNext()和toLast() 4个函数用于在记录间移动,setCurrentIndex()和setCurrentModelIndex()可以直接移动到某一行记录,revert()和submit()用于手工取消或提交当前记录的修改,当提交策略设置为QDataWidgetMapper::AutoSubmit时,行切换时将自动提交修改。

8.获取数据表的所有字段名称

自定义函数getFieldNames()获取数据表的所有字段名称,并填充到界面上的“排序字段”下拉列表框里,用于选择排序字段。getFieldNames()的实现代码如下:

void MainWindow::getFieldNames()
{ //获取所有字段名称
   QSqlRecord  emptyRec=tabModel->record();//获取空记录,只有字段名
   for (int i=0;i<emptyRec.count();i++)
      ui->comboFields->addItem(emptyRec.fieldName(i));
}

这里使用了QSqlTableModel::record()函数获取一个空的记录,包括数据表的所有字段信息。

所有这些初始化完成后,更新窗口上相关Actions的使能状态。

11.2.4 添加、插入与删除记录

工具栏上有“添加”“插入”“删除”3个按钮用于记录操作。actRecAppend用于添加记录,其槽函数代码如下:

void MainWindow::on_actRecAppend_triggered()
{//添加记录
   tabModel->insertRow(tabModel->rowCount(),QModelIndex());  
   QModelIndex curIndex=tabModel->index(tabModel->rowCount()-1,1);
   theSelection->clearSelection();//清空选择
   theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select); 
   int currow=curIndex.row(); //获得当前行号
   tabModel->setData(tabModel->index(currow,0),2000+tabModel->rowCount());
   tabModel->setData(tabModel->index(currow,2),"男"); 
}

QSqlTableModel::insertRow(int row)函数在数据模型的row行前面插入一行记录,如果row大于或等于数据模型的总行数,则在最后添加一行记录。

actRecInsert实现在当前行的前面插入一行,其槽函数代码如下:

void MainWindow::on_actRecInsert_triggered()
{//插入记录
   QModelIndex curIndex=ui->tableView->currentIndex();
   tabModel->insertRow(curIndex.row(),QModelIndex());
   theSelection->clearSelection();//清除已有选择
   theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}

actRecDelete用于删除当前记录,其槽函数实现代码如下:

void MainWindow::on_actRecDelete_triggered()
{//删除当前记录
   QModelIndex curIndex=theSelection->currentIndex();//获取当前模型索引
   tabModel->removeRow(curIndex.row()); //删除一行
}

在插入或删除记录操作,未提交保存之前,tableView的左侧表头会以标记表示记录编辑状态,“*”表示新插入的记录,“!”表示删除的记录。在保存或取消修改后,这些标记就消失,删除的记录行也从tableView里删除。

11.2.5 保存与取消修改

在打开数据表初始化时,设置数据模型的编辑策略为OnManualSubmit,即手动提交修改。当数据模型的数据被修改后,不管是直接修改字段值,还是插入或删除记录,在未提交修改前,QSqlTableModel::isDirty()函数返回true,就是利用这个函数在自定义槽函数on_currentChanged()里修改actSubmit和actRevert两个Action的使能状态。

actSubmit用于保存修改,actRevert用于取消修改,对应工具栏上的“保存”和“取消”两个按钮,下面是这两个Action的槽函数的代码。

void MainWindow::on_actSubmit_triggered()
{//保存修改
   bool res=tabModel->submitAll();
   if (!res)
      QMessageBox::information(this, "消息", "数据保存错误,错误信息\n" 
   +tabModel->lastError().text(),QMessageBox::Ok,QMessageBox::NoButton);
   else
   {
      ui->actSubmit->setEnabled(false);
      ui->actRevert->setEnabled(false);
   }
}
void MainWindow::on_actRevert_triggered()
{//取消修改
   tabModel->revertAll();
   ui->actSubmit->setEnabled(false);
   ui->actRevert->setEnabled(false);
}

QSqlTableModel::submitAll()函数用于将数据表所有未提交的修改保存到数据库,revertAll()函数取消所有修改。

调用submitAll()函数保存数据时如果失败,可以通过lastError()函数获取错误的具体信息,例如,Name是必填字段,添加记录时若没有填写Name字段的内容就提交,就会出现错误信息提示对话框。

11.2.6 设置和清除照片

employee数据表的Photo字段是BLOB字段,用于存储图片文件。Photo字段内容的显示已经在自定义槽函数on_currentRowChanged()里实现了,就是在当前记录变化时提取Photo字段的内容,并显示为dbLabPhoto的pixmap。

actPhoto和actPhotoClear是设置照片和清除照片的两个Action,与工具栏上的“设置照片”和“清除照片”按钮关联。actPhoto的槽函数代码如下:

void MainWindow::on_actPhoto_triggered()
{//设置照片
   QString aFile=QFileDialog::getOpenFileName(this,"选择图片", "",  "照片(*.jpg)");
   if (aFile.isEmpty())
      return;

   QByteArray data;
   QFile* file=new QFile(aFile); 
   file->open(QIODevice::ReadOnly);
   data = file->readAll();
   file->close();

   int curRecNo=theSelection->currentIndex().row();
   QSqlRecord  curRec=tabModel->record(curRecNo); //获取当前记录
   curRec.setValue("Photo",data); //设置字段数据
   tabModel->setRecord(curRecNo,curRec);
   QPixmap pic;
   pic.load(aFile);  //在界面上显示
   ui->dbLabPhoto->setPixmap(pic.scaledToWidth(ui->dbLabPhoto->width()));
}

代码功能是用文件对话框选择一个图片文件,读取文件内容到QByteArray类型的变量data里,获取当前记录到变量curRec后,用QSqlRecord的setValue()函数为Photo字段设置数据为data,然后用setRecord()函数更新当前记录。

curRec.setValue("Photo",data); //设置字段数据
tabModel->setRecord(curRecNo,curRec);

这里的更新只是更新到数据模型,并没有更新到数据库。

actPhotoClear用于清除照片,其槽函数实现代码如下:

void MainWindow::on_actPhotoClear_triggered()
{
   int curRecNo=theSelection->currentIndex().row();
   QSqlRecord  curRec=tabModel->record(curRecNo); //获取当前记录
   curRec.setNull("Photo");//设置为空值
   tabModel->setRecord(curRecNo,curRec);
   ui->dbLabPhoto->clear();
}

获取当前记录到变量curRec后,调用setNull()函数将Photo字段设置为NULL,就是清除了字段的内容,然后更新记录到数据模型。

11.2.7 数据记录的遍历

工具栏上的“涨工资”按钮用于将数据表内所有记录的salary字段的内容增加10%,演示了记录遍历的功能。actScan实现此按钮的功能,其槽函数实现代码如下:

void MainWindow::on_actScan_triggered()
{//涨工资,记录遍历
   if (tabModel->rowCount()==0)
      return;
   for (int i=0;i<tabModel->rowCount();i++)
   {
      QSqlRecord   aRec=tabModel->record(i); //获取当前记录
      float  salary=aRec.value("Salary").toFloat();
      salary=salary*1.1;
      aRec.setValue("Salary",salary);
      tabModel->setRecord(i,aRec);
   }
   if (tabModel->submitAll())
      QMessageBox::information(this, "消息", "涨工资计算完毕",
                       QMessageBox::Ok,QMessageBox::NoButton);
}

11.2.8 记录排序

QSqlTableModel的setSort()函数设置数据表根据某个字段按照升序或降序排列,实际上就是设置了SQL语句里的ORDER BY子句。

在打开数据库时,已经调用getFieldNames()函数将数据表的所有字段名添加到界面上的comboFields下拉列表框了。“排序字段”下拉列表框、“升序”和“降序”两个RadioButton的相应操作的槽函数实现按选择字段排序。

void MainWindow::on_comboFields_currentIndexChanged(int index)
{//选择字段进行排序
   if (ui->radioBtnAscend->isChecked())
      tabModel->setSort(index,Qt::AscendingOrder);
   else
      tabModel->setSort(index,Qt::DescendingOrder);
   tabModel->select();
}
void MainWindow::on_radioBtnAscend_clicked()
{//升序
   tabModel->setSort(ui->comboFields->currentIndex(),Qt::AscendingOrder);
   tabModel->select();
}
void MainWindow::on_radioBtnDescend_clicked()
{//降序
   tabModel->setSort(ui->comboFields->currentIndex(),Qt::DescendingOrder);
   tabModel->select();
}

在调用setSort()函数设置排序规则后,需要调用QSqlTableModel::select()重新读取数据表的数据才会使排序规则生效。

11.2.9 记录过滤

QSqlTableModel的setFilter()函数设置记录过滤条件,实际上就是设置了SQL语句里的WHERE字句。

实例里演示针对Gender字段设置字段过滤条件,窗口界面上“数据过滤”分组框里有3个RadioButton,分别为“男”“女”“全显示”,3个按钮的clicked()信号的槽函数实现代码如下:

void MainWindow::on_radioBtnMan_clicked()
{
   tabModel->setFilter(" Gender='男' ");
}
void MainWindow::on_radioBtnWoman_clicked()
{
   tabModel->setFilter(" Gender='女' ");
}
void MainWindow::on_radioBtnBoth_clicked()
{
   tabModel->setFilter("");
}

调用setFilter()后无需调用select()函数就可以立即刷新记录,若要取消过滤条件,只需在setFilter()函数里传递一个空字符串。

11.3 QSqlQueryModel的使用

11.3.1 QSqlQueryModel功能概述

从原书的图11-2的SQL数据模型类的继承关系看,QSqlQueryModel是QSqlTableModel的父类。QSqlQueryModel封装了执行SELECT语句从数据库查询数据的功能,但是QSqlQueryModel只能作为只读数据源使用,不可以编辑数据。

QSqlQueryModel类的主要接口函数见表11-10(省略了函数中的const关键字和缺省参数)。

表11-10 QSqlQueryModel类的主要函数

接口函数功能描述
void clear()清除数据模型,释放所有获得的数据
QSqlError lastError()返回上次的错误,可获取错误的类型和文本信息
QSqlQuery query()返回当前关联的QSqlQuery对象
void setQuery(QSqlQuery &query)设置一个QSqlQuery对象,获取数据
void setQuery(QString &query)设置一个SELECT语句创建查询,获取数据
QSqlRecord record()返回一个空记录,包含当前查询的字段信息
QSqlRecord record(int row)返回行号为row的记录
int rowCount()返回查询到的记录条数
int columnCount()返回查询的字段个数
void setHeaderData(int section, Qt::Orientation orientation, QVariant &value)设置表头数据,一般用于设置字段的表头标题

使用QSqlQueryModel作为数据模型从数据库里查询数据,只需使用setQuery()函数设置一个SELECT查询语句即可。QSqlQueryModel可以作为QTableView等视图组件的数据源,也可以使用QDataWidgetMapper创建字段与界面组件的映射,只是查询出来的数据是不可编辑的。

11.3.2 使用QSqlQueryModel实现数据查询

1.实例功能

使用QSqlQueryModel可以从一个数据表或多个数据表里查询数据,只需设计好SELECT语句即可。实例samp11_2使用QSqlQueryModel从employee表里查询记录,并在界面上显示,运行窗口见原书的图11-5。

窗口工具栏上几个工具栏按钮只有打开数据库和记录移动功能,记录移动通过调用QDataWidgetMapper类的记录移动功能实现。窗口上没有数据编辑和保存功能,因为QSqlQueryModel查询出来的数据是只读的。

主窗口类的定义部分如下(去除了界面组件自动生成的槽函数的定义):

class MainWindow : public QMainWindow
{
   Q_OBJECT
private:
   QSqlDatabase  DB; //数据库
   QSqlQueryModel  *qryModel; //数据模型
   QItemSelectionModel  *theSelection; //选择模型
   QDataWidgetMapper   *dataMapper;//数据界面映射
   void   openTable();//打开数据表
   void   refreshTableView();//移动记录时刷新TableView的当前行
public:
   explicit MainWindow(QWidget *parent = 0);
   ~MainWindow();
private slots:
//selectionModel的行发生了变化
    void on_currentRowChanged(const QModelIndex &current, const QModelIndex &previous);
};

这里定义了QSqlQueryModel类型的数据模型变量qryModel,也定义了数据库、选择模型和数据界面映射的变量。refreshTableView()函数用于记录移动后刷新tableView上的当前行位置。

自定义槽函数on_currentRowChanged()在选择模型的当前行变化时,处理Photo字段的查询与照片显示。

2.打开数据库

工具栏上的“打开数据库”按钮对应actOpenDB,选择SQLite数据库文件,然后调用openTable()函数打开数据库。槽函数on_actOpenDB_triggered()的代码与实例samp11_1完全一样,这里不再列出。

openTable()用于查询数据,建立界面显示等具体操作,其代码如下:

void MainWindow::openTable()
{//打开数据表
   qryModel=new QSqlQueryModel(this);
   qryModel->setQuery("SELECT EmpNo, Name, Gender, Height, Birthday, Mobile, Province, City, Department , Education, Salary FROM employee ORDER BY EmpNo");
   if (qryModel->lastError().isValid())
   {
      QMessageBox::critical(this, "错误", 
            "数据表查询错误,错误信息\n"+qryModel->lastError().text(),
            QMessageBox::Ok,QMessageBox::NoButton);
      return;
   }
   qryModel->setHeaderData(0,Qt::Horizontal,"工号");
   qryModel->setHeaderData(1,Qt::Horizontal,"姓名");
   qryModel->setHeaderData(2,Qt::Horizontal,"性别");
   qryModel->setHeaderData(3,Qt::Horizontal,"身高");
   qryModel->setHeaderData(4,Qt::Horizontal,"出生日期");
   qryModel->setHeaderData(5,Qt::Horizontal,"手机");
   qryModel->setHeaderData(6,Qt::Horizontal,"省份");
   qryModel->setHeaderData(7,Qt::Horizontal,"城市");
   qryModel->setHeaderData(8,Qt::Horizontal,"部门");
   qryModel->setHeaderData(9,Qt::Horizontal,"学历");
   qryModel->setHeaderData(10,Qt::Horizontal,"工资");

   theSelection=new QItemSelectionModel(qryModel); 
connect(theSelection,SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
             this,SLOT(on_currentRowChanged(QModelIndex,QModelIndex)));
   ui->tableView->setModel(qryModel);
   ui->tableView->setSelectionModel(theSelection);
//创建数据映射
   dataMapper= new QDataWidgetMapper();
   dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
   dataMapper->setModel(qryModel);
   dataMapper->addMapping(ui->dbSpinEmpNo,0);//"EmpNo";
   dataMapper->addMapping(ui->dbEditName,1);//"Name";
   dataMapper->addMapping(ui->dbComboSex,2);//"Gender";
   dataMapper->addMapping(ui->dbSpinHeight,3);//"Height";
   dataMapper->addMapping(ui->dbEditBirth,4);//"Birthday";
   dataMapper->addMapping(ui->dbEditMobile,5);//"Mobile";
   dataMapper->addMapping(ui->dbComboProvince,6);//"Province";
   dataMapper->addMapping(ui->dbEditCity,7);//"City";
   dataMapper->addMapping(ui->dbComboDep,8);//"Department";
   dataMapper->addMapping(ui->dbComboEdu,9);//"Education";
   dataMapper->addMapping(ui->dbSpinSalary,10);//"Salary";
   dataMapper->toFirst();
   ui->actOpenDB->setEnabled(false);
}

程序首先创建了QSqlQueryModel类型的私有变量qryModel,然后调用setQuery()函数设置了SELECT查询语句,SELECT语句从employee表里查询除了Memo和Photo之外的所有其他字段。

使用setHeaderData()函数为每个字段设置显示标题,为使代码简化,这里直接使用了字段的序号。

为qryModel创建了选择模型theSelection,并且将其currentRowChanged()信号与自定义槽函数on_currentRowChanged()关联起来。这个自定义槽函数用来在记录移动时,查询出Memo和Photo字段的内容,并在界面上显示出来。

下面是槽函数on_currentRowChanged()的代码:

void MainWindow::on_currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
{
   Q_UNUSED(previous);
   if (!current.isValid())
   {  ui->dbLabPhoto->clear();
      return;
   }
   dataMapper->setCurrentModelIndex(current); //更新数据映射的行号
   bool first=(current.row()==0); //是否首记录
   bool last=(current.row()==qryModel->rowCount()-1);//是否尾记录
   ui->actRecFirst->setEnabled(!first); //更新使能状态
   ui->actRecPrevious->setEnabled(!first);
   ui->actRecNext->setEnabled(!last);
   ui->actRecLast->setEnabled(!last);

   int curRecNo=theSelection->currentIndex().row();
   QSqlRecord  curRec=qryModel->record(curRecNo); //获取当前记录
   int empNo=curRec.value("EmpNo").toInt();
   QSqlQuery query; //查询当前EmpNo的Memo和Photo字段的数据
   query.prepare("select EmpNo, Memo, Photo from employee where EmpNo = :ID");
   query.bindValue(":ID",empNo);
   query.exec();
   query.first();

   QVariant   va=query.value("Photo");
   if (!va.isValid())  //图片字段内容为空
     ui->dbLabPhoto->clear();
   else
   {//显示照片
     QByteArray data=va.toByteArray();
     QPixmap pic;
     pic.loadFromData(data);
     ui->dbLabPhoto->setPixmap(pic.scaledToWidth( 
              ui->dbLabPhoto->size().width()));
}
   QVariant   va2=query.value("Memo");//显示备注
   ui->dbEditMemo->setPlainText(va2.toString());
}

这个函数实现3个功能,第1个功能是更新数据映射的行号,即:

dataMapper->setCurrentModelIndex(current);

使窗口上的字段关联的显示组件刷新显示当前记录的内容。

第2个功能是根据当前行号,判断是否是首记录或尾记录,以此更新界面上4个记录移动的Action的使能状态。

第3个功能是获取当前记录的EmpNo字段的值(即员工编号),然后用一个QSqlQuery变量query执行查询语句,只查询出这个员工的Memo和Photo字段的数据,然后在界面元件上显示。这里使用了QSqlQuery类,它用来执行任意的SQL语句。

在QSqlQueryModel类的变量qryModel里设置SELECT语句时,并没有查询所有字段,因为Photo是BLOB字段,全部查询出来后必然占用较大内存,而且在做记录遍历时,如果存在BLOB字段数据,执行速度会很慢。所以,这个实例里将普通字段的查询用QSqlQueryModel来查询并显示,而Memo和Photo字段数据的查询采用按需查询的方式,这样可以减少内存消耗,提高记录遍历时的执行速度。

openTable()函数剩余的部分是设置tableView的数据模型和选择模型,然后创建数据界面映射变量dataMapper,设置各个界面组件与字段的映射关系。

3.记录移动

用于数据映射的QDataWidgetMapper类设置数据模型后,总是指向数据模型的当前记录。QDataWidgetMapper有4个函数进行当前记录的移动,分别是toFirst()、toLast()、toNext()和toPrevious()。当前记录移动时,会引起数据模型关联的选择模型发射currentRowChanged()信号,也就会执行关联的自定义槽函数on_currentRowChanged()。

工具栏上有4个记录移动的按钮,它们调用QDataWidgetMapper的记录移动函数实现记录移动,4个Action的槽函数代码如下:

void MainWindow::on_actRecFirst_triggered()
{ //首记录
   dataMapper->toFirst();
   refreshTableView();
}
void MainWindow::on_actRecPrevious_triggered()
{ //前一条记录
   dataMapper->toPrevious();
   refreshTableView();
}
void MainWindow::on_actRecNext_triggered()
{//后一条记录
   dataMapper->toNext();
   refreshTableView();
}
void MainWindow::on_actRecLast_triggered()
{//最后一条记录
   dataMapper->toLast();
   refreshTableView();
}

使用QDataWidgetMapper的记录移动操作后,QDataWidgetMapper会移动到新的记录上,映射了字段的界面组件也会自动显示新记录的字段的数据。但是,tableView的当前行并不会自动变化,所以需要调用refreshTableView()函数刷新tableView的显示,refreshTableView()函数的代码如下:

void MainWindow::refreshTableView()
{//刷新tableView的当前选择行
   int index=dataMapper->currentIndex();
   QModelIndex curIndex=qryModel->index(index,1);
   theSelection->clearSelection();//清空选择项
   theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select); 
}

11.4 QSqlQuery的使用

11.4.1 QSqlQuery基本用法

QSqlQuery是能执行任意SQL语句的类,如SELECT、INSERT、UPDATE、DELETE等,QSqlQuery类的一些常用函数见表11-11(省略函数中的const关键字,省略缺省参数,不同参数的同名函数一般只给出一种参数形式)。

表11-11 QSqlQuery类的主要函数

接口函数功能描述
bool prepare(QString &query)设置准备执行的SQL语句,一般用于带参数的SQL语句
void bindValue(QString &placeholder, QVariant &val)设置SQL语句中参数的值,以占位符表示参数
void exec()执行由prepare()和bindValue()设置的SQL语句
void exec(QString &query)直接执行一个不带参数的SQL语句
bool isActive()如果成功执行了exec()函数,就返回true
bool isSelect()如果执行的SQL语句是SELECT语句,就返回true
QSqlRecord record()返回当前记录
QVariant value(QString &name)返回当前记录名称为name的字段的值
bool isNull(QString &name)判断一个字段是否为空,当query非活动、未定位在有效记录、无此字段或字段为空时都返回true
int size()对于SELECT语句,返回查询到的记录条数,其他语句返回-1
int numRowsAffected()返回SQL语句影响的记录条数,对于SELECT语句无定义
bool first()定位到第一条记录,isActive和isSelect都为true时才有效
bool previous()定位到上一条记录,isActive和isSelect都为true时才有效
bool next()定位到下一条记录,isActive和isSelect都为true时才有效
bool last()定位到最后一条记录,isActive和isSelect都为true时才有效
bool seek(int index)定位到指定序号的记录
int at()返回当前记录的序号

使用QSqlQuery执行不带参数的SQL语句时可以用exec(QString)函数,如:

QSqlQuery query;
query.exec("SELECT * FROM employee");
query.exec("UPDATE employee SET Salary=3000 where Gender='女' ");

使用带参数的SQL语句时,先用prepare()函数准备SQL语句,然后用bindValue()函数设置参数值,再用exec()执行SQL语句,如:

QSqlQuery query;
query.prepare ("SELECT * FROM employee where EmpNo=:ID");
query.bindValue(":ID", 2003);
query.exec();

上面是SQL语句中的参数用“冒号+参数名”表示的形式,还可以直接用占位符来表示参数,如:

QSqlQuery query;
query.prepare ("UPDATE employee SET Name=?, Gender=?, Height=? where EmpNo=?");
query.bindValue(0, "高某某");
query.bindValue(1, "男");
query.bindValue(2, 1.78);
query.bindValue(3,2010);
query.exec();

11.4.2 QSqlQueryModel和QSqlQuery联合使用

1.数据表显示

QSqlQueryModel可以查询数据并作为数据模型,实现数据的显示,QSqlQuery可以执行UPDATE、INSERT、DELETE等SQL语句实现数据的编辑修改。

实例samp11_3通过联合使用QSqlQueryModel和QSqlQuery组件实现数据表的显示和编辑修改,原书的图11-6是实例samp11_3运行主窗口。

主窗口类定义如下(去掉了一些自动生成的部分):

class MainWindow : public QMainWindow
{   Q_OBJECT
private:
   QSqlDatabase  DB; //数据库
   QSqlQueryModel  *qryModel; //数据库模型
   QItemSelectionModel  *theSelection; //选择模型
   void   openTable();//打开数据表
   void   updateRecord(int recNo); //更新记录
public:
   explicit MainWindow(QWidget *parent = 0);
};

这个主窗口类的定义相比于实例samp11_2只是增加了一个updateRecord()函数。工具栏上的“打开数据库”按钮的代码与实例samp11_2完全相同,会调用openTable()连接数据库并查询数据表的数据。

本实例的openTable()函数的代码如下:

void MainWindow::openTable()
{//打开数据表
   qryModel=new QSqlQueryModel(this);
   theSelection=new QItemSelectionModel(qryModel);
   qryModel->setQuery("SELECT EmpNo, Name, Gender, Height, Birthday, Mobile, Province, City, Department, Education, Salary FROM employee order by EmpNo"); 
   if (qryModel->lastError().isValid())
   {
      QMessageBox::information(this, "错误", "数据表查询错误,错误信息\n" 
   +qryModel->lastError().text(), QMessageBox::Ok,QMessageBox::NoButton);
      return;
   }
   qryModel->setHeaderData(0,Qt::Horizontal,"工号");
   qryModel->setHeaderData(1,Qt::Horizontal,"姓名");
   qryModel->setHeaderData(2,Qt::Horizontal,"性别");
   qryModel->setHeaderData(3,Qt::Horizontal,"身高");
   qryModel->setHeaderData(4,Qt::Horizontal,"出生日期");
   qryModel->setHeaderData(5,Qt::Horizontal,"手机");
   qryModel->setHeaderData(6,Qt::Horizontal,"省份");
   qryModel->setHeaderData(7,Qt::Horizontal,"城市");
   qryModel->setHeaderData(8,Qt::Horizontal,"部门");
   qryModel->setHeaderData(9,Qt::Horizontal,"学历");
   qryModel->setHeaderData(10,Qt::Horizontal,"工资");

   ui->tableView->setModel(qryModel);
   ui->tableView->setSelectionModel(theSelection);
   ui->actOpenDB->setEnabled(false);
   ui->actRecInsert->setEnabled(true);
   ui->actRecDelete->setEnabled(true);
   ui->actRecEdit->setEnabled(true);
   ui->actScan->setEnabled(true);
}

openTable()函数创建了QSqlQueryModel类对象qryModel,从数据表employee里查询出除了Memo和Photo之外的其他字段,并作为界面上的tableView的数据模型。还创建了选择模型theSelection,但是没有为选择模型的currentRowChanged()信号关联槽函数,因为不需要在记录移动时做什么处理。由于使用QSqlQueryModel作为tableView的数据源,在tableView里是无法编辑修改数据的。

2.编辑记录对话框

由于在tableView上无法编辑修改数据,只是作为一个只读的数据显示,在主窗口工具栏上提供了“插入记录”“编辑记录”“删除记录”3个按钮对数据进行编辑。“插入记录”和“编辑记录”都会打开一个对话框,编辑一条记录的所有字段数据,确认插入后用QSqlQuery执行一条INSERT语句插入一条记录,确认编辑时用QSqlQuery执行一个UPDATE语句更新一条记录。

设计了一个对话框WDialogData,用于编辑一条记录的所有字段的数据,在“插入记录”和“编辑记录”时调用此对话框,对话框运行界面如原书的图11-7所示。

WDialogData类的定义如下:

class WDialogData : public QDialog
{
   Q_OBJECT
private:
   QSqlRecord  mRecord; //保存一条记录的数据
public:
   explicit WDialogData(QWidget *parent = 0);
   ~WDialogData();
   void   setUpdateRecord(QSqlRecord &recData); //更新记录
   void   setInsertRecord(QSqlRecord &recData); //插入记录
   QSqlRecord  getRecordData();//获取录入的数据
private slots:
   void on_btnClearPhoto_clicked(); //清理照片
   void on_btnSetPhoto_clicked(); //设置照片
private:
   Ui::WDialogData *ui;
};

QSqlRecord类型的私有变量mRecord用于存储一条记录的数据。

插入一条记录时,创建对话框后调用setInsertRecord()函数初始化对话框的数据;编辑一条记录时,创建对话框后调用setUpdateRecord ()函数初始化对话框的数据。

对话框确认修改后,调用getRecordData()时,界面数据存入mRecord,并将mRecord作为返回值,返回编辑后的一条记录的数据。

对话框WDialogData的所有自定义函数,以及对话框上的“导入照片”“清除照片”按钮的代码如下,程序代码比较简单,这里不再过多解释。

void WDialogData::setUpdateRecord(QSqlRecord &recData)
{ //编辑记录,更新记录数据到界面
   mRecord=recData;
   ui->spinEmpNo->setEnabled(false); //员工编号不允许编辑
   setWindowTitle("更新记录");
//根据recData的数据更新界面显示
   ui->spinEmpNo->setValue(recData.value("EmpNo").toInt());
   ui->editName->setText(recData.value("Name").toString());
   ui->comboSex->setCurrentText(recData.value("Gender").toString());
   ui->spinHeight->setValue(recData.value("Height").toFloat());
   ui->editBirth->setDate(recData.value("Birthday").toDate());
   ui->editMobile->setText(recData.value("Mobile").toString());
  ui->comboProvince->setCurrentText(recData.value("Province").toString());
   ui->editCity->setText(recData.value("City").toString());
   ui->comboDep->setCurrentText(recData.value("Department").toString());
   ui->comboEdu->setCurrentText(recData.value("Education").toString());
   ui->spinSalary->setValue(recData.value("Salary").toInt());
   ui->editMemo->setPlainText(recData.value("Memo").toString());
   QVariant   va=recData.value("Photo");
   if (!va.isValid())  //图片字段内容为空
      ui->LabPhoto->clear();
   else
   {
      QByteArray data=va.toByteArray();
      QPixmap pic;
      pic.loadFromData(data);
      ui->LabPhoto->setPixmap(pic.scaledToWidth(
                  ui->LabPhoto->size().width()));
   }
}
void WDialogData::setInsertRecord(QSqlRecord &recData)
{//插入记录,无需更新界面显示,但是要存储recData的字段结构
   mRecord=recData; //保存recData到内部变量
   ui->spinEmpNo->setEnabled(true); //插入的记录,员工编号允许编辑
   setWindowTitle("插入新记录");
   ui->spinEmpNo->setValue(recData.value("EmpNo").toInt());
}
QSqlRecord WDialogData::getRecordData()
{ //"确定"按钮后,界面数据保存到记录mRecord
   mRecord.setValue("EmpNo",ui->spinEmpNo->value());
   mRecord.setValue("Name",ui->editName->text());
   mRecord.setValue("Gender",ui->comboSex->currentText());
   mRecord.setValue("Height",ui->spinHeight->value());
   mRecord.setValue("Birthday",ui->editBirth->date());
   mRecord.setValue("Mobile",ui->editMobile->text());
   mRecord.setValue("Province",ui->comboProvince->currentText());
   mRecord.setValue("City",ui->editCity->text());
   mRecord.setValue("Department",ui->comboDep->currentText());
   mRecord.setValue("Education",ui->comboEdu->currentText());
   mRecord.setValue("Salary",ui->spinSalary->value());
   mRecord.setValue("Memo",ui->editMemo->toPlainText());
//照片编辑时已经修改了mRecord的Photo字段的值
   return  mRecord; //以记录作为返回值
}

void WDialogData::on_btnClearPhoto_clicked()
{ //清除照片
   ui->LabPhoto->clear();
   mRecord.setNull("Photo"); //Photo字段清空
}

void WDialogData::on_btnSetPhoto_clicked()
{//设置照片
   QString aFile=QFileDialog::getOpenFileName(this,"选择图片", "",  "照片(*.jpg)");
   if (aFile.isEmpty())
      return;
   QByteArray data;
   QFile* file=new QFile(aFile); 
   file->open(QIODevice::ReadOnly);
   data = file->readAll();
   file->close();
   mRecord.setValue("Photo",data); //图片保存到Photo字段
   QPixmap pic;
   pic.loadFromData(data);
 ui->LabPhoto->setPixmap(pic.scaledToWidth(ui->LabPhoto->size().width()));
}
3.编辑记录

单击主窗口工具栏上的“编辑记录”按钮,或在tableView上双击某条记录,会编辑当前记录,代码如下:

void MainWindow::on_actRecEdit_triggered()
{//编辑当前记录
   int curRecNo=theSelection->currentIndex().row(); 
   updateRecord(curRecNo);
}
void MainWindow::on_tableView_doubleClicked(const QModelIndex &index)
{ //tableView上双击,编辑当前记录
   int curRecNo=index.row();
   updateRecord(curRecNo);
}

两个槽函数都会调用updateRecord()函数,并且以记录的序号作为传递参数。updateRecord()函数实现当前记录的编辑,代码如下:

void MainWindow::updateRecord(int recNo)
{ //更新一条记录
   QSqlRecord  curRec=qryModel->record(recNo); //获取当前记录
   int empNo=curRec.value("EmpNo").toInt();//获取EmpNo
   QSqlQuery query; //查询出当前记录的所有字段
   query.prepare("select * from employee where EmpNo = :ID");
   query.bindValue(":ID",empNo);
   query.exec();
   query.first();
   if (!query.isValid()) //是否为有效记录
      return;

   curRec=query.record();
   WDialogData   *dataDialog=new WDialogData(this);
   Qt::WindowFlags   flags=dataDialog->windowFlags();
   dataDialog->setWindowFlags(flags | Qt::MSWindowsFixedSizeDialogHint); 
   dataDialog->setUpdateRecord(curRec);//调用对话框函数更新数据和界面
   int ret=dataDialog->exec();// 以模态方式显示对话框
   if (ret==QDialog::Accepted) //OK键被按下
   {
    QSqlRecord  recData=dataDialog->getRecordData(); //获得对话框返回的记录
    query.prepare("update employee set Name=:Name, Gender=:Gender,"
" Height=:Height, Birthday=:Birthday, Mobile=:Mobile, Province=:Province,"
" City=:City,Department=:Department, Education=:Education, Salary=:Salary,"
" Memo=:Memo, Photo=:Photo  where EmpNo = :ID");
     query.bindValue(":Name",recData.value("Name"));
     query.bindValue(":Gender",recData.value("Gender"));
     query.bindValue(":Height",recData.value("Height"));
     query.bindValue(":Birthday",recData.value("Birthday"));
     query.bindValue(":Mobile",recData.value("Mobile"));
     query.bindValue(":Province",recData.value("Province"));
     query.bindValue(":City",recData.value("City"));
     query.bindValue(":Department",recData.value("Department"));
     query.bindValue(":Education",recData.value("Education"));
     query.bindValue(":Salary",recData.value("Salary"));
     query.bindValue(":Memo",recData.value("Memo"));
     query.bindValue(":Photo",recData.value("Photo"));
     query.bindValue(":ID",empNo);
     if (!query.exec())
         QMessageBox::critical(this, "错误",
                  "记录更新错误\n"+query.lastError().text(),
                  QMessageBox::Ok,QMessageBox::NoButton);
     else//重新执行SQL语句查询
         qryModel->query().exec();
   }
   delete dataDialog;
}

函数updateRecord(int recNo)根据行号recNo从qryModel获取当前记录的EmpNo字段的值,即员工编号,然后使用一个QSqlQuery对象从数据表里查询出这一个员工的所有字段的一条记录。使用QsqlQuery时用到了参数SQL语句:

query.prepare("select * from employee where EmpNo = :ID");
query.bindValue(":ID",empNo);
query.exec();

由于EmpNo是数据表employee的主键字段,不允许出现重复,所以只会查询出一条记录,查询出的一条完整记录保存到变量curRec。

然后创建WDialogData类型的对话框dataDialog,调用setUpdateRecord(curRec)将完整记录传递给对话框。对话框执行后,如果是“确定”返回,则通过getRecordData()函数获取对话框编辑后的记录数据:

QSqlRecord  recData=dataDialog->getRecordData();

recData里包含了编辑后的最新数据,然后使用QSqlQuery对象执行带参数的UPDATE语句更新一条记录。更新成功后,将数据模型qryModel的SELECT 语句重新执行一次,可刷新tableView的显示。

4.插入记录

工具栏上的“插入记录”可以插入一条新记录,代码如下:

void MainWindow::on_actRecInsert_triggered()
{//插入记录
   QSqlQuery query;
   query.exec("select * from employee where EmpNo =-1"); //只查询字段信息
   QSqlRecord curRec=query.record();//获取当前记录,实际为空记录
   curRec.setValue("EmpNo",qryModel->rowCount()+3000);

   WDialogData   *dataDialog=new WDialogData(this);
   Qt::WindowFlags   flags=dataDialog->windowFlags();
   dataDialog->setWindowFlags(flags | Qt::MSWindowsFixedSizeDialogHint); 
   dataDialog->setInsertRecord(curRec); //插入记录
   int ret=dataDialog->exec();// 以模态方式显示对话框
   if (ret==QDialog::Accepted) //OK键被按下
   {
     QSqlRecord  recData=dataDialog->getRecordData();
  query.prepare("INSERT INTO employee (EmpNo,Name,Gender, Height,Birthday, "
"Mobile,Province, City,Department,Education,Salary,Memo,Photo) "
" VALUES(:EmpNo,:Name, :Gender,:Height,:Birthday, :Mobile, :Province,"
  " :City, :Department, :Education,:Salary,:Memo,:Photo)");
     query.bindValue(":EmpNo",recData.value("EmpNo"));
     query.bindValue(":Name",recData.value("Name"));
     query.bindValue(":Gender",recData.value("Gender"));
     query.bindValue(":Height",recData.value("Height"));
     query.bindValue(":Birthday",recData.value("Birthday"));
     query.bindValue(":Mobile",recData.value("Mobile"));
     query.bindValue(":Province",recData.value("Province"));
     query.bindValue(":City",recData.value("City"));
     query.bindValue(":Department",recData.value("Department"));
     query.bindValue(":Education",recData.value("Education"));
     query.bindValue(":Salary",recData.value("Salary"));
     query.bindValue(":Memo",recData.value("Memo"));
     query.bindValue(":Photo",recData.value("Photo"));
     if (!query.exec())
         QMessageBox::critical(this, "错误",
                   "插入记录错误\n"+query.lastError().text(),
                   QMessageBox::Ok,QMessageBox::NoButton);
     else //重新执行SQL语句查询
         qryModel->query().exec();
   }
   delete dataDialog;
}

程序首先用QSqlQuery类对象query执行一个SQL语句“select * from employee where EmpNo =-1”,这样不会查询到任何记录,目的就是得到一条空记录curRec。

创建WDialogData类型的对话框dataDialog后,调用setInsertRecord()函数对话框进行初始化设置。

对话框dataDialog运行“确认”返回后,使用query执行INSERT语句插入一条新记录。若插入记录执行成功,需要重新执行数据模型qryModel的SQL语句查询数据,才会更新界面上的tableView的显示。

5.删除记录

工具栏上的“删除记录”删除tableView上的当前记录,代码如下:

void MainWindow::on_actRecDelete_triggered()
{//删除当前记录
   int curRecNo=theSelection->currentIndex().row();
   QSqlRecord  curRec=qryModel->record(curRecNo); //获取当前记录
   if (curRec.isEmpty()) //当前为空记录
      return;

   int empNo=curRec.value("EmpNo").toInt();//获取员工编号
   QSqlQuery query;
   query.prepare("delete  from employee where EmpNo = :ID");
   query.bindValue(":ID",empNo);
   if (!query.exec())
      QMessageBox::critical(this, "错误", 
            "删除记录出现错误\n"+query.lastError().text(),
             QMessageBox::Ok,QMessageBox::NoButton);
   else //重新执行SQL语句查询
      qryModel->query().exec();
}

从数据模型的当前记录中获取员工编号,然后使用一个QSqlQuery类的对象执行一条DELETE语句删除这条记录。

删除记录后需要重新设置数据模型qryModel的SQL语句并查询数据,以更新数据和tableView的显示。

6.记录遍历

工具栏上的“涨工资”按钮通过记录遍历,修改所有记录的Salary字段的值,其实现代码如下:

void MainWindow::on_actScan_triggered()
{//涨工资,记录遍历
   QSqlQuery qryEmpList; 
   qryEmpList.exec("SELECT EmpNo,Salary FROM employee ORDER BY EmpNo");
   qryEmpList.first();
   QSqlQuery qryUpdate;                                   //临时 QSqlQuery
   qryUpdate.prepare("UPDATE employee SET Salary=:Salary WHERE EmpNo = :ID");

   while (qryEmpList.isValid())                           //当前记录有效
   {
      int   empID=qryEmpList.value("EmpNo").toInt();      //获取EmpNo
      float  salary=qryEmpList.value("Salary").toFloat(); //获取Salary
      salary=salary+1000;                                 //涨工资
      qryUpdate.bindValue(":ID",empID);
      qryUpdate.bindValue(":Salary",salary);              //设置SQL语句参数
      qryUpdate.exec(); //执行update
      if (!qryEmpList.next()) //移动到下一条记录,并判断是否到末尾了
         break;
   }
   qryModel->query().exec();//重新查询数据,更新tableView的显示
   QMessageBox::information(this, "提示", "涨工资计算完毕",
                      QMessageBox::Ok,QMessageBox::NoButton);
}

程序里使用了两个QSqlQuery类变量,qryEmpList查询EmpNo和Salary两个字段的全部记录,qryUpdate用于执行一个带参数的UPDATE语句,每次更新一条记录。

遍历qryEmpList的所有记录,QSqlQuery有first()、previous()、next()、last()等函数记录移动,若到了最后一条记录后再执行next(),将返回false,以此判断是否遍历完所有记录。

11.5 QSqlRelationalTableModel的使用

11.5.1 关系型数据表和实例功能

从原书的图11-2的类继承关系图上看,QSqlRelationalTableModel是QSqlTableModel的子类。QSqlRelationalTableModel可以处理关系数据表,所谓关系数据表,是指将主表里的某个字段存储为代码型字段,而代码的具体意义在另外一个数据表里。

表11-5给出了studInfo的字段定义,studInfo数据表实际存储的数据示例见表11-12,字段departID和majorID存储的是学院代码和专业代码。

表11-12 表studInfo的数据记录示例

序号studIDnamegenderdepartIDmajorID
12017200211张三202003
22017200102李四101002
32017300212小明505001
42017400102小雅505002

学院代码字段departID的具体意义在数据表departments里定义,departments表的字段结构见表11-3,departments表的示例数据见表11-13。

表11-13 表departments的数据记录示例

序号departIDdepartment
110生物科学学院
220数理学院
350化工学院

同样,专业数据表majors定义了专业代码及其专业名称,majors表的字段结构见表11-4,majors表的示例数据见表11-14。majors数据表不仅定义了专业代码的意义,还使用了学院代码字段departID,与departments数据表发生关联。

表11-14 表majors的数据记录示例

序号majorIDmajordepartID
11001生物遗传学10
21002生物工程10
35001计算机网络50
45002自动化50

在数据库设计中使用代码字段和代码表的好处;一是可以减少数据表的存储量,一个大的数据表存储代码远比存储具体文字用的存储空间少;二是代码表示的文字可能会被修改,例如学院的名称可能会修改,这时只需修改代码表里一条记录而已。

QSqlRelationalTableModel类专门用来编辑这种具有代码字段的数据表,可以很方便地将代码字段与关系数据表建立关系,在显示和编辑数据表时,直接使用关系数据表的代码意义字段的内容。实例samp11_4演示关系型数据表使用的方法,原书的图11-8是程序运行界面。

实例samp11_4使用QSqlRelationalTableModel作为tableView的数据源,显示和编辑studInfo数据表。学院和专业两个字段与代码表建立了关系,tableView中直接显示这两个字段的文字内容,编辑时有一个下拉列表框,列表框里是代码表里全部代码意义文字。

11.5.2 关系型数据模型功能实现

1.主窗口类定义

主窗口类定义如下,定义了一个QSqlRelationalTableModel类型的变量tabModel作为数据模型。

class MainWindow : public QMainWindow
{
   Q_OBJECT
private:
   QSqlDatabase  DB;                   //数据库连接
   QSqlRelationalTableModel  *tabModel;//数据模型
   QItemSelectionModel *theSelection;  //选择模型
   void   openTable();                 //打开数据表
public:
   explicit MainWindow(QWidget *parent = 0);
   ~MainWindow();
private slots:
  void on_currentChanged(const QModelIndex &current, const QModelIndex &previous);
};
2.打开数据表

主窗口工具栏的“打开”按钮选择数据库文件demodb.db3,然后调用openTable()打开数据表,openTable()函数的代码如下:

void MainWindow::openTable()
{//打开数据表
   tabModel=new QSqlRelationalTableModel(this,DB);
   tabModel->setTable("studInfo"); //设置数据表
   tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit);  
   tabModel->setSort(0,Qt::AscendingOrder);
   tabModel->setHeaderData(0,Qt::Horizontal,"学号");
   tabModel->setHeaderData(1,Qt::Horizontal,"姓名");
   tabModel->setHeaderData(2,Qt::Horizontal,"性别");
   tabModel->setHeaderData(3,Qt::Horizontal,"学院");
   tabModel->setHeaderData(4,Qt::Horizontal,"专业");
//设置代码字段的查询关系数据表
   tabModel->setRelation(3, 
        QSqlRelation("departments","departID","department")); //学院
   tabModel->setRelation(4,
        QSqlRelation("majors","majorID","major"));//专业

   theSelection=new QItemSelectionModel(tabModel);
   connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),
         this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
   ui->tableView->setModel(tabModel);
   ui->tableView->setSelectionModel(theSelection);
//为关系型字段设置缺省代理组件
   ui->tableView->setItemDelegate(
                    new QSqlRelationalDelegate(ui->tableView)); 
   tabModel->select(); //打开数据表
   ui->actOpenDB->setEnabled(false);
   ui->actRecAppend->setEnabled(true);
   ui->actRecInsert->setEnabled(true);
   ui->actRecDelete->setEnabled(true);
   ui->actFields->setEnabled(true);
}

QSqlRelationalTableModel类的主要函数与QSqlTableModel相同,有一个新的函数setRelation()用于设置代码字段的关联数据表和关联字段。setRelation()函数的定义如下:

void setRelation(int column, const QSqlRelation &relation)

其中column是主表中代码字段的序号,relation是QSqlRelation类型的表示关联数据表的关系。设置代码字段departID的关系的代码如下:

tabModel->setRelation(3,
             QSqlRelation("departments","departID","department")); 

第1个参数(数字3)是departID字段在studInfo表中的字段序号。

第2个参数用QSqlRelation(“departments”,“departID”,“department”)创建了一个QSqlRelation类型对象,其中,参数"departments"是代码表departments,参数"departID"是代码字段名称,参数"department"是代码意义字段名称。

openTable()函数中还有关键的一行:

ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));

这是在tableView中为代码字段创建缺省的关系型代理组件,这样在tableView中编辑代码字段的内容时,才会出现一个下拉列表框,列出代码表的所有可选内容。

3.实际字段列表

使用QSqlRelationalTableModel类设置代码字段的关系后,在tableView中以代码意义显示代码字段的内容,其实际字段有没有变化呢?

工具栏上的按钮“字段列表”列出了tabModel的所有字段的名称,其代码如下:

void MainWindow::on_actFields_triggered()
{//获取字段列表
   QSqlRecord  emptyRec=tabModel->record();//获取空记录,只有字段名
   QString  str;
   for (int i=0;i<emptyRec.count();i++)
      str=str+emptyRec.fieldName(i)+'\n';
   QMessageBox::information(this, "所有字段名", str, 
                QMessageBox::Ok,QMessageBox::NoButton);
}

字段列表的对话框如原书的图11-9所示,可以看到两个代码字段departID和majorID被代码表中的代码意义字段department和major替换了。但是在界面上修改数据后,数据能以代码的形式保存到原数据表里。

4.其他功能的实现

工具栏上其他“添加”“插入”“删除”“保存”“取消”等按钮的功能的实现与实例samp11_1相同,自定义槽函数on_currentChanged()的实现也与samp11_1相同,这些功能的实现就不具体介绍了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值