使用窗体编辑记录
我们将看到如何创建一次只显示一条记录的对话窗体。这个对话框可以用于增加、编辑、删除单独的记录,也可以遍历表中所有的记录。
我们将通过Staff Manager应用程序来阐明这些概念。该应用程序记录了雇员所属的部门、部门所处的位置以及诸如雇员内部电话分机号等一些基本信息。应用程序使用了如下三个表:
CREATE TABLE location(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VACHAR(40) NOT NULL));
CREATE TABLE location(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VACHAR(40) NOT NULL,
locationid INTEGER NOT NULL,
FOREIGN KEY (locationid) REFERENCES location));
CREATE TABLE location(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VACHAR(40) NOT NULL,
departmentid INTEGER NOT NULL,
extension INTEGER NOT NULL,
email VARCHAR(40) NOT NULL,
FOREIGN KEY (departmentid) REFERENCES department));
表以及表之间的关系如图,在每一地点可以有许多部门,而每一部门可以拥有许多雇员。指定外键的语法主要针对SQLite3,但可能会随数据库的不同而有所变化。
本节将重点关注编辑雇员信息用的对话框EmployeeForm。下一节讨论的是MainForm,它提供 了部门和雇员的主从关系视图。
EmployeeForm类提供了一个从主窗体雇员概要信息到某一雇员的具体细节信息的下钻型视图。当调用该类时,如果给出了有效的雇员ID,窗体将显示指定的雇员信息;否则显示第一个雇员的信息(窗体如图133所示)。用户可以浏览查看所有扉员的信息,编辑或者删除现有扉员的信息同时还可以添加新雇员的信息。
employeeform.h文件中已经提供了如下的enum枚举类型变量,为表的列索引号给出有具体含义的名称:
enum {
Employee_Id = 0,
Employee_Name = 1,
Employee_DepartmentId = 2,
Employee_Extension = 3,
Employee_Email = 4,
Employee_StartDate = 5
};
头文件的其余部分则定义EmployeeForm类:
class EmployeeForm : public QDialog
{
Q_OBJECT
public:
EmployeeForm(int id, QWidget *parent = 0);
void done(int result);
private slots:
void addEmployee();
void deleteEmployee();
private:
QSqlRelationalTableModel *tableModel;
QDataWidgetMapper *mapper;
QLabel *nameLabel;
...
QDialogButtonBox *buttonBox;
};
为了访问数据库我们使用QSqlRelationalTableModel而不是普通的 QSqlTableModel因为需要析表之间的外键关系。QSqlRelationalTableModel是允许将某一窗体中的窗口部件映射到数据模型中对应的列的类。
窗体的构造函数非常长,所以将其分为几个部分,逐段进行分析。至于没有太多实质性用途的版面布局代码,我们就不去分析了。
EmployeeForm::EmployeeForm(int id, QWidget *parent)
: QDialog(parent)
{
nameEdit = new QLineEdit;
nameLabel = new QLabel(tr("Na&me:"));
nameLabel->setBuddy(nameEdit);
departmentComboBox = new QComboBox;
departmentLabel = new QLabel(tr("Depar&tment:"));
departmentLabel->setBuddy(departmentComboBox);
extensionLineEdit = new QLineEdit;
extensionLineEdit->setValidator(new QIntValidator(0, 99999, this));
extensionLabel = new QLabel(tr("E&xtension:"));
extensionLabel->setBuddy(extensionLineEdit);
emailEdit = new QLineEdit;
emailLabel = new QLabel(tr("&Email:"));
emailLabel->setBuddy(emailEdit);
startDateEdit = new QDateEdit;
startDateEdit->setCalendarPopup(true);
QDate today = QDate::currentDate();
startDateEdit->setDateRange(today.addDays(-90), today.addDays(90));
startDateLabel = new QLabel(tr("&Start Date:"));
startDateLabel->setBuddy(startDateEdit);
首先为每个字段创建一个可编辑的窗口都件。还创建了对应的标签,放在每个可编辑的窗口部件旁边,用以识别相应的字段。
我们使用 QlntValidator 确保Extension 行编辑器仅接受有效的分机号。这样的话,所有的号码都将在 0-99999 的范围之内。我们也为 Start Date 编辑器设置了数值范围,同时设置编辑器以提供一个弹出式的日历。我们并不直接组装组合框,稍后将给它一个能自我组装的模型。
firstButton = new QPushButton(tr("<< &First"));
previousButton = new QPushButton(tr("< &Previous"));
nextButton = new QPushButton(tr("&Next >"));
lastButton = new QPushButton(tr("&Last >>"));
addButton = new QPushButton(tr("&Add"));
deleteButton = new QPushButton(tr("&Delete"));
closeButton = new QPushButton(tr("&Close"));
buttonBox = new QDialogButtonBox;
buttonBox->addButton(addButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(deleteButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(closeButton, QDialogButtonBox::AcceptRole);
我们创建浏览导航按钮(<<First 、<Previous、Next> 和Last>>),这些按钮都集中在对话框的顶部。然后,创建其他的按钮(诸如 Add、Delete和Close,并将它们放在位于对话框底部的 QDialogButtonBox 中。创建版面布局的代码很简单,这里不再赘述。
这样,我们就完成了组成用户接口界面的窗口部件,因此现在可以将注意刀转移到如何实现其内在的功能上。
tableModel = new QSqlRelationalTableModel(this);
tableModel->setTable("employee");
tableModel->setRelation(Employee_DepartmentId,
QSqlRelation("department", "id", "name"));
tableModel->setSort(Employee_Name, Qt::AscendingOrder);
tableModel->select();
QSqlTableModel *relationModel =
tableModel->relationModel(Employee_DepartmentId);
departmentComboBox->setModel(relationModel);
departmentComboBox->setModelColumn(
relationModel->fieldIndex("name"));
模型的构建与设置过程都与之前介绍的 QSqlTableModel 的设立过程基本一致。但是这次使用QSqlRelationalTableModel 并设立一个外键关联。 setRelation()函数获得一个外键宇段索引及 QSqlRelation 索引。 QSqIRelation 构造函数则用表名(外键关系对应的表)、外键字段名以及要显示的字段名来表示外键字段值。
QComboBox与ListWidget 很相似,因为它有一个内部模型去保存它的数据条目项。我们可以用自己建的模型代替那个模型,这里需要做的,就是给出QSqlRelationalTableModel 使用的关系模型。这个关系模型有两列,所以必须指出组合框应该显示的是哪一列。当调用setRelation()时,就建立关系模型,所以我们不知道 name 列的标题索引。由于这个原因,我们使用fieldIndex() 函数与字段名得到正确的标题索引,以使组合框显示部门名。由于 QSqlRelationalTableModel的使用,组合框将显示部门名而不是部门的 ID 号。
mapper = new QDataWidgetMapper(this);
mapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
mapper->setModel(tableModel);
mapper->setItemDelegate(new QSqlRelationalDelegate(this));
mapper->addMapping(nameEdit, Employee_Name);
mapper->addMapping(departmentComboBox, Employee_DepartmentId);
mapper->addMapping(extensionLineEdit, Employee_Extension);
mapper->addMapping(emailEdit, Employee_Email);
mapper->addMapping(startDateEdit, Employee_StartDate);
QDataWidgeMapper将一个数据库记录字段反映到其映射的窗口部件中,同时将窗口部件中所做出的更改反映回数据库。我们既可以自己负责提交这些更改,也可以让映射器自动完成这个工作,这里选择自动选项(QDataWidgetMapper::AutoSubmit)。
让模型有效工作的映射器必须给定,对于含有外键的模型,还需要给它一个QSqlRelationalDelegate委托基类。这个委托基类可以确保用户看到的是QSqlRelation显示栏的值,而不是原始的 ID号,同时它也保证在用户开始编辑时组合框会显示值而映射器则实际上将相应的索引值(外键)写回到数据库中。
对于外键引用约束含有大量记录的表的情况,最好创建自己的委托基类并与搜索性能一起使用以显示"列表值"窗体,而不是依靠QSglRelationalTableModel的默认组合框。
一旦模型和委托基类都设置好,就可在窗体的窗口部件及其相应的字段索引间添加映射关系。组合框则被当作其他的一般窗口部件来处理因为所有处理外键的工作都将用已经设置好的关系模型来操作。
if (id != -1) {
for (int row = 0; row < tableModel->rowCount(); ++row) {
QSqlRecord record = tableModel->record(row);
if (record.value(Employee_Id).toInt() == id) {
mapper->setCurrentIndex(row);
break;
}
}
} else {
mapper->toFirst();
}
如果对话框通过一个有效的雇员ID号而调用,我们就利用这个ID查找这个雇员的记录,并让它成为映射器的当前记录。否则,我们只浏览到第一个雇员记录。或者,在另一种情况下,记录的数据将被反映到其映射的窗口部件中 。
connect(firstButton, SIGNAL(clicked()), mapper, SLOT(toFirst()));
connect(previousButton, SIGNAL(clicked()),
mapper, SLOT(toPrevious()));
connect(nextButton, SIGNAL(clicked()), mapper, SLOT(toNext()));
connect(lastButton, SIGNAL(clicked()), mapper, SLOT(toLast()));
connect(addButton, SIGNAL(clicked()), this, SLOT(addEmployee()));
connect(deleteButton, SIGNAL(clicked()),
this, SLOT(deleteEmployee()));
connect(closeButton, SIGNAL(clicked()), this, SLOT(accept()));
导航浏览按钮都是直接连接到相应的映射器槽的。(如果使用于动提交的策略,则需要实现自定义的这些槽,在这些槽中提交当前记录,然后执行导航浏览以避免所做的修改丢失。)数据窗口部件映射器:允许进行编辑和导航浏览。若要增加和删除记录,可使用底层的模型。
void EmployeeForm::addEmployee()
{
int row = mapper->currentIndex();
mapper->submit();
tableModel->insertRow(row);
mapper->setCurrentIndex(row);
nameEdit->clear();
extensionLineEdit->clear();
startDateEdit->setDate(QDate::currentDate());
nameEdit->setFocus();
}
当用户单击 Add 按钮时,就会调用 addEmployee()槽。我们首先重新找回当前行,因为它在提交后就遗失了。然后调用 submit() 以确保对当前记录的修改没有任何遗失。尽管已经设置了自动提交策略 QDataWidgetMapper::AutoSubmit,仍必须手动提交。这是因为自动提交仅在用户改变光标焦点位置时才能使用——这样可以避免在每次用户插入或者删除一个字符时,频繁地对数据库进行更新操作——因为用户有可能刷编辑完一个字段,但当单击 Add 按钮时光标焦点位置并没有跳出该字段。接下来,我们插入一个新的空白行并让映射器导航浏览至该空白行。最后,初始化窗口部件,并将光标聚焦到用户开始键入的第一个就绪的窗口部件。
void EmployeeForm::deleteEmployee()
{
int row = mapper->currentIndex();
tableModel->removeRow(row);
mapper->submit();
mapper->setCurrentIndex(qMin(row, tableModel->rowCount() - 1));
}
对于删除操作,应从指明当前行开始,然后删除这一行并向数据库提交这个更改。必须手动提交删除的更改,因为自动提交策略仅适用于对记录做出的更改。最后,让映射器当前索引指向被删除行的下一行。如果删除的是最后一行,则指向最后一行。
QDataWidgetMapper 类使那些采用数据模型显示信息的数据可知型窗体的开发变得容易得多。在这个例子中,我们使用一个QSqlRelationalTableModel 作为底层的数据模型,而 QDataWidgetMapper 则可以与任何数据模型一起使用,包括非 SQL 模型。另外一个可供选择的方式是直接使用 QSqlQuery在窗体中填写数据,并更新数据库。这个方法要求更多的工作量,但也相应地更灵活一些。
employeeform.h
#ifndef EMPLOYEEFORM_H
#define EMPLOYEEFORM_H
#include <QDialog>
class QComboBox;
class QDataWidgetMapper;
class QDateEdit;
class QDialogButtonBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QSqlRelationalTableModel;
enum {
Employee_Id = 0,
Employee_Name = 1,
Employee_DepartmentId = 2,
Employee_Extension = 3,
Employee_Email = 4,
Employee_StartDate = 5
};
class EmployeeForm : public QDialog
{
Q_OBJECT
public:
EmployeeForm(int id, QWidget *parent = 0);
void done(int result);
private slots:
void addEmployee();
void deleteEmployee();
private:
QSqlRelationalTableModel *tableModel;
QDataWidgetMapper *mapper;
QLabel *nameLabel;
QLabel *departmentLabel;
QLabel *extensionLabel;
QLabel *emailLabel;
QLabel *startDateLabel;
QLineEdit *nameEdit;
QComboBox *departmentComboBox;
QLineEdit *extensionLineEdit;
QLineEdit *emailEdit;
QDateEdit *startDateEdit;
QPushButton *firstButton;
QPushButton *previousButton;
QPushButton *nextButton;
QPushButton *lastButton;
QPushButton *addButton;
QPushButton *deleteButton;
QPushButton *closeButton;
QDialogButtonBox *buttonBox;
};
#endif
employeeform.cpp
#include <QtGui>
#include <QtSql>
#include "employeeform.h"
EmployeeForm::EmployeeForm(int id, QWidget *parent)
: QDialog(parent)
{
nameEdit = new QLineEdit;
nameLabel = new QLabel(tr("Na&me:"));
nameLabel->setBuddy(nameEdit);
departmentComboBox = new QComboBox;
departmentLabel = new QLabel(tr("Depar&tment:"));
departmentLabel->setBuddy(departmentComboBox);
extensionLineEdit = new QLineEdit;
extensionLineEdit->setValidator(new QIntValidator(0, 99999, this));
extensionLabel = new QLabel(tr("E&xtension:"));
extensionLabel->setBuddy(extensionLineEdit);
emailEdit = new QLineEdit;
emailLabel = new QLabel(tr("&Email:"));
emailLabel->setBuddy(emailEdit);
startDateEdit = new QDateEdit;
startDateEdit->setCalendarPopup(true);
QDate today = QDate::currentDate();
startDateEdit->setDateRange(today.addDays(-90), today.addDays(90));
startDateLabel = new QLabel(tr("&Start Date:"));
startDateLabel->setBuddy(startDateEdit);
firstButton = new QPushButton(tr("<< &First"));
previousButton = new QPushButton(tr("< &Previous"));
nextButton = new QPushButton(tr("&Next >"));
lastButton = new QPushButton(tr("&Last >>"));
addButton = new QPushButton(tr("&Add"));
deleteButton = new QPushButton(tr("&Delete"));
closeButton = new QPushButton(tr("&Close"));
buttonBox = new QDialogButtonBox;
buttonBox->addButton(addButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(deleteButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(closeButton, QDialogButtonBox::AcceptRole);
tableModel = new QSqlRelationalTableModel(this);
tableModel->setTable("employee");
tableModel->setRelation(Employee_DepartmentId,
QSqlRelation("department", "id", "name"));
tableModel->setSort(Employee_Name, Qt::AscendingOrder);
tableModel->select();
QSqlTableModel *relationModel =
tableModel->relationModel(Employee_DepartmentId);
departmentComboBox->setModel(relationModel);
departmentComboBox->setModelColumn(
relationModel->fieldIndex("name"));
mapper = new QDataWidgetMapper(this);
mapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
mapper->setModel(tableModel);
mapper->setItemDelegate(new QSqlRelationalDelegate(this));
mapper->addMapping(nameEdit, Employee_Name);
mapper->addMapping(departmentComboBox, Employee_DepartmentId);
mapper->addMapping(extensionLineEdit, Employee_Extension);
mapper->addMapping(emailEdit, Employee_Email);
mapper->addMapping(startDateEdit, Employee_StartDate);
if (id != -1) {
for (int row = 0; row < tableModel->rowCount(); ++row) {
QSqlRecord record = tableModel->record(row);
if (record.value(Employee_Id).toInt() == id) {
mapper->setCurrentIndex(row);
break;
}
}
} else {
mapper->toFirst();
}
connect(firstButton, SIGNAL(clicked()), mapper, SLOT(toFirst()));
connect(previousButton, SIGNAL(clicked()),
mapper, SLOT(toPrevious()));
connect(nextButton, SIGNAL(clicked()), mapper, SLOT(toNext()));
connect(lastButton, SIGNAL(clicked()), mapper, SLOT(toLast()));
connect(addButton, SIGNAL(clicked()), this, SLOT(addEmployee()));
connect(deleteButton, SIGNAL(clicked()),
this, SLOT(deleteEmployee()));
connect(closeButton, SIGNAL(clicked()), this, SLOT(accept()));
QHBoxLayout *topButtonLayout = new QHBoxLayout;
topButtonLayout->setContentsMargins(20, 0, 20, 5);
topButtonLayout->addStretch();
topButtonLayout->addWidget(firstButton);
topButtonLayout->addWidget(previousButton);
topButtonLayout->addWidget(nextButton);
topButtonLayout->addWidget(lastButton);
topButtonLayout->addStretch();
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addLayout(topButtonLayout, 0, 0, 1, 3);
mainLayout->addWidget(nameLabel, 1, 0);
mainLayout->addWidget(nameEdit, 1, 1, 1, 2);
mainLayout->addWidget(departmentLabel, 2, 0);
mainLayout->addWidget(departmentComboBox, 2, 1, 1, 2);
mainLayout->addWidget(extensionLabel, 3, 0);
mainLayout->addWidget(extensionLineEdit, 3, 1);
mainLayout->addWidget(emailLabel, 4, 0);
mainLayout->addWidget(emailEdit, 4, 1, 1, 2);
mainLayout->addWidget(startDateLabel, 5, 0);
mainLayout->addWidget(startDateEdit, 5, 1);
mainLayout->addWidget(buttonBox, 7, 0, 1, 3);
mainLayout->setRowMinimumHeight(6, 10);
mainLayout->setRowStretch(6, 1);
mainLayout->setColumnStretch(2, 1);
setLayout(mainLayout);
if (id == -1) {
nextButton->setFocus();
} else {
nameEdit->setFocus();
}
setWindowTitle(tr("Edit Employees"));
}
void EmployeeForm::done(int result)
{
mapper->submit();
QDialog::done(result);
}
void EmployeeForm::addEmployee()
{
int row = mapper->currentIndex();
mapper->submit();
tableModel->insertRow(row);
mapper->setCurrentIndex(row);
nameEdit->clear();
extensionLineEdit->clear();
startDateEdit->setDate(QDate::currentDate());
nameEdit->setFocus();
}
void EmployeeForm::deleteEmployee()
{
int row = mapper->currentIndex();
tableModel->removeRow(row);
mapper->submit();
mapper->setCurrentIndex(qMin(row, tableModel->rowCount() - 1));
}