参考书目
《C++ GUI Qt4编程》
一、Qt框架概述
1. 什么是Qt?
Qt是一个用C++编写的、成熟的、跨平台的GUI工具包,它是挪威Trolltech公司的产品,该公司为商业市场开发、销售和支持Qt及其相关软件。
Qt作为一个出色的软件框架,除了为应用程序提供GUI库,还提供了容器、输入输出、数据库、多线程、网络、三维图形等开发工具,十分强大。
Qt除了价格高昂的商用版本,还对自由软件社区提供了免费开源版本。
2. Qt的优点
-
跨平台
Qt的第一大优点,也是最令人印象深刻的特点就是它的跨平台性。
Qt支持Linux、类UNIX系统、Windows、Mac OS、甚至嵌入式平台。
Qt对于每一种操作系统平台,都提供了对应的底层类库,也就是在OS层和应用层之间增加了一个“平台适应层”,而对应用程序提供出来的接口时一致的。所以同样一套代码,不管放在Linux还是Windows,几乎不用改动就可以通过编译,此特性大大方便了软件的移植。 -
信号槽机制
信号与信号处理时GUI应用程序用来相应用户输入的主要机制,也是一个GUI库的核心特征。
Qt的信号处理机制由 信号 和 槽 构成。使用信号槽功能的类需要在类声明中添加 Q_OBJECT 宏声明,然后使用 QObject::connect() 函数进行连接,它明确地将信号和信号处理函数联系了起来。
-
Qt对象树
Qt对象之间是可以存在父子关系的。父对象会保存一个子对象链表,记录所有指向其子对象的指针,同时子对象也会保留指向父对象的指针。当一个父对象被析构时,其子对象也会被析构。
所以销毁一个对象,它的所有子孙后代对象都会被销毁,表现在Qt对象树上就是删除一个节点,那这个节点下的整个分支都会被删除。这种特性让开发人员在一个对象下new出若干子对象后,不需要手动去delete,只需要销毁一个父对象,它们都会被自动销毁,大大减轻开发人员的负担。
使用此特性时仍需注意的问题: 对象树和析构函数冲突 -
在线帮助系统
由于Qt提供的封住类和接口十分丰富,Qt Creator 在文本编辑器中提供了在线帮助系统,只需要选中要查询的内容,按下F1,即可弹出相应的帮助页面,极大地减少了查询和学习的时间。
二、Qt体系
1. Qt模块图
2. Qt继承树
3. Qt学习路线图
三、Qt编程练习
Spreadsheet 制作一个电子表格
项目总览
-
项目介绍
该项目会创建一个电子表格,功能类似Excel表格。
有基本的单元格编辑,复制,剪切,删除等功能。
有批量复制,搜索,排序等进阶功能。
有保存文件,打开文件,展示最近文件的功能。 -
所有文件
总文件个数17,总代码量约1800行
-
主窗口类 (继承于QMainWindow类)
mainwindow.h
mainwindow.cpp
主窗口类负责管理整体的布局,生成顶部工具栏以及快捷按钮,并且包含了提供核心功能的电子表格。主窗口还负责文件的保存与读取,以及提供第二级功能弹窗。
-
表格类 (继承于QTableWidget类)
spreadsheet.h
spreadsheet.cpp
表格类只生成由单元格组成的空白表格,有CCell单元格类作为私有成员。表格类的主要意义是接收输入(键盘、鼠标)并显示内容,以及提供所有跟表格内容有关的操作,比如拷贝、粘贴和剪切单元格,完成公式的计算,打开文件后的表格显示,从表格中读取数据,搜索数据等等…
-
单元格类 (继承于QTableWidgetItem,能很好地与表格类配合)
cell.h
cell.cpp
单元格类是最小的单元,所有数据最终都要通过它来显示和返回,它主要提供公式设置和解析的功能。
-
搜索对话框类 (继承于QDialog类)
sortdialog.h
sortdialog.cpp
在主窗口的Edit菜单点击Find搜索选项,或者按下快捷栏的Find按钮,都会弹出搜索弹窗。该窗口除了提供基本的搜索功能外,还增加了两个细微的操作来增加搜索的精确性:反向搜索和大小写感。搜索时勾选该选项(CheckBox)即可。只有当输入框中内容满足一定条件,Find按钮才会被激活,这里用到了Qt提供的正则表达式。
-
跳转对话框类 (继承于QDialog类)
gotocelldialog.ui
ui_gotocelldialog.h
gotocelldialog.cpp
该对话框的作用很简单,跳转到用户指定的单元格(比如A21、H55)。
(创建此对话框时用到了Qt Designer,因此生成了对应的ui文件)
-
排序对话框类 (继承于QDialog类)
sortdialog.ui
ui_sortdialog.h
sortdialog.cpp
该对话框提供对选中区域的排序功能,所有被选中的列号都会被加载进Column选项,你可以任选一列进行排序,按下more按钮,允许你再选择两列进行排序,这里对more键的toggle信号和后两个设置框的visible属性作了信号槽连接。
(创建此对话框时用到了Qt Designer,因此生成了对应的ui文件)
关键代码分析
- 主窗口功能 - 另存为SaveAs
以下是SaveAs操作会调用到的三个函数
在主函数中,SaveAs作为槽函数,在SaveAsAction被触发后调用。此函数调用 QFileDialog类 提供的 getSaveFileName 函数,会弹出一个已封装好的文件保存对话框,可以在参数列表中设置标题、保存类型。
保存后会返回文件名filename,为了之后交给spreasheet以保存当前数据内容。所以在return处调用saveFile私有函数
bool MainWindow::saveAs()
{
QString filename = QFileDialog::
getSaveFileName(
this,
tr("Save Spreadsheet"),
tr("Spreadsheet files (*.sp)"));
if (filename.isEmpty()) {
return false;
}
return saveFile(filename);
}
saveFile函数把filename交给spreadsheet,让其把表格中的数据写入,若写入成功,则设置为当前文件并给出提示。
bool MainWindow::saveFile(const QString& filename)
{
if (!spreadSheet->writeFile(filename)) {
statusBar()->showMessage(tr("Saving canceled"), 2000);
return false;
}
setCurrentFile(filename);
statusBar()->showMessage(tr("File saved"), 2000);
return true;
}
在CSpreadSheet类中完成数据写入操作。先用文件名构造一个 QFile 对象,然后调用open打开,若文件打开成功,则以此创建一个 QDataStream 数据流对象。最开始写入的MAGIC_NUMBER用于标识.sp文件,在读取数据的时候需要通过判断文件头来确定是否是sp文件。接着,通过遍历表格,将有公式的行号、列号和公式写入数据流。在此期间,将鼠标设为等待状态图形。
bool CSpreadSheet::writeFile(const QString& filename)
{
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("cannot write file %1:\n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream out(&file);
out.setVersion(QDataStream::Qt_5_12);
out << quint32(MAGIC_NUMBER);
QApplication::setOverrideCursor(Qt::WaitCursor);
for (int row = 0; row < rowCount(); ++row) {
for (int col = 0; col < columnCount(); ++col) {
QString str = formula(row, col);
if (!str.isEmpty()) {
out << quint16(row) << quint16(col) << str;
}
}
}
QApplication::restoreOverrideCursor();
return true;
}
- 主窗口功能 - 创建Qt行为 - 创建菜单
Spreadsheet应用中每个基本“操作”都对应一个 QAction 对象,它们都是主窗口类的私有成员,在以下函数中被初始化,并且建立了信号槽连接,这让每个按键都能发挥自己的功能,而非摆设。由于要初始化的操作过多,这里省略了大部分代码。
void MainWindow::createActions()
{
// new
newAction = new QAction(tr("&New"), this);
newAction->setShortcut(QKeySequence::New);
newAction->setStatusTip(tr("Create a new spread sheet file"));
connect(newAction, SIGNAL(triggered(bool)),
this, SLOT(newFile()));
// recent
for (int i = 0; i < MAX_RECENT_FILES; ++i) {
recentFileActions[i] = new QAction(this);
recentFileActions[i]->setVisible(false);
connect(recentFileActions[i], SIGNAL(triggered(bool)),
this, SLOT(openRecentFile()));
}
// save
saveAction = new QAction("&Save", this);
saveAction->setStatusTip(tr("save current file"));
connect(saveAction, SIGNAL(triggered(bool)),
this, SLOT(save()));
// cut
cutAction = new QAction("&Cut", this);
cutAction->setStatusTip(tr("cut"));
connect(cutAction, SIGNAL(triggered(bool)),
spreadSheet, SLOT(cut()));
...
// sort
sortAction = new QAction("&Sort", this);
connect(sortAction, SIGNAL(triggered(bool)),
this, SLOT(sort()));
}
建立好所有“动作”之后,需要将它们加入到菜单栏中。menuBar() 函数由 QMainWindow 类提供,它返回一个指向 QMenuBar,通过调用 addMenu 函数创建一个菜单,然后在这个菜单中依次添加属于它的基本操作,比如对于文件菜单,则添加“新文件”、“打开文件”、“保存文件”、“另存为”、“最近文件”和“退出”的操作。
void MainWindow::createMenus()
{
// filemenu
fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(newAction);
fileMenu->addAction(openAction);
fileMenu->addAction(saveAction);
fileMenu->addAction(saveasAction);
separatorAction = fileMenu->addSeparator(); // 间隔器,分隔开不同选项
for (int i = 0; i < MAX_RECENT_FILES; ++i) {
fileMenu->addAction(recentFileActions[i]);
}
fileMenu->addSeparator();
fileMenu->addAction(exitAction);
// editmenu
editMenu = menuBar()->addMenu(tr("&Edit"));
editMenu->addAction(cutAction);
editMenu->addAction(copyAction);
editMenu->addAction(pasteAction);
editMenu->addAction(deleteAction);
selectSubMenu = editMenu->addMenu(tr("&Select"));
selectSubMenu->addAction(selectRowAction);
selectSubMenu->addAction(selectColumnAction);
selectSubMenu->addAction(selectAllAction);
editMenu->addSeparator();
editMenu->addAction(findAction);
editMenu->addAction(goToCellAction);
// tools menu
toolsMenu = menuBar()->addMenu(tr("&Tools"));
toolsMenu->addAction(recalculateAction);
toolsMenu->addAction(sortAction);
//options menu
optionsMenu = menuBar()->addMenu(tr("&Options"));
optionsMenu->addAction(showGridAction);
optionsMenu->addAction(autoRecalcAction);
menuBar()->addSeparator();
//help menu
helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(aboutAction);
helpMenu->addAction(aboutQtAction);
}
添加完毕后,窗口的菜单栏就形成了:
- 主窗口功能 - 搜索Find
此find函数在主窗口类中作为槽函数,在点击Find按键后被触发。find函数new一个findDialog对话框,并把它的两个查找按键发出的信号和spreadsheet表格对应的执行槽连接起来。最后调用 show() 、raise() 和 activateWindow() 使查找对话框显示,位于最上层并且激活。其实只调用show()就可以完成,但是考虑到可能findDialog已经显示出来,那么show()不会作任何事情,此时就需要调用后两个函数。
void MainWindow::find()
{
if (!findDialog) {
findDialog = new CFindDialog(this);
connect(findDialog, SIGNAL(sigFindNext(QString,Qt::CaseSensitivity)),
spreadSheet, SLOT(findNext(QString,Qt::CaseSensitivity)));
connect(findDialog, SIGNAL(sigFindPrevious(QString,Qt::CaseSensitivity)),
spreadSheet, SLOT(findPrevious(QString,Qt::CaseSensitivity)));
}
findDialog->show();
findDialog->raise();
findDialog->activateWindow();
}
findDialog的两个信号(向前查找和向后查找)在 findClicked 函数中被发出,而该函数作为槽函数,与findDialog中的Find按键相连接。在该函数中,通过是否勾选了backwardCheckBox来判断该发送前向查找信号还是后向查找信号。
void CFindDialog::findClicked()
{
QString qstr = lineEdit->text();
Qt::CaseSensitivity cs = caseCheckBox->isChecked() ? Qt::CaseSensitive :
Qt::CaseInsensitive;
if (backwardCheckBox->isChecked()) {
emit sigFindPrevious(qstr, cs);
}
else {
emit sigFindNext(qstr, cs);
}
}
在向后查找函数中,遍历从当前行列开始之后的单元格,若找到符合条件的单元格,则将焦点设置到该单元格,并且激活主窗口,否则继续查找。
void CSpreadSheet::findNext(const QString& str, Qt::CaseSensitivity cs)
{
int curRow = currentRow();
int curCol = currentColumn() + 1;
while (curRow < ROW_COUNT) {
while (curCol < COLUMN_COUNT) {
if (text(curRow, curCol).contains(str, cs)) {
clearSelection();
setCurrentCell(curRow, curCol);
activateWindow();
return;
}
++curCol;
}
curCol = 0;
++curRow;
}
QApplication::beep();
}
- 主窗口功能 - 跳转GoToCell
跳转功能在主窗口的槽函数gotoCell中实现。和findDialog的生成方式一样,goToCellDialog也是通过new一个对象创建,由于CGoToCellDialog类继承于 QWidget类,可直接调用执行函数 exec(),对话框弹出。如果对话框中执行 accept(),则exec()返回非零值,调用spreadsheet的设置当前单元格功能进行跳转,若对话框中执行 reject(),则跳过。
void MainWindow::goToCell()
{
CGoToCellDialog* dialog = new CGoToCellDialog(this);
if (dialog->exec()) {
QString str = dialog->lineEdit->text().toUpper();
spreadSheet->setCurrentCell(str.mid(1).toInt()-1,
str[0].unicode()-'A');
}
delete dialog;
}
在goToCell对话框中,建立按键和对话框行为的信号槽以及输入栏编辑和对应改变的信号槽。
由于电子表格的坐标有固定格式(行号由阿拉伯数字表示,列号由英文字母表示),这里由正则表达式类 QRegExp 来对lineEdit创建一个格式验证器,通过 hasAcceptableInput 的返回值来设置okButton是否可用。
void CGoToCellDialog::connectSlots()
{
connect(lineEdit, SIGNAL(textChanged(const QString&)),
this, SLOT(onLineEditChanged()));
connect(okButton, SIGNAL(clicked(bool)),
this, SLOT(accept()));
connect(cancelButton, SIGNAL(clicked(bool)),
this, SLOT(reject()));
}
void CGoToCellDialog::setDefault()
{
okButton->setEnabled(false);
QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");
lineEdit->setValidator(new QRegExpValidator(regExp, this));
}
void CGoToCellDialog::onLineEditChanged()
{
okButton->setEnabled(lineEdit->hasAcceptableInput());
}
- 表格功能 - 复制Copy 粘贴Paste 剪切Cut
电子表格最基本的复制粘贴功能。
copy函数实现复制功能,它将选中单元格中的内容复制到剪贴板。其做法是对表格进行二维遍历,在每一行数据中,用 “\t” 制表符来区分列,用 “\n” 换行符来区分行。每个单元格的公式都被返回并记录在 QString 中,最后将该字符串交给剪贴板 clipboard(),该函数由 QApplication 类提供。
void CSpreadSheet::copy()
{
QTableWidgetSelectionRange range = selectedRange();
QString str;
// get selected formulas within range
for (int i = 0; i < range.rowCount(); ++i) {
if (i > 0) {
str += "\n";
}
for (int j = 0; j < range.columnCount(); ++j) {
if (j > 0) {
str += "\t";
}
str += formula(range.topRow() + i, range.leftColumn() + j);
}
}
QApplication::clipboard()->setText(str);
}
拷贝的过程就是把剪贴板保存的数据写到对应单元格中。
paste函数把数据用 QString 提供的 split 函数解析并保存到 QStringList 中,然后判断所选区域与之前复制的数据区域是否一致,如果一致则进行下一步,否则报错返回。在粘贴时,同样进行二维遍历,调用 setFormula 函数对所选范围内的每个单元格进行赋值。
void CSpreadSheet::paste()
{
QString str = QApplication::clipboard()->text();
QStringList rows = str.split('\n');
int iRow = rows.count();
int iCol = rows.first().count('\t') + 1;
QTableWidgetSelectionRange range = selectedRange();
if ((range.rowCount() * range.columnCount() != 1) &&
((range.rowCount() != iRow) || (range.columnCount() != iCol))) {
QMessageBox::information(this, tr("Spreadsheet"),
tr("paste failed. copy and paste area are not the same"));
return;
}
for (int i = 0; i < iRow; ++i) {
QStringList cols = rows[i].split('\t');
for (int j = 0; j < iCol; ++j) {
int row = range.topRow() + i;
int col = range.leftColumn() + j;
if (row < ROW_COUNT && col < COLUMN_COUNT) {
setFormula(row, col, cols[j]);
}
}
}
somethingChanged();
}
剪切功能可以通过先拷贝,再删除实现。删除功能较简单,分析略过。
void CSpreadSheet::cut()
{
copy();
del();
}
- 表格功能 - 读写文件数据
主窗口的打开文件和保存文件的底层操作都是由CSpreadSheet表格类提供的读写数据函数实现的,之前分析SaveAs功能时提到了writeFile函数在写入数据流时先写入MAGIC_NUMBER用于标识sp文件,那么在readFile函数中则先检查输入数据流首部是否等于MAGIC_NUMBER,若是则继续,否则提示错误并返回。
bool CSpreadSheet::readFile(const QString& filename)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("cannot read file %1:\n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream in(&file);
quint32 magic;
in >> magic;
if (magic != MAGIC_NUMBER) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("The file is not a spreadsheet file!"));
return false;
}
clear();
quint16 row;
quint16 col;
QString str;
QApplication::setOverrideCursor(Qt::WaitCursor);
while (!in.atEnd()) {
in >> row >> col >> str;
setFormula(row, col, str);
}
QApplication::restoreOverrideCursor();
return true;
}
bool CSpreadSheet::writeFile(const QString& filename)
{
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("cannot write file %1:\n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream out(&file);
out.setVersion(QDataStream::Qt_5_12);
out << quint32(MAGIC_NUMBER);
QApplication::setOverrideCursor(Qt::WaitCursor);
for (int row = 0; row < rowCount(); ++row) {
for (int col = 0; col < columnCount(); ++col) {
QString str = formula(row, col);
if (!str.isEmpty()) {
out << quint16(row) << quint16(col) << str;
}
}
}
QApplication::restoreOverrideCursor();
return true;
}
源码已上传至百度云盘,欢迎下载
链接:https://pan.baidu.com/s/1-t4M52_sOqSjpglqmE9WAg
提取码:gfmm