界面展示核心概念应用程序中的主窗口主窗口是与用户进行长时间交互的顶层窗口程序的绝大多数功能直接由主窗口提供主窗口通常是应用程序启动后显示的第一个窗口装个程序由一个主窗口和多个对话框组成Qt 中的主窗口- Qt 开发平台中直接支持主窗口的概念- QMainWindow 是 Qt 中主窗口的基类- QMainWindow 继承于 QWidget 是一种容器类型的组件QMainWindow 中的封装1. 菜单栏2. 工具栏3. 中心组件4. 停靠组件5. 状态栏QMainWindow 中的组件布局在 Qt 中与菜单相关的类组件/** @brief 创建菜单栏 /bool MainWindow::initMenuBar(){ QMenuBar mb = menuBar(); bool ret = (mb != nullptr); ret = ret && initFileMenu(mb); ret = ret && initEditMenu(mb); ret = ret && initFormatMenu(mb); ret = ret && initViewMenu(mb); ret = ret && initHelpMenu(mb); return ret;}/* @brief 创建下拉菜单组 /bool MainWindow::initFileMenu(QMenuBar mb){ QMenu menu = new QMenu(“文件(&F)”, mb); bool ret = (menu != nullptr); if( ret ) { QAction* action = nullptr; ret = ret && makeAction(action, menu, “新建(&N)”, Qt::CTRL + Qt::Key_N); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileNew())); menu->addAction(action); } ret = ret && makeAction(action, menu, “打开(&O)…”, Qt::CTRL + Qt::Key_O); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileOpen())); menu->addAction(action); } ret = ret && makeAction(action, menu, “保存(&S)”, Qt::CTRL + Qt::Key_S); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileSave())); menu->addAction(action); } ret = ret && makeAction(action, menu, “另存为(&A)…”, 0); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileSaveAs())); menu->addAction(action); } menu->addSeparator(); ret = ret && makeAction(action, menu, “页面设置(&U)…”, Qt::CTRL + Qt::Key_U); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFilePageSetup())); menu->addAction(action); } ret = ret && makeAction(action, menu, “打印(&P)…”, Qt::CTRL + Qt::Key_P); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFilePrint())); menu->addAction(action); } menu->addSeparator(); ret = ret && makeAction(action, menu, “退出(&X)”, 0); if( ret ) { menu->addAction(action); } } if( ret ) { mb->addMenu(menu); } else { delete menu; } return ret;}/** @brief 创建菜单项 /bool MainWindow::makeAction(QAction& action, QWidget parent, QString text, int key){ bool ret = true; action = new QAction(text, parent); if( action != nullptr ) { action->setShortcut(QKeySequence(key)); // 添加快捷键 } else { ret = false; } return ret;}主窗口中的工具栏工具栏的概念和意义应用程序中集成各种功能实现快捷使用的一个区域工具栏并不是应用程序中必须存在的组件工具栏中的元素可以是各种组件窗口工具栏中的元素通常以图标按钮的方式存在在 Qt 中与工具栏相关的组件/** @brief 创建工具栏 /bool MainWindow::initToolBar(){ QToolBar tb = addToolBar(“工具栏”); bool ret = true; tb->setIconSize(QSize(16, 16)); tb->setFloatable(false); tb->setMovable(false); ret = ret && initFileToolItem(tb); tb->addSeparator(); ret = ret && initEditToolItem(tb); tb->addSeparator(); ret = ret && initFormatToolItem(tb); tb->addSeparator(); ret = ret && initViewToolItem(tb); return ret;}/* @brief 创建与文件操作相关的快捷项 /bool MainWindow::initFileToolItem(QToolBar tb){ QAction action = nullptr; bool ret = true; ret = ret && makeAction(action, tb, “新建”, “:/Res/pic/new.png”); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileNew())); tb->addAction(action); } ret = ret && makeAction(action, tb, “打开”, “:/Res/pic/open.png”); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileOpen())); tb->addAction(action); } ret = ret && makeAction(action, tb, “保存”, “:/Res/pic/save.png”); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileSave())); tb->addAction(action); } ret = ret && makeAction(action, tb, “另存为”, “:/Res/pic/saveas.png”); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFileSaveAs())); tb->addAction(action); } ret = ret && makeAction(action, tb, “打印”, “:/Res/pic/print.png”); if( ret ) { connect(action, SIGNAL(triggered()), this, SLOT(onFilePrint())); tb->addAction(action); } return ret;}/** @brief 创建具体的快捷项 /bool MainWindow::makeAction(QAction& action, QWidget parent, QString tip, QString icon){ bool ret = true; action = new QAction("", parent); if( action != nullptr ) { action->setToolTip(tip); action->setIcon(QIcon(icon)); } else { ret = false; } return ret;}主窗口中的状态栏状态栏的概念和意义状态栏是应用程序中输出简要信息的区域状态栏一般位于主窗口的最底部状态栏中的消息类型实时消息,如:当前程序状态永久消息,如:程序版本号,机构名称进度消息,如:进度条提示,百分比提示在 Qt 中提供与状态栏相关的类组件Qt 状态栏的设计原则左边的区域用于输出实时消息右边的区域用于设置永久消息addWidget 在左半部分添加组件addPermanentWidget 在状态栏右半部分调价组件/** @brief 创建状态栏 /bool MainWindow::initStatusBar(){ QStatusBar sb = statusBar(); QLabel label = new QLabel(“D.T.TianSong”); bool ret = true; if( label != nullptr ) { sb->addPermanentWidget(new QLabel()); statusLabel.setMinimumWidth(150); statusLabel.setAlignment(Qt::AlignCenter); statusLabel.setText(“length: " + QString::number(0) + " lines: " + QString::number(1)); sb->addPermanentWidget(&statusLabel); statusCursorLabel.setMinimumWidth(150); statusCursorLabel.setAlignment(Qt::AlignCenter); statusCursorLabel.setText(“Ln: " + QString::number(1) + " Col: " + QString::number(1)); sb->addPermanentWidget(&statusCursorLabel); label->setMinimumWidth(150); label->setAlignment(Qt::AlignCenter); sb->addPermanentWidget(label); } else { ret = false; } return ret;}Qt 中的文本编辑组件Qt 中支持 3 中常用的文本编辑组件QLineEdit 单行文本编辑组件QTextEdit 多行富文本编辑组件QPlainTextEdit 多行普通文本编辑组件Qt 中常用文本编辑组件的集成层次图不同文本组件的特性比较 单行文本支持多行文本支持自定义格式支持富文本支持QLineEditYesNoNoNoQPlainTextEditYesYesNoNoQTextEditYesYesYesYesQt 中常用文本编辑组件的内置功能右键弹出菜单快捷键功能(复制,粘贴,剪切,等)/** @brief 创建中心组件 /bool MainWindow::initMainEditor(){ bool ret = true; QPalette p = mainEditor.palette(); p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight)); p.setColor(QPalette::Inactive, QPalette::HighlightedText, p.color(QPalette::Active, QPalette::HighlightedText)); mainEditor.setPalette(p); mainEditor.setParent(this); mainEditor.setAcceptDrops(false); setCentralWidget(&mainEditor); return ret;}Qt 中的 IO 操作Qt 中 IO 操作的处理方式Qt 通过统一的接口简化了文件与外部设备的操作方式Qt 中的文件被看作一种特殊的外部设备Qt 中的文件操作与外部设备的操作相同IO操作的微本质:连续存储空间的数据读写Qt 中 IO 设备的继承层次图QFile 是 Qt 中用于文件操作的类,对应到计算机上的一个文件QFileInfo 类用于读取文件信息QTemporaryFile 安全的创建一个全局唯一的临时文件,对象销毁时临时文件删除void write(QString f){ QFile file(f); if( file.open(QIODevice::WriteOnly | QIODevice::Text) ) { file.write(“D.T.Software\n”); file.write(“Delphi Tang\n”); file.close(); }}void read(QString f){ QFile file(f); if( file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QByteArray ba = file.readLine(); QString s(ba); qDebug() << s; file.close(); }}void info(QString f){ QFile file(f); QFileInfo info(file); qDebug() << info.exists(); qDebug() << info.isFile(); qDebug() << info.isReadable(); qDebug() << info.isWritable(); qDebug() << info.created(); qDebug() << info.lastRead(); qDebug() << info.lastModified(); qDebug() << info.path(); qDebug() << info.fileName(); qDebug() << info.suffix(); qDebug() << info.size();}文本流和数据流Qt 中将文件类型分为 2 大类文本文件: 文件内容是可读的文本字符数据文件: 文件内容是直接的二进制数据Qt 提供辅助类简化了文本文件/数据文件的读写QTextStream - 写入的数据全部转换为可读文本QDataStream - 写入的数据根据类型转换为二进制数据void text_stream_test(QString f){ QFile file(f); if( file.open(QIODevice::WriteOnly | QIODevice::Text) ) { QTextStream out(&file); out << QString(“D.T.Software”) << endl; out << QString(“Result: “) << endl; out << 5 << ‘’ << 6 << ‘=’ << 5 * 6 << endl; file.close(); } if( file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QTextStream in(&file); while( !in.atEnd() ) { QString line = in.readLine(); qDebug() << line; } file.close(); }}void data_stream_test(QString f){ QFile file(f); if( file.open(QIODevice::WriteOnly) ) { QDataStream out(&file); out.setVersion(QDataStream::Qt_4_7); out << QString(“D.T.Software”); out << QString(“Result: “); out << 3.14; file.close(); } if( file.open(QIODevice::ReadOnly) ) { QDataStream in(&file); QString dt = “”; QString result = “”; double value = 0; in.setVersion(QDataStream::Qt_4_7); in >> dt; in >> result; in >> value; file.close(); qDebug() << dt; qDebug() << result; qDebug() << value; }}不同 Qt 版本的数据流文件格式可能不同设置读写版本号:void setVersion(int v)获取读写版本号:int version() const当前数据流文件可能在不同版本的 Qt 程序间传递数据时,需要考虑版本问题缓冲区与目录操作Qt 中缓冲区的概念缓冲区的本质为一段连续的存储空间QBuffer 是 Qt 中缓冲区相关的类在 Qt 中可以将缓冲区看作一种特殊的 IO 设备文件辅助类可以直接用于操作缓冲区QBuffer 缓冲区的使用场合在线程间进行不同类型的数据传递缓存外部设备中的数据返回数据读取速度小于数据写入速度void write_buffer(int type, QBuffer& buffer){ if( buffer.open(QIODevice::WriteOnly) ) { QDataStream out(&buffer); out << type; if( type == 0 ) { out << QString(“D.T.Software”); out << QString(“3.1415”); } else if( type == 1 ) { out << 3; out << 1415; } else if( type == 2 ) { out << 3.1415; } buffer.close(); }}void read_buffer(QBuffer& buffer){ if( buffer.open(QIODevice::ReadOnly) ) { int type = -1; QDataStream in(&buffer); in >> type; if( type == 0 ) { QString dt = “”; QString pi = “”; in >> dt; in >> pi; qDebug() << dt; qDebug() << pi; } else if( type == 1 ) { int a = 0; int b = 0; in >> a; in >> b; qDebug() << a; qDebug() << b; } else if( type == 2 ) { double pi = 0; in >> pi; qDebug() << pi; } buffer.close(); }}int main(int argc, char argv[]){ QCoreApplication a(argc, argv); QByteArray array; QBuffer buffer(&array); write_buffer(2, buffer); read_buffer(buffer); return a.exec();}QDir 是 Qt 中功能强大的目录操作类Qt 中的目录分隔符统一使用 ‘/‘QDir 能够对目标目录进行任意操作(创建,删除,重命名)QDir 能够获取指定目录中的所有条目QDir 能够获取系统中的所有根目录QFileSystemWatcher 用于监控文件和目录的状态变化能够监控特定目录和文件的状态能够同时对多个目录和文件进行监控当目录或者文件改变时将触发信号可以通过信号与槽的机制捕捉信号并作出相应文本编辑器中的数据存储QAction 的信号QAction 被点击之后会产生一个 triggered 信号通过信号与槽的机制能够捕捉对 QAction 对象的操作项目中可以将多个信号映射到同一个槽函数文件打开操作文件保存操作定义成员变量 m_filePath 用于标记数据来源文件另存为操作int MainWindow::showQueryMessage(QString message){ QMessageBox msg(this); msg.setIcon(QMessageBox::Question); msg.setWindowTitle(“记事本”); msg.setWindowFlag(Qt::Drawer); msg.setText(message); msg.setStandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); return msg.exec();}void MainWindow::preEditChange(){ if( m_isTextChanged ) { QString path = (m_filePath != “”) ? m_filePath : “无标题”; int r = showQueryMessage(QString(“是否将更改保存到\n”) + “"” + path + “" ?”); switch ( r ) { case QMessageBox::Yes: saveCurrentData(“保存”, m_filePath); break; case QMessageBox::No: m_isTextChanged = false; break; case QMessageBox::Cancel: break; } }}void MainWindow::openFileEditor(QString path){ if( path != "” ) { QFile file(path); if( file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QTextStream in(&file); in.setCodec(“GBK”); mainEditor.setPlainText(in.readAll()); file.close(); m_filePath = path; m_isTextChanged = false; setWindowTitle(m_filePath + “- 记事本”); } else { showErrorMessage(QString(“打开文件失败!\n\n”) + “"” + path + “"。”); } }}void MainWindow::openFile(QString path){ preEditChange(); if( !m_isTextChanged ) { openFileEditor(path); }}void MainWindow::onFileOpen(){ preEditChange(); if( !m_isTextChanged ) { QString path = showFileDialog(QFileDialog::AcceptOpen, “打开”, “:/Res/pic/logo.png”); openFileEditor(path); }}QString MainWindow::saveCurrentData(QString title, QString path){ QString ret = path; if( ret == "” ) { ret = showFileDialog(QFileDialog::AcceptSave, title, “:/Res/pic/logo.png”); } if( ret != "” ) { QFile file(ret); if( file.open(QIODevice::WriteOnly | QIODevice::Text) ) { QTextStream out(&file); out << mainEditor.toPlainText(); file.close(); setWindowTitle(ret + " - 记事本”); m_isTextChanged = false; } else { showErrorMessage(QString(“保存文件失败!\n\n”) + “"。” + ret + “"”); } } return ret;}void MainWindow::onFileSave(){ QString path = saveCurrentData(“保存”, m_filePath); if( path != "” ) { m_filePath = path; }}void MainWindow::onFileSaveAs(){ QString path = saveCurrentData(“另存为”); if( path != "” ) { m_filePath = path; }}文本编辑器中的功能交互QPlainTextEdit 相关的信号使用关键槽函数判断数据状态void textChanged() ==> 辅助判断是否有数据未保存void copyAvailabel(bool)void cursorPositionChanged()void redoAvailable(bool);void undoAvailable(bool)判断是由存在未保存的数据定义槽函数 void onTextChanged()映射 textChanged() 到槽函数定义成员变量 bool m_isTextChanged = false;文本框中的字符发生变化时: m_isTextChanged = true当 m_isTextChanged 为真,则存在未保存的数据void MainWindow::onTextChanged(){ if( !m_isTextChanged ) { setWindowTitle(" " + windowTitle()); } m_isTextChanged = true; statusLabel.setText(“length: " + QString::number(mainEditor.toPlainText().length()) + " lines: " + QString::number(mainEditor.document()->lineCount()));}文件新建操作void MainWindow::onFileNew(){ preEditChange(); if( !m_isTextChanged ) { mainEditor.clear(); m_isTextChanged = false; setWindowTitle(“新建文本文档 - 记事本”); }}文本编辑器中的后缀映射通过 QMap 实现QString MainWindow::showFileDialog(QFileDialog::AcceptMode mode, QString title, QString icon){ QFileDialog fd(this); QStringList filters; QMap<QString, QString> map; const char filterArray[][2] = { {“文本文档(.txt)”, “.txt”}, {“所有文件(.)” , “.” }, {nullptr , nullptr} }; QString ret = “”; for(int i=0; filterArray[i][0]!=nullptr; i++) { filters.append(filterArray[i][0]); map.insert(filterArray[i][0], filterArray[i][1]); } fd.setWindowTitle(title); fd.setWindowIcon(QIcon(icon)); fd.setAcceptMode(QFileDialog::AcceptOpen); fd.setNameFilters(filters); if( mode == QFileDialog::AcceptOpen ) { fd.setFileMode(QFileDialog::ExistingFile); } if( fd.exec() == QFileDialog::Accepted ) { ret = fd.selectedFiles()[0]; if( mode == QFileDialog::AcceptSave ) { QString postfix = map[fd.selectedNameFilter()]; if( (postfix != “.”) && !ret.endsWith(postfix) ) { ret = ret + postfix; } } } return ret;}Qt 中的事件处理图形界面应用程序的消息处理模型Qt 平台将系统产生的消息转换为 Qt 事件Qt 事件用于描述程序内部或外部发生的动作任意的 QObject 对象都具备事件处理的能力GUI 应用程序的事件处理方式Qt 事件产生后立即被分发到 QWidget 对象QWidget 中的 event(QEvent) 进行事件处理event() 根据事件类型调用不同的事件处理函数在事件处理函数中发送 Qt 中预定义的信号调用信号关联的槽函数事件(QEvent)和信号(SIGNAL)不同事件由具体对象进行处理信号由具体对象主动产生改写事件处理函数可能导致程序行为发生改变信号是否存在对应的槽函数不会改变程序行为一般而言,信号在具体的事件处理函数中产生文本编辑器的关闭操作Qt 没有提供预定义的关闭信号,因此重写关闭事件/** @brief 重写关闭事件处理函数 /void MainWindow::closeEvent(QCloseEvent event){ preEditChange(); if( !m_isTextChanged ) { QFont font = mainEditor.font(); bool isWrap = (mainEditor.lineWrapMode() == QPlainTextEdit::WidgetWidth); bool tbVisible = (findMenuBarAction(“工具栏”)->isCheckable() && findToolBarAction(“工具栏”)->isChecked()); bool sbVisible = (findMenuBarAction(“状态栏”)->isCheckable() && findToolBarAction(“状态栏”)->isChecked()); AppConfig config(mainEditor.font(), size(), pos(), isWrap, tbVisible, sbVisible, this); config.store(); QMainWindow::closeEvent(event); } else { event->ignore(); }}/ @brief 查找菜单栏中对应的 ACtion /QAction MainWindow::findMenuBarAction(QString text){ QAction ret = nullptr; const QObjectList& list = menuBar()->children(); for(int i=0; i<list.count(); i++) { QMenu men = dynamic_cast<QMenu*>(list[i]); if( men != nullptr ) { QList<QAction*> actions = men->actions(); for(int j=0; j<actions.count(); j++) { if( actions[j]->text().startsWith(text) ) { ret = actions[j]; break; } } } } return ret;}/** @brief 查找工具栏中对应的 ACtion /QAction MainWindow::findToolBarAction(QString text){ QAction ret = nullptr; QList<QAction*> actions = toolBar()->actions(); for(int j=0; j<actions.count(); j++) { if( actions[j]->toolTip().startsWith(text) ) { ret = actions[j]; break; } } return ret;}Qt 中的拖放事件拖放一个文件进入窗口时将触发拖放事件每一个 QWidget 对象都能够处理拖放事件拖放事件的处理函数为:void dragEnterEvent(QDragEnterEvent* e);void dropEvent(QDropEvent* e);拖放事件中的 QMimeDataQMimeData 是 Qt 中的多媒体数据类拖放事件通过 QMimeData 对象传递数据QMimeData 支持多种不同类型的多媒体数据常用 MIME 类型数据处理函数自定义拖放事件的步骤对接收拖放事件的对象调用 setAcceptDrop 成员函数重写 dragEnterEvent 函数并判断 MIME 类型期望数据: e->acceptProposedAction();其他数据: e->ignore();重写 dropEvent 函数并判断 MIMI 类型期望数据: 从事件对象中获取 MIME 数据并处理其它数据: e->ignore();文本编辑器中的拖放操作void MainWindow::dragEnterEvent(QDragEnterEvent* event){ if( event->mimeData()->hasUrls() ) { event->acceptProposedAction(); } else { event->ignore(); }}void MainWindow::dropEvent(QDropEvent* event){ if( event->mimeData()->hasUrls() ) { QList<QUrl> list = event->mimeData()->urls(); QString path = list[0].toLocalFile(); QFileInfo fi(path); if( fi.isFile() ) { preEditChange(); if( !m_isTextChanged ) { openFileEditor(path); } } else { showErrorMessage(QString(“对 “) + “"” + path + “" 的访问被拒绝。”); } } else { event->ignore(); }}文本打印与光标定位QPlainTextEdit 内部的文档结构(数据与界面分离)QPlainTextEdit 通过 QTextDocument 对象存储文本QPlainTextEdit 本身只负责界面形态的显示QTextDocument 是表示文本以及文本属性的数据类设置文本属性: 排版,字体,标题,等获取文本参数: 行数,文本宽度,文本信息,等实现标准操作:撤销,重做,查找,打印,等打印功能的实现步骤连接 QAction 打印对象的信号到槽在槽函数中定义 QPrintDialog 对象根据用户选择获取 QPrinter 对象通过 QTextDocument 对象进行打印void MainWindow::onFilePrint(){ QPrintDialog dlg(this); dlg.setWindowTitle(“打印”); if( dlg.exec() == QPrintDialog::Accepted ) { QPrinter* p = dlg.printer(); p->setPageLayout(m_pPageSetupDlg->printer()->pageLayout()); mainEditor.document()->print(p); }}光标位置的计算思路文本框对象的内部包含了 QTextCursor 对象通过 position() 成员函数获取当前光标的字符位置根据光标的字符位置计算横纵坐标当光标位置发生变化时进行计算算法流程描述通过 ’n’ 字符的个数计算所在行通过最后一个 ’n’ 字符的下标计算所在列void MainWindow::onCursorPositionChanged(){ int col = 0; int ln = 0; int flg = -1; int pos = mainEditor.textCursor().position(); QString text = mainEditor.toPlainText(); for(int i=0; i<pos; i++) { if( text[i] == ‘\n’ ) { ln ++; flg = i; } } flg ++; col = pos - flg; statusCursorLabel.setText(“Ln: " + QString::number(ln + 1) + " Col: " + QString::number(col + 1));}在程序中发送自主事件阻塞型事件发送: 时间发送后需要等待事件处理完成非阻塞型事件发送:事件发送后立即返回; 事件被发送到事件队列中等待处理QApplication 类提供了支持事件发送的静态成员函数阻塞型发送函数: bool sendEvent(QObject receiver, QEvent event);非阻塞型事件发送函数: bool postEvent(QObject receiver, QEvent event);注意事项sendEvent 中事件对象的生命期由 Qt 平台管理同时支持栈事件对象和堆事件对象postEvent 中事件对象的生命期由 Qt 平台管理