界面展示
核心概念
应用程序中的主窗口
- 主窗口是与用户进行长时间交互的顶层窗口
- 程序的绝大多数功能直接由主窗口提供
- 主窗口通常是应用程序启动后显示的第一个窗口
- 装个程序由一个主窗口和多个对话框组成
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 中常用文本编辑组件的集成层次图
不同文本组件的特性比较
单行文本支持 | 多行文本支持 | 自定义格式支持 | 富文本支持 | |
QLineEdit | Yes | No | No | No |
QPlainTextEdit | Yes | Yes | No | No |
QTextEdit | Yes | Yes | Yes | Yes |
Qt 中常用文本编辑组件的内置功能
- 右键弹出菜单
- 快捷键功能(复制,粘贴,剪切,等)
/**
*@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);
拖放事件中的 QMimeData
- QMimeData 是 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 平台管理
- 只能发送堆事件对象
- 事件被处理后由 Qt 平台销毁
-
- 使用 sendEvent 发送事件对象
- 消息发送过程可以理解为:在 sendEvent() 函数内部直接调用 Qt 对象的event() 事件处理
- 使用 postEvent 发送事件对象
菜单栏中删除功能的实现
- 定义事件对象 KeyPress
- 定义事件对象 KeyRelease
- 发送事件 KePress
- 发送事件 KeyRelease
void MainWindow::onEditDelete()
{
QKeyEvent keyPress(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
QKeyEvent keyRelease(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
QApplication::sendEvent(&mainEditor, &keyPress);
QApplication::sendEvent(&mainEditor, &keyRelease);
}
创建可复用的查找对话框
查找对话框的架构与设计
查找对话框的界面布局
查找功能的核心思想
- 获取当前光标的位置并作为起始点
- 向前(向后)目标第一次出现的位置
- 通过目标位置以及目标长度在文本框中进行标记
查找算法流程
MainWindow 与 FindDialog 之间的关系图
文件: FindDialog.h
#ifndef FINDDIALOG_H
#define FINDDIALOG_H
#include <QDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QRadioButton>
#include <QPointer>
#include <QPlainTextEdit>
class FindDialog : public QDialog
{
Q_OBJECT
protected:
QGroupBox m_radioGrpBx;
QGridLayout m_layout;
QHBoxLayout m_hbLayout;
QLabel m_findLbl;
QLineEdit m_findEdit;
QPushButton m_findBtn;
QPushButton m_cancelBtn;
QCheckBox m_matchChkBx;
QRadioButton m_upwardBtn;
QRadioButton m_downwardBtn;
QPointer<QPlainTextEdit> m_pText; // 注意这里,弱耦合设计!!
void initControl();
void connectSlot();
public slots:
void onFindClicked();
void onCancelClicked();
public:
FindDialog(QWidget* parent = nullptr, QPlainTextEdit* pText = nullptr);
void setPlainTextEdit(QPlainTextEdit* pText);
QPlainTextEdit* getPlainTextEdit();
bool event(QEvent* e);
~FindDialog();
};
#endif // FINDDIALOG_H
文件:FindDialog.cpp
#include "FindDialog.h"
#include <QEvent>
#include <QTextCursor>
#include <QMessageBox>
FindDialog::FindDialog(QWidget* parent, QPlainTextEdit* pText) : QDialog (parent, Qt::WindowCloseButtonHint | Qt::Drawer)
{
initControl();
connectSlot();
setLayout(&m_layout);
setFixedSize(450, 120);
setWindowTitle("查找");
setPlainTextEdit(pText);
}
void FindDialog::initControl()
{
m_findLbl.setText("查找目标:");
m_findBtn.setText("查找下一个(&F)");
m_cancelBtn.setText("取消");
m_matchChkBx.setText("区分大小写(&C)");
m_radioGrpBx.setTitle("方向");
m_upwardBtn.setText("向上(&U)");
m_downwardBtn.setText("向下(&D)");
m_downwardBtn.setChecked(true);
m_hbLayout.addWidget(&m_upwardBtn);
m_hbLayout.addWidget(&m_downwardBtn);
m_radioGrpBx.setLayout(&m_hbLayout);
m_layout.addWidget(&m_findLbl, 0, 0);
m_layout.addWidget(&m_findEdit, 0, 1);
m_layout.addWidget(&m_findBtn, 0, 2);
m_layout.addWidget(&m_matchChkBx, 1, 0);
m_layout.addWidget(&m_radioGrpBx, 1, 1);
m_layout.addWidget(&m_cancelBtn, 1, 2);
}
void FindDialog::connectSlot()
{
connect(&m_findBtn, SIGNAL(clicked()), this, SLOT(onFindClicked()));
connect(&m_cancelBtn, SIGNAL(clicked()), this, SLOT(onCancelClicked()));
}
void FindDialog::setPlainTextEdit(QPlainTextEdit* pText)
{
m_pText = pText;
}
QPlainTextEdit* FindDialog::getPlainTextEdit()
{
return m_pText;
}
void FindDialog::onFindClicked()
{
QString target = m_findEdit.text();
if( (m_pText != nullptr) && (target != "") )
{
QString text = m_pText->toPlainText();
QTextCursor c = m_pText->textCursor();
int index = -1;
if( m_downwardBtn.isChecked() )
{
index = text.indexOf(target, c.position(), m_matchChkBx.isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive);
if( index >= 0 )
{
c.setPosition(index);
c.setPosition(index + target.length(), QTextCursor::KeepAnchor);
m_pText->setTextCursor(c);
}
}
if( m_upwardBtn.isChecked() )
{
index = text.lastIndexOf(target, c.position() - text.length() - 1, m_matchChkBx.isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive);
if( index >=0 )
{
c.setPosition(index + target.length());
c.setPosition(index, QTextCursor::KeepAnchor);
m_pText->setTextCursor(c);
}
}
if( index < 0 )
{
QMessageBox msg(this);
msg.setWindowTitle("记事本");
msg.setText(QString("找不到 ") + "\"" + target + "\"");
msg.setWindowFlag(Qt::Drawer);
msg.setIcon(QMessageBox::Information);
msg.setStandardButtons(QMessageBox::Ok);
msg.exec();
}
}
}
void FindDialog::onCancelClicked()
{
close();
}
bool FindDialog::event(QEvent* e)
{
if( e->type() == QEvent::Close )
{
hide(); // 为了保持上一次用户的操作属性,进隐藏窗口
return true;
}
return QDialog::event(e);
}
FindDialog::~FindDialog()
{
}
创建可复用的替换对话框
替换对话框的设计与实现
替换对话框的界面布局
替换流程图算法
MainWindow 与 ReplaceDialog 之间的关系图
文件:ReplaceDialog.h
#ifndef REPLACEDIALOG_H
#define REPLACEDIALOG_H
#include "FindDialog.h"
class ReplaceDialog : public FindDialog
{
Q_OBJECT
protected:
QLabel m_replaceLbl;
QLineEdit m_replaceEdit;
QPushButton m_replaceBtn;
QPushButton m_replaceAllBtn;
void initControl();
void connectSlot();
protected slots:
void onReplaceClicked();
void onReplaceAllClicked();
public:
ReplaceDialog(QWidget* parent = nullptr, QPlainTextEdit* pText = nullptr);
};
#endif // REPLACEDIALOG_H
文件:ReplaceDialog.cpp
#include "ReplaceDialog.h"
ReplaceDialog::ReplaceDialog(QWidget* parent, QPlainTextEdit* pText) : FindDialog (parent, pText)
{
initControl();
connectSlot();
}
void ReplaceDialog::initControl()
{
m_replaceLbl.setText("替换为:");
m_replaceBtn.setText("替换(&R)");
m_replaceAllBtn.setText("全部替换(&A)");
m_layout.removeWidget(&m_matchChkBx);
m_layout.removeWidget(&m_radioGrpBx);
m_layout.removeWidget(&m_cancelBtn);
m_layout.addWidget(&m_replaceLbl, 1, 0);
m_layout.addWidget(&m_replaceEdit, 1, 1);
m_layout.addWidget(&m_replaceBtn, 1, 2);
m_layout.addWidget(&m_matchChkBx, 2, 0);
m_layout.addWidget(&m_radioGrpBx, 2, 1);
m_layout.addWidget(&m_replaceAllBtn, 2, 2);
m_layout.addWidget(&m_cancelBtn, 3, 2);
setFixedSize(450, 170);
setWindowTitle("替换");
}
void ReplaceDialog::connectSlot()
{
connect(&m_replaceBtn, SIGNAL(clicked()), this, SLOT(onReplaceClicked()));
connect(&m_replaceAllBtn, SIGNAL(clicked()), this, SLOT(onReplaceAllClicked()));
}
void ReplaceDialog::onReplaceClicked()
{
QString target = m_findEdit.text();
QString to = m_replaceEdit.text();
if( (m_pText != nullptr) && (target != "") && (to != "") )
{
QString selText = m_pText->textCursor().selectedText();
if( selText == target )
{
m_pText->insertPlainText(to);
}
onFindClicked();
}
}
void ReplaceDialog::onReplaceAllClicked()
{
QString target = m_findEdit.text();
QString to = m_replaceEdit.text();
if( (m_pText != nullptr) && (target != "") && (to != "") )
{
QString text = m_pText->toPlainText();
text.replace(target, to, m_matchChkBx.isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive);
m_pText->clear();
m_pText->insertPlainText(text);
}
}
Qt 中的调色板
- QPalette 类包含了组件状态的颜色组
-
QPalette 对象包含了三个状态的颜色描述
-
激活颜色组(Active)
- 组件获得焦点使用的颜色搭配方案
-
非激活颜色组(Inactive)
- 组件失去焦点使用的颜色方案
-
失效颜色组(Disabled)
- 组件处于不可用状态使用的颜色方案
-
调色板是存储组件颜色信息的数据结构
组件外观所使用的颜色都位于调色板中
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);
文本编辑器项目持续开发
行间跳转
-
算法设计
- 通过输入对话框获取目标行号
- 查找换行符的位置计算目标行第一个字符的下标
- 通过 QTextCursor 定位到目标行
void MainWindow::onEditGoto()
{
bool ok = false;
int ln = QInputDialog::getInt(this, "转到", "行号:", 1, 1, mainEditor.document()->lineCount(), 1, &ok,
Qt::WindowCloseButtonHint | Qt::Drawer);
if( ok )
{
QString text = mainEditor.toPlainText();
QTextCursor c = mainEditor.textCursor();
int pos = 0;
int next = -1;
for(int i=0; i<ln; i++)
{
pos = next + 1;
next = text.indexOf('\n', pos);
}
c.setPosition(pos);
mainEditor.setTextCursor(c);
}
}
设置工具栏和状态栏的可见性
-
实现思路
- 通过 setVisble() 设置可见性
-
更新界面上 QAction 对象的状态
- 菜单栏中的 QAction 对象是否勾选
- 工具栏中的 QAction 对象是否按下
void MainWindow::onViewToolBar()
{
QToolBar* tb = toolBar();
bool visible = tb->isVisible();
tb->setVisible(!visible);
findMenuBarAction("工具栏")->setChecked(!visible);
findToolBarAction("工具栏")->setChecked(!visible);
}
void MainWindow::onViewStatusBar()
{
QStatusBar* sb = statusBar();
bool visible = sb->isVisible();
sb->setVisible(!visible);
findMenuBarAction("状态栏")->setChecked(!visible);
findToolBarAction("状态栏")->setChecked(!visible);
}
自定义文本框中的字体和大小
-
实现思路
- 通过 QFontDialog 选择字体以及大小
- 将 QFont 对象设置到文本编辑框
void MainWindow::FormatFont()
{
bool ok = false;
QFont font = QFontDialog::getFont(&ok, mainEditor.font(), this, "打印");
if( ok )
{
mainEditor.setFont(font);
}
}
自动换行
- 获取当前编辑框的换行模式
- 将模式进行反转后并进行设置
- 更新对应 QAction 对象的状态
void MainWindow::FormatWrap()
{
QPlainTextEdit::LineWrapMode mode = mainEditor.lineWrapMode();
if( mode == QPlainTextEdit::NoWrap )
{
mainEditor.setLineWrapMode(QPlainTextEdit::WidgetWidth);
findMenuBarAction("自动换行")->setChecked(true);
findToolBarAction("自动换行")->setChecked(true);
}
else
{
mainEditor.setLineWrapMode(QPlainTextEdit::NoWrap);
findMenuBarAction("自动换行")->setChecked(false);
findToolBarAction("自动换行")->setChecked(false);
}
}
打开外部文件
- QDesktopServices 提供了一系列桌面开发相关的服务接口
- 通过 QDesktopService 中的成员函数打开帮助文件
void MainWindow::onHelpManual()
{
QDesktopServices::openUrl(QUrl("https://segmentfault.com/u/tiansong"));
}
程序中的配置文件
- 应用程序在运行后都有一个初始化的状态
- 一般而言: 程序的初始状态是最近一次运行退出前的状态
- 解决思路
- 程序退出前保存状态参数到文件(数据库)
- 程序再次启动时读出状态参数并恢复
-
状态参数的存储方式
- 文件文件格式(XML, JSon, 等)
- 轻量级数据库(Access, SQLite, 等)
- 私有二进制文件
-
Qt 中的解决方案
- 通过二进制数据流将状态参数直接存储于文件中
-
优势:
- 参数的存储和读取简单高效,易于编码实现
- 最终文件为二进制文件,不易被恶意修改
- 设计与实现
文件: AppConfig.h
#ifndef APPCONFIG_H
#define APPCONFIG_H
#include <QObject>
#include <QFont>
#include <QPoint>
#include <QSize>
class AppConfig : public QObject
{
protected:
QFont m_editFont;
QSize m_mainWindowSize;
QPoint m_mainWindowPoint;
bool m_isAutoWrap;
bool m_isToolBarVisible;
bool m_isStatusVisible;
bool m_isVilid;
bool restore();
public:
explicit AppConfig(QObject *parent = nullptr);
explicit AppConfig(QFont font, QSize size, QPoint point, bool isWrap, bool tbvisible, bool sbVisible, QObject *parent = nullptr);
bool store();
QFont editFont();
QSize mainWindowSize();
QPoint mainWindowPoint();
bool isAutoWrap();
bool isToolBarVisible();
bool isStatusVisible();
bool isVilid();
};
#endif // APPCONFIG_H
文件: AppConfig.cpp
#include "AppConfig.h"
#include <QFile>
#include <QDataStream>
#include <QApplication>
AppConfig::AppConfig(QObject *parent) : QObject(parent)
{
m_isVilid = restore();
}
AppConfig::AppConfig(QFont font, QSize size, QPoint point, bool isWrap, bool tbvisible, bool sbVisible, QObject *parent) : QObject(parent)
{
m_editFont = font;
m_mainWindowSize = size;
m_mainWindowPoint = point;
m_isAutoWrap = isWrap;
m_isToolBarVisible = tbvisible;
m_isStatusVisible = sbVisible;
m_isVilid = true;
}
bool AppConfig::restore()
{
bool ret = true;
QFile file(QApplication::applicationDirPath() + "/app.config");
if( file.open(QIODevice::ReadOnly) )
{
QDataStream in(&file);
in >> m_editFont;
in >> m_mainWindowSize;
in >> m_mainWindowPoint;
in >> m_isAutoWrap;
in >> m_isToolBarVisible;
in >> m_isStatusVisible;
file.close();
}
else
{
ret = false;
}
return ret;
}
bool AppConfig::store()
{
bool ret = true;
QFile file(QApplication::applicationDirPath() + "/app.config");
if( file.open(QIODevice::WriteOnly) )
{
QDataStream out(&file);
out << m_editFont;
out << m_mainWindowSize;
out << m_mainWindowPoint;
out << m_isAutoWrap;
out << m_isToolBarVisible;
out << m_isStatusVisible;
file.close();
}
else
{
ret = false;
}
return ret;
}
QFont AppConfig::editFont()
{
return m_editFont;
}
QSize AppConfig::mainWindowSize()
{
return m_mainWindowSize;
}
QPoint AppConfig::mainWindowPoint()
{
return m_mainWindowPoint;
}
bool AppConfig::isAutoWrap()
{
return m_isAutoWrap;
}
bool AppConfig::isToolBarVisible()
{
return m_isToolBarVisible;
}
bool AppConfig::isStatusVisible()
{
return m_isStatusVisible;
}
bool AppConfig::isVilid()
{
return m_isVilid;
}
- 值得思考的问题: 什么时候保存主窗口的状态数据?
-
应用程序退出的过程
- 收到关闭事件
- 执行关闭事件处理函数
- 主窗口从屏幕消失
- 主窗口的析构函数执行
- 。。。
- 一般而言, 应用程序收到关闭事件时进行状态参数的保存
-
Qt 中的解决方案
- 重写关闭事件处理函数
- 在关闭事件处理函数中保存状态参数
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();
}
}
命令行参数的应用
-
命令行参数的应用 一
-
传统应用方式
- 在命令行启动 GUI 程序时传递参数
-
-
命令行参数的应用 二
-
操作系统关联方式
- 在文件被双击时,操作系统根据文件后缀选择应用程序
- 将文件路径作为命令行参数启动应用程序
-
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
int ret = -1;
if( w != nullptr )
{
if( argc > 1 )
{
QFileInfo fi(argv[1]);
if( fi.exists() )
{
w->openFile(argv[1]);
}
}
w->show();
ret = a.exec();
delete w;
}
return ret;
}
应用程序的打包与发布
发布应用程序时的候选者
-
调试版(debug): 开发阶段的可执行程序
- 包含与调试相关的各种信息,体积巨大
- 执行速度慢,支持断点调试
-
发布版(release): 最终产品的可执行程序
- 无任何冗余信息,体积小巧
- 执行速度快,无法映射到源码调试
程序的依赖库
- 可执行程序的正常运行需要外部库的支持
- 因此,发布程序是必须保证所有的依赖库都存在
- Window 中可以使用 Depends 工具查看程序的依赖库
-
Linux 中可以使用 ldd 命令查看程序的依赖库
- ldd 是 Linux 系统中一个脚本程序
- 文件路径: /usr/bin/ldd
程序的环境依赖
- 应用程序对执行环境可能存在依赖
-
可能的依赖:
- 环境变量,驱动程序,数据库引擎
- Java 虚拟机, .net Framework
- 。。。。。。
-
Window 下的环境部署
- 免费应用软件,小巧,简便,精美
- 以 Pascal 语言作为脚本
- 官方网址:http://www.jrsoftware.org/isd...
-
Linux 下的环境部署
-
方式一:
- 通过 ldd 命令确定程序的库依赖
- 通过 Shell 脚本开发部署程序
-
方式二:
- 根据具体发行版开发专用部署程序(deb, rpm)
-
附:为了降低模块间的耦合性,多处使用了QSharedPointer
以上内容参考狄泰软件学院系列课程,请大家保护原创!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。