1. 前言&资源链接
大家好我是程序员Akgry,最近天气是越来越冷了,大家要注意防寒。阿克这几天学得可谓是神魂颠倒,Qt框架这个技术远比阿克想得更加有趣,虽然天气寒冷,但是也挡不住阿克前进的热情。那么今天阿克给大家带来一款宝藏级项目,文档办公软件AkeWord,它的目标是对标WPS和Office Word,虽然开发出来之后功能有一定的差距,但是麻雀虽小五脏俱全,使用起来还是非常舒服的,那么接下来就跟着阿克看一下项目的设计思路吧。(PS: 阿克给代码仓库的项目划分了等级排序是 入门级—普通级—精品级—宝藏级—传说级—神话级,该博客目前是阿克的第一篇宝藏级项目博客)
工具:QtCreator5.9.6
源码链接:GitHub代码仓库,项目名AkeWord
素材包:蓝奏云图片素材整合包
2. 设计思路&开发日志
想要设计一款功能健全的Qt软件,首先要在软件开发之前规划好要做的事情和步骤,如果在开发中边写代码边做调整,就会导致精力的分散。关于规划的方法和工具,专业人士比如架构师采用的是 设计模式+UML+项目调优 的方法,但阿克明显不是架构师,没有任何设计项目架构的经验,关于软件设计方面的知识阿克也只从本科课堂里积榨过一点点,还没来得及消化就老死不相往来了,相信不少读者都跟阿克有同样的感觉。那么我们作为一个开发者该如何解决这些难题呢,我的恩师曾指导过我,编程上的任何过渡问题,都可以从需求上入手,通过逐步深入即可直达底层。抱着这个思想,我们先想一下一块文档办公软件用户拿到手里应该是什么样的,这其实不难想象,因为我们已经有了很好的模版,成熟的WPS和Office Word。
如果你熟练Qt5设计模式里QMainWindow类的话你就会发现,纵使成熟的Office Word,也摆脱不了Windows用户对于习惯的束缚,换句话说无论多么高级成熟的软件,本质都离不开菜单栏和工具栏这两个核心部件,可能有读者看到这里脑中直接就浮现出开发流程了,就是在设计界面完成组件的设计工作,然后依次实现对应的槽方法和成员方法和子窗口方法呗。没错,阿克正是采取的这种方式进行开发的,不过在实际开发过程中遭遇了不小的情况,出现过各种各样的接口选择等问题。比如工具栏的图片素材怎么来,看似很容易解决的问题当你实际操作时会发现资源非常难找,阿克当时就找了很长时间的素材,因为阿克不是美术生也不会画这些图标,最终还是在游戏素材网上找到的哥布林像素包......,当然这都不是主要的开发阻碍,真正需要我们认真攻克的永远是技术上的难题。那么废话不多说了,接下来就跟着阿克一起看一下项目的具体实现吧。
3. 界面设计
如上一节内容所说,阿克先在设计界面对软件整体的UI进行开发和类名完善等工作,这里就需要注意创建项目的时候一定要选择创建图形界面并且继承的基类是QMainWindow,否则后续切换到设计界面的时候有可能看不到菜单栏和工具栏在哪。然后后续的工作基本就是在菜单栏里添加Action部件,并在属性设置里为Action改名加图标,这里需要注意的一点是直接在菜单栏中输入中文会发现无效,这是因为Qt5在这个地方并没有对中文输入做优化,解决方法也非常简单,在添加Action部件的时候直接使用自己记得住的英文名字即可,然后后续在部件的右下方属性栏中将部件的文本修改成中文即可,当然也可以在记事本上写好部件名,然后一个一个粘贴到菜单栏,不过后者的效率说实话实在是太低了,不如第一种方法效率高。设计界面的拖放和编辑工作阿克就不带着大家做了,这个说实话太简单了,没有什么技术含量,设计好的界面大概模样如下图所示:
其中灰色的部分是MDI Area部件,除了MDIArea和新建、打开、关于功能,阿克建议其他Actioin部件的enabled属性全部取消,包括checkable属性也不要。当然如果有读者实在不会在设计界面进行操作的话也不必太紧张,你可以在本篇博客左侧的目录栏回到第一节下载本项目的源码,找到其中的mainwindow.ui文件导入即可,里面连Action部件的名字以及快捷键和状态栏提示我都替你写好了。
4. 复杂部件实现
本次的项目比较特殊,之前做的一些小项目我一般都会通过类的方式讲解,但本次项目不行,因为本次项目满打满所只有两个类,如果像之前一样讲的话,肯定会有读者觉得一脸懵,而且我的开发流程也就不是按照类来设计的,不过在说明复杂部件功能的具体实现之前还是先介绍一下这两个类,老规矩我们上表格:
类名 | 介绍 | 技术难度 |
---|---|---|
SmallWindow : QTextEdit |
软件多窗口区域的子窗口类,包含大量对于窗口富文本的处理函数和槽方法,有少量像外界发送的信号 | 偏难 |
MainWindow :QMainWindow |
整个软件的主窗口类,包含大量的Action部件槽方法和实现方法,少量信号 | 中等 |
这里也是罗列了一下我们项目中这两个类的一些基本信息,有读者会问为什么不展示具体的成员方法和槽函数这些内容呢,因为这正是我们接下来要具体实现的内容,首先我们盘点一下复杂的部件有哪些:新建Action、打开Action、保存另存Action、打印Action、插入图片Action和Combo兄弟。这些内容我们在本节一一进行剖析。首先是新建Action,它的槽方法我就不带大家做了,直接在设计界面转槽,然后调用成员方法即可,我们来做新建的具体实现,首先要将MainWindow类的构造方法做一个初始化操作,具体代码如下:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { // 主窗口的一些配置 ui->setupUi(this); setWindowTitle("AkeWord—文档办公"); setFixedSize(size()); ui->mdiArea->setViewMode(QMdiArea::TabbedView); // 将mdiArea的子窗口设置为选项卡模式 ui->mdiArea->setTabsClosable(true); // 令mdiArea的选项卡可以被快捷关闭 // 初始化字号栏 QFontDatabase fontdb; for(int index : fontdb.standardSizes()){ ui->comboBox_size->addItem(QString::number(index)); // setNum不是静态方法,所以无法直接使用 } // 找到系统默认的字号 QFont defaultFont; defaultFont = QApplication::font(); int defalutSize = defaultFont.pointSize(); auto tempQStr = QString::number(defalutSize); int tempIndex = ui->comboBox_size->findText(tempQStr); ui->comboBox_size->setCurrentIndex(tempIndex); // 连接一些必要的信号槽 connect(this, SIGNAL(signal_new(SmallWindow*)), this, SLOT(window_new(SmallWindow*))); connect(ui->mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::actions_en); }
其中的window_new和action_en是两个比较重要的槽方法,当子窗口新建内容的时候就会发送信号给window_new,而当mdiArea内部有子窗口被激活后就会发送信号给action_en,那这两个函数是什么呢,其代码如下:
void MainWindow::window_new(SmallWindow* smallWindow) { QString tempName = QFileInfo(smallWindow->m_path).fileName(); ui->comboBox_window->addItem(tempName); int tempIndex = ui->comboBox_window->findText(tempName); ui->comboBox_window->setCurrentIndex(tempIndex); } // 判断当前mdiArea是否有拥有子窗口 bool smallHave = false; smallHave = ui->mdiArea->activeSubWindow(); if(smallHave){ auto tempWindow = ui->mdiArea->activeSubWindow(); SmallWindow* smallWindow = qobject_cast<SmallWindow*>(tempWindow->widget()); auto tempName = QFileInfo(smallWindow->m_path).fileName(); int tempIndex = ui->comboBox_window->findText(tempName); ui->comboBox_window->setCurrentIndex(tempIndex); } ui->action_close->setEnabled(smallHave); ui->action_closeAll->setEnabled(smallHave); ui->action_next->setEnabled(smallHave); ui->action_last->setEnabled(smallHave); ui->action_paste->setEnabled(smallHave); ui->action_picture->setEnabled(smallHave); ui->action_save->setEnabled(smallHave); ui->action_saveAs->setEnabled(smallHave); ui->action_print->setEnabled(smallHave); ui->action_undo->setEnabled(smallHave); ui->action_redo->setEnabled(smallHave); ui->action_bold->setEnabled(smallHave); ui->action_italic->setEnabled(smallHave); ui->action_underline->setEnabled(smallHave); ui->action_left->setEnabled(smallHave); ui->action_right->setEnabled(smallHave); ui->action_center->setEnabled(smallHave); ui->action_justify->setEnabled(smallHave); ui->action_color->setEnabled(smallHave); }
不难看出,window_new只是将当前已经激活的文档名传入combo内,而action_en则是在子窗口激活之后,将action部件的可用性设置为真。那此时可能有读者要问为什么多此一举,默认action部件可用难道不行么,当然阿克这里不敢保证默认可用就不行,只是后续我们的部件很多是直接作用在子窗口内的,如果不在这里进行一个预防,后续很有可能出现各种各样的程序crash,所以有必要进行一下防御性编程。然后我们就可以实现新建文档的操作了,新建文档需要在MainWindow类和SmallWindow中一起实现,代码如下:
// MainWindow类 void MainWindow::operationNew() { SmallWindow* smWindow = new SmallWindow(); ui->mdiArea->addSubWindow(smWindow); connect(smWindow, SIGNAL(copyAvailable(bool)), ui->action_copy, SLOT(setEnabled(bool))); connect(smWindow, SIGNAL(copyAvailable(bool)), ui->action_cut, SLOT(setEnabled(bool))); connect(smWindow, SIGNAL(signal_delete(SmallWindow*)), this, SLOT(window_delete(SmallWindow*))); smWindow->newSmallWindow(); //asmWindow->setStyleSheet("background-color: white; border: 2px solid #6D6D6D; margin: 0px;"); smWindow->showMaximized(); emit signal_new(smWindow); } //SmallWindow类 void SmallWindow::newSmallWindow() { // 设置新建文档的临时标题 static int seq = 1; QString tempStr= QString("新建文档 %1").arg(seq++); // 为当前路径和窗口标题赋值 m_path = tempStr; setWindowTitle(tempStr + "[*]" + "—AkeWord"); setWindowFlag(Qt::WindowCloseButtonHint); // 连接文档信号与子窗口槽方法 connect(document(), SIGNAL(contentsChanged()), this, SLOT(contents_changed(void))); } void SmallWindow::contents_changed(void) { bool isModify = document()->isModified(); setWindowModified(isModify); }
其中的contents_changed必须要设置为一个槽方法,这样才能令setWindowTitle中的"[*]"字符动态 生效。然后接下来是打开Action的实现,同样需要在两个类中写代码,代码如下:
// MainWindow类 void MainWindow::operationOpen() { QString pathStr = QFileDialog::getOpenFileName (this, "请选择要打开的文档", "", "HTML文件(*.html *.htm)"); if(pathStr.isEmpty()) return; SmallWindow* smWindow = new SmallWindow(); ui->mdiArea->addSubWindow(smWindow); connect(smWindow, SIGNAL(copyAvailable(bool)), ui->action_copy, SLOT(setEnabled(bool))); connect(smWindow, SIGNAL(copyAvailable(bool)), ui->action_cut, SLOT(setEnabled(bool))); connect(smWindow, SIGNAL(signal_delete(SmallWindow*)), this, SLOT(window_delete(SmallWindow*))); if(smWindow->loadSmallWindow(pathStr)){ statusBar()->showMessage("成功打开文档: " + smWindow->m_path, 3000); smWindow->showMaximized(); emit signal_new(smWindow); }else{ smWindow->close(); statusBar()->showMessage("打开文档出错" + smWindow->m_path, 3000); } } void MainWindow::window_delete(SmallWindow *smallWindow) { // 将要删除的子窗口从comboBox中移除 QString tempName = QFileInfo(smallWindow->m_path).fileName(); int tempIndex = ui->comboBox_window->findText(tempName); ui->comboBox_window->removeItem(tempIndex); } // SmallWindow类 bool SmallWindow::loadSmallWindow(const QString& pathStr) { // 合法性检查 if(pathStr.isEmpty()) return false; QFile fileWill(pathStr); if(!fileWill.exists()) return false; if(!fileWill.open(QIODevice::ReadOnly)) return false; QByteArray text = fileWill.readAll(); // 设置文档内容 if(Qt::mightBeRichText(text)){ setHtml(text); }else{ setPlainText(text); } // 设置窗口属性 m_path = QFileInfo(pathStr).canonicalFilePath(); m_card = true; document()->setModified(false); setWindowModified(false); QString tempName = QFileInfo(pathStr).fileName(); setWindowTitle(tempName + "[*]"); // 连接文档信号与子窗口的槽方法 connect(document(), SIGNAL(contentsChanged()), this, SLOT(contents_changed(void))); return true; }
这里的window_delete槽方法作用其实和前面的window_new大同小异,这里表明的是在删除子窗口的时候将combo栏中对应的文档名进行删除。打开Action其他的地方虽然有一定的技术难度,但只要不是算法太拉跨,应该都能看懂阿克的代码。然后我们接着下一个保存和另存Action的实现,同样需要在两个类中进行实现,代码如下:
// MainWindow类 void MainWindow::operationSave() { SmallWindow* smallWindow = qobject_cast<SmallWindow*> (ui->mdiArea->activeSubWindow()->widget()); if(smallWindow && smallWindow->saveSmallWindow()){ statusBar()->showMessage("保存成功!", 3000); } } void MainWindow::operationSaveAs() { SmallWindow* smallWindow = qobject_cast<SmallWindow*> (ui->mdiArea->activeSubWindow()->widget()); if(smallWindow && smallWindow->saveAsSmallWindow()){ statusBar()->showMessage("文档已另存到本地!", 3000); } } // SmallWindow类 bool SmallWindow::saveSmallWindow() { if(m_card == false){ return saveAsSmallWindow(); }else{ if(!m_path.endsWith("html", Qt::CaseSensitive) && !m_path.endsWith("htm", Qt::CaseSensitive)){ m_path += ".html"; } QTextDocumentWriter writer(m_path); if(!writer.write(document())) return false; m_card = true; document()->setModified(false); setWindowModified(false); return true; } } bool SmallWindow::saveAsSmallWindow() { QString pathStr = QFileDialog::getSaveFileName (this, "请选择要存储的位置", "","HTML文档(*.html *htm)"); if(pathStr.isEmpty()) return false; QTextDocumentWriter writer(pathStr); if( !writer.write(document()) ) return false; m_card = true; return saveSmallWindow(); }
这几段代码逻辑上倒没什么难度,不过要声明一点,代码中出现的m_card成员变量表明的意思是当前文档是否在磁盘上对应的文件体,换句话说就是当前激活的这个子窗口是否已经保存过了,知道这个变量的含义之后,再把剩余的接口搞清楚,代码就迎刃而解了,至于如何掌握接口的具体作用,这里还是推荐大家先通过Ctrl索引的方式进入底层代码,通过参数和返回值甚至接口名等特征推断作用,实在搞不清楚的情况下再翻阅Qt配套的文档即可。然后是我们的打印Action,在实现打印功能之前我们还需要在pro文件内添加一行代码:
QT += printsupport
这行代码的作用是保证我们的项目支持打印功能,然后是具体的代码实现,这个功能复杂性相对来说低一些,难的是对于接口的理解,阿克目前为止对打印功能还是有些问题,比如调用出来的打印机界面预览功能无法使用,不过阿克认为代码是没有问题的,可能是根阿克的打印驱动有关,具体代码如下:
void MainWindow::operationPrint() { QPrinter printer(QPrinter::HighResolution); QPrintDialog* printerDialog = new QPrintDialog(&printer, this); if(ui->mdiArea->activeSubWindow()){ printerDialog->setOption(QAbstractPrintDialog::PrintSelection, true); printerDialog->setWindowTitle("打印文档"); SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(printerDialog->exec() == QDialog::Accepted){ smallWindow->print(&printer); } delete printerDialog; } }
然后是插入图片的Action,这一部分也比较简单,主要设计到文件选择对话框的防御性处理,之前的打开和保存Action代码里都有,记得一定要对文件选择类对话框进行合法性检查,否则会出现各种各样的程序crash,注意是crash,如果逻辑bug的话编译器能自动拦截下来,但是有些设计问题的bug编译器不会拦截,就有潜在的可能性导致程序crash,这是我们在设计Qt软件需要注意的地方,然后是Combo兄弟的实现,说实话这部分有点难度,不过也在可以接受的范围之内,具体代码如下:
// MainWindow类 void MainWindow::operationFontFamily(const QString &fontStr) { QTextCharFormat format; format.setFontFamily(fontStr); SmallWindow* smallWindow= qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(smallWindow){ smallWindow->format_change(format); } } void MainWindow::operationFontSize(const QString &fontStr) { QTextCharFormat format; format.setFontPointSize(fontStr.toDouble()); SmallWindow* smallWindow= qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(smallWindow){ smallWindow->format_change(format); } } void MainWindow::operationGoSmall(const QString &windowStr) { auto list = ui->mdiArea->subWindowList(); foreach(QMdiSubWindow* tempWindow, list){ SmallWindow* smallWindow= qobject_cast<SmallWindow*>(tempWindow->widget()); if(!smallWindow) return; QString tempName = QFileInfo(smallWindow->m_path).fileName(); if(tempName == windowStr){ ui->mdiArea->setActiveSubWindow(tempWindow); return; } } } // SmallWindow类 void SmallWindow::format_change(const QTextCharFormat &format) { QTextCursor cursor = textCursor(); if(!cursor.hasSelection()){ cursor.select(QTextCursor::WordUnderCursor); } cursor.mergeCharFormat(format); mergeCurrentCharFormat(format); }
其中fontFamily和fontSize顾名思义都是对文档字体格式的设计,实现起来也不是太难,然后是文件名的Combo,这个的主要作用是用于跳转界面,虽然代码实现起来不难,可相比各位读者也注意到了,其中的SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget())在整个项目中出现了大量的复用,其作用是找到当前多文本区域处于激活状态的子窗口,并通过类型转换的方式获得一个我们自己定义的子窗口对象指针,有兴趣的读者可以针对本项目做一个优化,将这串大量复用的代码封装成一个函数,阿克这里懒得搞了,反正也没多少东西,全当是练习打字了。
5. 简单部件实现
实现完复杂的部件以后,我们就可以接着实现剩余比较简单的部件了,像复制、粘贴、剪切这些action我就不带着看了,非常简单,直接转槽后调用子窗口对应的方法就行,比如撤销的方法就直接是一个undo()、同样恢复的方法也是redo(),这些东西Qt5的接口都已仅替我们封装好了,非常不错,然后是我们要说的是格式的一些Action,包括:加粗、倾斜、下划线Action和段落对齐Action。首先是字体格式的Action,这部分主要都包括加粗、倾斜和下划线,同样需要调用我们之前已经在SmallWindow里实现过的format_change方法,代码如下(转槽的步骤和之前一样,这里省略):
// 以下代码均在MainWindow中进行实现 void MainWindow::operationBold() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(!smallWindow) return; QTextCharFormat currentFormat = smallWindow->currentCharFormat(); if(currentFormat.fontWeight() == QFont::Normal){ QTextCharFormat tempFormat; tempFormat.setFontWeight(QFont::Bold); smallWindow->format_change(tempFormat); }else{ QTextCharFormat tempFormat; tempFormat.setFontWeight(QFont::Normal); smallWindow->format_change(tempFormat); } } void MainWindow::operationItalic() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(!smallWindow) return; QTextCharFormat currentFormat = smallWindow->currentCharFormat(); if(currentFormat.fontItalic()){ QTextCharFormat tempFormat; tempFormat.setFontItalic(false); smallWindow->format_change(tempFormat); }else{ QTextCharFormat tempFormat; tempFormat.setFontItalic(true); smallWindow->format_change(tempFormat); } } void MainWindow::operationUnderline() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(!smallWindow) return; QTextCharFormat currentFormat = smallWindow->currentCharFormat(); if(currentFormat.fontUnderline()){ QTextCharFormat tempFormat; tempFormat.setFontUnderline(false); smallWindow->format_change(tempFormat); }else{ QTextCharFormat tempFormat; tempFormat.setFontUnderline(true); smallWindow->format_change(tempFormat); } }
实现完上述代码之后,除了段落对齐不能使用,其实我们的程序已经可以跑起来了,很多功能也都表现的不错了,阿克在开发中也是实现完一个小模块后就马上尽可能的测试一下看看有没有bug什么的,这样才能保证软件的健壮性。最后是段落对齐的Action,这一部分包含左对齐、右对齐、居中对齐和两端对齐,需要转槽的地方比较多,具体代码如下:
// MainWindow类 void MainWindow::operationLeft() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(smallWindow){ smallWindow->alignSmallWindow(1); } } void MainWindow::operationRight() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(smallWindow){ smallWindow->alignSmallWindow(2); } } void MainWindow::operationCenter() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(smallWindow){ smallWindow->alignSmallWindow(3); } } void MainWindow::operationJustify() { SmallWindow* smallWindow = qobject_cast<SmallWindow*>(ui->mdiArea->activeSubWindow()->widget()); if(smallWindow){ smallWindow->alignSmallWindow(4); } } // SmallWindow类 void SmallWindow::alignSmallWindow(int indexType) { switch(indexType){ case 1: setAlignment(Qt::AlignLeft | Qt::AlignAbsolute); break; case 2: setAlignment(Qt::AlignRight | Qt::AlignAbsolute); break; case 3: setAlignment(Qt::AlignCenter); break; case 4: setAlignment(Qt::AlignJustify); break; default: break; } }
其实可以看到我们为了区分不同的对齐方式,采用了传参的方式降低耦合度,这里说实话阿克设计的并不是很好,因为在保证后续功能不再扩展的情况下,尤其是这种底层已经封装完毕的接口,不需要考虑耦合的问题,当然阿克这里就不再改动了,有兴趣的读者可以自己整合一下。至此我们的简单部件也实现完毕了。
6. 特殊部件实现&事件优化
到这步如果各位读者都能跟上的话,可以运行一下程序,基本效果就是下面这个样子:
不仅可以新建文档,还可以完成各种各样的富文本操作,效果非常不错,但是阿克要郑重告诉读者,项目还是有缺陷的,没有时间处理,比如一份文档,当用户修改完后,没有保存就关闭文档了,我们需要做一个对应的提醒,或者直接关闭软件时,我们也需要做一个对应的提醒,两个关闭事件和一键关闭的代码如下(记得在头文件里将两个方法声明为protected类型):
// MainWindow类 void MainWindow::closeEvent(QCloseEvent *event) { ui->mdiArea->closeAllSubWindows(); if(ui->mdiArea->currentSubWindow()){ event->ignore(); // 忽略此事件 }else{ event->accept(); // 接受此事件 } } // SmallWindow类 void SmallWindow::closeEvent(QCloseEvent *event) { if(tempCloseSmallWindow()){ emit signal_delete(this); event->accept(); }else{ event->ignore(); } } bool SmallWindow::tempCloseSmallWindow() { if(!document()->isModified()) return true; QMessageBox::StandardButton res; QString tempName = QFile(m_path).fileName(); // 等效于m_path res = QMessageBox::warning(this, "AKeWord提醒", QString("文档: %1 已被修改, 是否在关闭前选择保存?").arg(tempName), QMessageBox::Save | QMessageBox::Ignore | QMessageBox::Cancel); if(res == QMessageBox::Save){ if(saveSmallWindow()){ QMessageBox::information(this, "AkeWord提醒", QString("文档: %1 已成功保存").arg(tempName), QMessageBox::Ok); return true; }else{ return false; } }else if(res == QMessageBox::Cancel){ return false; } return true; }
可能有读者会问,怎么没有关闭Action的代码,这个其实有,不够太简单了,直接转槽,然后调用mdiArea对子窗口关闭就行了,阿克就不在详细罗列出来了。OK,至此我们的项目彻底结束。
7. 总结
对于阿克来说,项目的等级有着严格要求的,比如这次的宝藏级项目所对标的就是一些普遍的市场项目了,虽然称不上企业级项目吧,但至少也算是作坊级项目了, 如果能跟着阿克这篇博客亲自敲一遍代码并吃透,用来巩固和提升技术实力是非常有效果的。那么本次项目分享到此结束,欢迎大家提出各种各样的问题,我们一起讨论,关注博主,后续更多精彩项目免费分享!