4.6 实现文本编辑器功能
现在基本上已经创建好了应用程序的主窗口界面,包括主窗口的菜单、工具栏、中心部件、锚接部件以及状态栏。接下来,实现文本编辑器的基本功能,包括新建文件、打开文件、保存文件,实现编辑器的操作撤销,以及文本的剪切、复制、粘贴和文本全选功能等。
还是以Qt设计器绘制的主窗口为例,实现上述编辑器功能。在代码创建的主窗口界面中实现文本编辑器的功能是相同的。因为在定义成员变量的时候采用和Qt设计器中相同的名字,因此,只要将功能实现代码复制到相应的头文件和实现文件中就可以了。
首先,分析一下主窗口类CMainWindow的定义文件mainwindow.h,内容如下。
// chapter04/designmainwindow/src/mainwindow.h. #ifndef _MAINWINDOW_H_ #define _MAINWINDOW_H_ #include "ui_mainwindow.h" class QLabel; class CMainWindow : public QMainWindow, public Ui::MainWindow { Q_OBJECT public: CMainWindow(QWidget* = 0); private: QDockWidget* dockWidget; QLabel* label1; QLabel* label2; bool isUntitled; QString curFile; enum{MaxRecentFiles = 9}; QAction* recentFileActs[MaxRecentFiles]; QAction* separatorAct; void iniDockWidget(); void iniStatusBar(); void iniConnect(); void setCurrentFile(const QString&); bool saveFile(const QString&); bool loadFile(const QString&); void maybeSave(); void updateRecentFiles();
在类CMainWindow的私有区,新声明了5个成员变量,布尔型变量isUntitled记录当前正在编辑的文本是否已经保存在了硬盘上;字符串变量curFile保存当前编辑的文本文件的文件名;枚举变量MaxRecentFiles指定了最近文件列表的最多文件数目;数组recentFileActs[] 存放预先初始化好的QAction对象,以备在文件菜单上显示最近的文件列表;动作对象指针separatorAct指向一个菜单间隔线(separator),当文本编辑器显示最近打开的文件时,在文件菜单上显示间隔线,否则隐藏。
私有函数iniConnect()完成所有窗口部件的信号和槽的关联操作;函数setCurrentFile()设置文本编辑器的编辑状态;函数saveFile()保存指定文件名的文件;函数loadFile()从指定的位置读取指定的文件到文本编辑器;函数maybeSave()判断是否要保存文件,如果是则进行保存;函数updateRecentFiles()更新文本编辑器中文件菜单上的最近文件列表。
private slots: void doCursorChanged(); void doNew(); void doOpen(); void doClose(); void doSave(); void doASave(); void doQuit(); void doUndo(); void doCut(); void doCopy(); void doPast(); void doAll(); void doFind(); void doModified(); void openRecentFile(); }; #endif
头文件定义了14个私有槽函数,实现相应的菜单和工具栏功能。槽函数doCursorChanged()实现光标移动时动态显示光标在编辑器状态中的位置;槽函数doModified()完成编辑器状态变化时的实时状态显示;槽函数openRecentFile()打开最近文件列表指定的文件。
接下来,看一下类CMainWindow的实现文件mainwindow.cpp的内容。
// chapter04/designmainwindow/src/mainwindow.cpp. #include <QtGui> #include "mainwindow.h" #include "findfileform.h" CMainWindow::CMainWindow(QWidget* parent) : QMainWindow(parent) { setupUi(this); iniDockWidget(); iniStatusBar(); iniConnect(); updateRecentFiles(); showMaximized(); isUntitled = true; curFile = tr("untitled"); setWindowTitle(curFile + "[*]"); }
类CMainWindow的构造函数实现文本编辑器应用程序主窗口的初始化,包括初始化锚接部件、主窗口状态栏和关联所有必需的信号和槽。
在文本编辑器启动的时候,调用函数updatRecentFiles()完成文件菜单上的最近文件列表的更新。
初始化成员变量isUntitled为ture,表明新文档从未保存过。
函数setWindowTitle()设置窗口的标题。标题文本中的“[*]”指示当文档被修改时,窗口标题中的修改标识“*”出现的位置。必须指定“[*]”,否则当文档改变时,窗口的标题不会出现修改标识“*”。
void CMainWindow::iniDockWidget() { CFindFileForm* findFileForm = new CFindFileForm; dockWidget = new QDockWidget(tr("查找文件"), this); dockWidget->setAllowedAreas(Qt::RightDockWidgetArea); dockWidget->setFeatures(QDockWidget::AllDockWidgetFeatures); dockWidget->setFloating(false); dockWidget->setWidget(findFileForm); dockWidget->setVisible(true); addDockWidget(Qt::RightDockWidgetArea, dockWidget); }
函数iniDockWidget()实现对锚接部件的初始化,完成查找文件功能的初始化。前面已经详细分析了该函数的实现细节。
void CMainWindow::iniStatusBar() { QStatusBar* bar = statusBar(); label1 = new QLabel; label1->setMinimumSize(200, 25); label1->setFrameShadow(QFrame::Sunken); label1->setFrameShape(QFrame::WinPanel); label2 = new QLabel; label2->setMinimumSize(200, 25); label2->setFrameShadow(QFrame::Sunken); label2->setFrameShape(QFrame::WinPanel); bar->addWidget(label1); bar->addWidget(label2); }
函数iniStatusBar()完成主窗口状态栏的初始化。
void CMainWindow::iniConnect() { connect(textEdit, SIGNAL(cursorPositionChanged()), this, SLOT(doCursorChanged())); connect(actNew, SIGNAL(triggered()), this, SLOT(doNew())); connect(actOpen, SIGNAL(triggered()), this, SLOT(doOpen())); connect(actClose, SIGNAL(triggered()), this, SLOT(doClose())); connect(actSave, SIGNAL(triggered()), this, SLOT(doSave())); connect(actASave, SIGNAL(triggered()), this, SLOT(doASave())); connect(actQuit, SIGNAL(triggered()), this, SLOT(doQuit())); connect(actUndo, SIGNAL(triggered()), this, SLOT(doUndo())); connect(actCut, SIGNAL(triggered()), this, SLOT(doCut())); connect(actCopy, SIGNAL(triggered()), this, SLOT(doCopy())); connect(actPaste, SIGNAL(triggered()), this, SLOT(doPast())); connect(actAll, SIGNAL(triggered()), this, SLOT(doAll())); connect(actFind, SIGNAL(triggered()), this, SLOT(doFind())); connect(textEdit->document(), SIGNAL(contentsChanged()), this, SLOT(doModified())); separatorAct = menu_F->insertSeparator(actQuit); separatorAct->setVisible(false); for (int i = 0; i < MaxRecentFiles; ++i) { recentFileActs[i] = new QAction(this); menu_F->insertAction(separatorAct, recentFileActs[i]); recentFileActs[i]->setVisible(false); connect(recentFileActs[i], SIGNAL(triggered()), this, SLOT(openRecentFile())); } }
函数iniConnect()完成相关信号和槽的关联。对于编辑器的有些功能,可以通过直接将Qt窗口部件提供的信号和槽关联起来就可以了。比如将QAction对象actCopy的triggered()信号与QTextEdit对象textEidt的槽copy()直接关联起来完成选中文本的复制操作。但为了便于解释代码,我们将actCopy的triggered()信号与CMainWindow对象的槽doCopy()关联起来,然后在槽doCopy()中调用槽函数QText::copy()。
在iniConnect()函数的最后,初始化文件菜单的最近文件列表的动作,将它们插入到文件菜单中,并隐藏这些动作。
void CMainWindow::doCursorChanged() { int pageNum = textEdit->document()->pageCount(); const QTextCursor cursor = textEdit->textCursor(); int colNum = cursor.columnNumber(); int rowNum = textEdit->document()->blockCount(); label1->setText(tr("%1 页 %3 列").arg(pageNum).arg(colNum)); }
槽函数doCursorChanged()接收文本编辑器中光标的位置变化时QTextEdit对象发出的信号QTextEdit::cursorPositionChanged(),它完成主窗口状态栏中相应显示信息的更新。
函数QTextEdit::document()获取QTextEdit对象的底层QTextDocument对象的指针并返回。QTextDocument是一个结构化的富文本(rich text)文档的容器,提供了对样式文本(styled text)和多种文档元素的支持,比如列表(lists)、表格(tables)、框(frames)和图片;提供了一些方便的操作,比如撤销undo、恢复redo等。QTextDocument既可以为QTextEdit所使用,也可以单独使用。在构造一个QTextEdit对象的时候,也将会构造一个QTextDocument对象,通过函数QTextEdit::document()获取该QTextDocument对象的指针引用。QTextDocument::pageCount()函数获取文本文档的页数。
函数QTextEdit::textCursor()获取当前可见光标QTextCursor对象的一个拷贝。QTextCursor类封装了访问和编辑QTextDocument的API接口。
QTextCursor::columnNumber()函数获取光标所在的列位置。
QTextDocument::blockCount()函数获取文档的块数。因为本例子只是一个简单的文本编辑器,没有图片、表格等其他文档元素,因此该函数返回的也是文档的实际行数。
最后,将获得的文档的信息显示在状态栏上的label1中。
void CMainWindow::doFind() { dockWidget->show(); dockWidget->setFloating(false); }
槽函数doFind()显示锚接部件,并将它停靠在应用程序主窗口内。
void CMainWindow::doNew() { maybeSave(); isUntitled = true; curFile = tr("untitled"); setWindowTitle(curFile + "[*]"); textEdit->clear(); textEdit->setVisible(true); setWindowModified(false); }
槽函数doNew()完成创建新文本文件的功能。首先,它调用maybeSave()函数对先前编辑过的文本的进行保存。然后,设置isUntitled成员变量的值为true,表示新文档从来没有保存过。最后,设置应用程序窗口的标题和修改状态。
函数QTextEdit::clear()清空文本编辑框的内容,同时也清除了撤销和恢复的历史,并设置文档的修改状态为未编辑的(此时QTextEdit::isModified()返回false)。
函数QMainWindow::setWindowModified()设置应用程序窗口的修改状态,如果参数为true,则窗口标题将会出现修改标识“*”。
void CMainWindow::maybeSave() { if(textEdit->document()->isModified()) { QMessageBox box; box.setWindowTitle(tr("警告")); box.setIcon(QMessageBox::Warning); box.setText(tr("文档没有保存,是否保存?")); box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); if(box.exec() == QMessageBox::Yes) { doSave(); } } }
函数maybeSave()实现文件的保存。它首先判断文档在最后一次保存之后是否有修改,如果没有修改该函数返回;如果修改了,它会弹出一个对话框,提示用户是否要保存。如果用户需要保存文件,则调用槽函数doSave()完成文本文件的真正保存。
void CMainWindow::doOpen() { QString fileName = QFileDialog::getOpenFileName(this); if (!fileName.isEmpty()) { maybeSave(); if (loadFile(fileName)) { label2->setText(tr("已经读取")); } } textEdit->setVisible(true); }
槽函数doOpen()响应用户的打开操作,打开一个新的文件。它首先弹出一个打开文件对话框,如果用户选择了要打开的文件,该函数将会保存当前正在编辑的文件(如果还没有及时保存的话),然后调用loadFile()读取指定的文件,并设置状态栏的显示信息为“已经读取”。
bool CMainWindow::loadFile(const QString& fileName) { QFile file(fileName); if (!file.open(QFile::ReadOnly | QFile::Text)) { QMessageBox::warning(this, tr("读取文件"), tr("无法读取文件 %1:\n%2.") .arg(fileName) .arg(file.errorString())); return false; } QTextStream in(&file); QApplication::setOverrideCursor(Qt::WaitCursor); textEdit->setText(in.readAll()); QApplication::restoreOverrideCursor(); setCurrentFile(fileName); return true; }
loadFile()函数读取指定的文件内容并显示在应用程序主窗口的文本编辑框内。
函数QApplication::setOverrideCursor()设置应用程序的鼠标状态。当鼠标移动到应用程序的任何一个窗口部件上时,鼠标的形状都是新设置的鼠标形状。一般当应用程序向用户显示一个特定的状态时,需要调用该函数设置鼠标的形状,比如比较耗时的操作。它具有一个QCursor类型的形参。在此,传递实参Qt::WaitCursor给Qcursor的构造函数,它将构造一个“等待”形状的鼠标对象。
函数QApplication::restoreOverrideCursor()将鼠标形状恢复到调用函数QApplication::setOverride Curosr()之前的状态。
最后,调用setCurrentFile()设置当前文本编辑器的状态。函数setCurrentFile()的实现如下所示。
void CMainWindow::setCurrentFile(const QString& fileName) { curFile = QFileInfo(fileName).canonicalFilePath(); isUntitled = false; setWindowTitle(curFile + "[*]"); textEdit->document()->setModified(false); setWindowModified(false); QSettings settings("709", "SDI example"); QStringList files = settings.value("recentFiles").toStringList(); files.removeAll(fileName); files.prepend(fileName); while (files.size() > MaxRecentFiles) files.removeLast(); settings.setValue("recentFiles", files); updateRecentFiles(); }
函数QFileInfo::canonicalFilePath()返回一个标准的路径(不包含符号链接、当前目录“.”和父目录“..”的路径)。在一个没有符号链接的系统上该函数将会和函数QFileInfo::absoluteFilePath()返回相同的字符串。
QSettings类提供了平台无关的、永久保存应用程序配置项的功能。它使用<键,值>对的模式保存应用程序的数据。QSettings的键有点类似于文件系统的路径,可以通过类似于路径的方式指定子键(比如“SDI example/currentFileList”),也可以使用beginGroup()和endGroup()函数开始或结束一个键(子键)。例如,
QStringList fileList; fileList << “file1” << “file2”; settings.setValue(“SDI example/currentFileList”, fileList);
或者
QStringList fileList; fileList << “file1” << “file2”; settings.beginGroup(“SDI example”); settings.setValue(“currentFileList”, fileList); settings.endGroup();
在默认情况下,QSettings将会保存应用程序的数据到一个平台相关的位置。在Windows系统,它使用系统注册表;在UNIX系统,它将数据保存在文本文件(用户目录下的.config文件。例如,作者使用的是红旗Linux系统,运行该应用程序后,Qt会将这些数据保存在/home/lcf/.config文件中);在Mac系统,它使用Core Foundation Preferences API。
QSettings构造函数的参数指定了组织机构的名字和应用程序的名字。QSetting将会使用这些字符串信息并根据特定的平台查找应用程序保存的数据。
QSettings::value()函数取出应用程序原先保存的数据,并保存在一个QStringList列表中。完成最近文件列表的更新后,最后QSettings::setValue()将数据重新保存,原来的数据将会被覆盖。
最后,updateRecentFiles()更新文件菜单中的最近文件列表,代码实现如下。
void CMainWindow::updateRecentFiles() { QSettings settings("709", "SDI example"); QStringList files = settings.value("recentFiles").toStringList(); int numRecentFiles = qMin(files.size(), (int)MaxRecentFiles); for (int i = 0; i < numRecentFiles; ++i) { QString text = tr("&%1 %2").arg(i + 1).arg(files[i]); recentFileActs[i]->setText(text); recentFileActs[i]->setData(files[i]); recentFileActs[i]->setVisible(true); } for (int i = numRecentFiles; i < MaxRecentFiles; ++i) recentFileActs[i]->setVisible(false); separatorAct->setVisible(numRecentFiles > 0); }
updateRecentFiles()函数中,首先获取应用程序保存的最近文件列表,然后根据列表中的文件数目,依次设置文件菜单中的动作。
qMin()是Qt提供的一个全局的模板函数,它返回两个数值中的最小值。
函数QAction::setData()设置动作的内部数据,通过QAction::data()可以重新获得内部数据的内容。设置的数据内容是一个文件的绝对路径,当打开最近列表中的文件时将用到该数据。
最后,如果最近文件列表非空,那么显示菜单间隔线separatorAct对象;否则,置为不可见。
void CMainWindow::doClose() { maybeSave(); textEdit->setVisible(false); }
槽函数doClose()接收用户的“关闭”操作发出的信号,关闭当前显示的文本文档。
void CMainWindow::doSave() { if (isUntitled) { doASave(); } else { saveFile(curFile); } }
槽函数doSave()接收用户的“保存”操作发出的信号,完成文件的保存。
void CMainWindow::doASave() { QString fileName = QFileDialog::getSaveFileName(this, tr("另存为"), curFile); if (!fileName.isEmpty()) { saveFile(fileName); } }
槽函数doASave()接收用户的“另存为”操作发出的信号,将文本文档保存到一个新的文件中。
void CMainWindow::doQuit() { doClose(); qApp->quit(); }
槽函数doQuit()接收用户的“退出”文本编辑器操作发出的信号,保存文件并退出系统。
在一个应用程序中,只能创建一个QApplication对象。变量qApp是一个全局的指向Qt应用程序的指针,它等价于调用函数QCoreApplication::instance()。
void CMainWindow::doUndo() { textEdit->undo(); }
槽函数接收用户撤销操作发出的信号,完成文本编辑器的撤销操作。该函数直接调用QTextEdit类提供的函数undo()。
void CMainWindow::doCut() { textEdit->cut(); }
槽函数doCut()完成文本的剪切功能。槽函数QTextEdit::cut()将会将选中的文本内容剪切到剪贴板。
void CMainWindow::doCopy() { textEdit->copy(); }
槽函数doCopy()完成文本的复制功能。槽函数QTextEdit::copy()将会将选中的文本内容复制到剪贴板。
void CMainWindow::doPast() { textEdit->paste(); }
槽函数doPaste()完成文本的粘贴功能。槽函数QTextEdit::paste()将剪贴板中的文本粘贴到光标所在的位置。
void CMainWindow::doAll() { textEdit->selectAll(); }
槽函数doAll()选中文本编辑器中的全部文本内容。
bool CMainWindow::saveFile(const QString& fileName) { QFile file(fileName); if (!file.open(QFile::WriteOnly | QFile::Text)) { QMessageBox::warning(this, tr("保存文件"), tr("无法保存文件 %1:\n%2.") .arg(fileName) .arg(file.errorString())); return false; } QTextStream out(&file); QApplication::setOverrideCursor(Qt::WaitCursor); out << textEdit->toPlainText(); QApplication::restoreOverrideCursor(); setCurrentFile(fileName); label2->setText(tr("已经保存")); return true; }
函数saveFile()实现对指定文件的保存。
函数QTextEdit::toPlainText()将文本编辑框中的文本内容转换为普通文本格式并返回转换后的文本内容。
void CMainWindow::doModified() { setWindowModified(textEdit->document()->isModified()); label2->setText(tr("正在修改")); }
槽函数doModified()接收QTextEdit对象textEdit的信号textChanged(),完成文本编辑器状态的修改和显示。当文本编辑框QTextEdit的内容改变时,QTextEdit对象会发出信号textChanged()。
void CMainWindow::openRecentFile() { QAction *action = qobject_cast<QAction *>(sender()); if (action) loadFile(action->data().toString()); }
槽函数openRecentFile()接收用户选择文件菜单中的打开最近文件操作时发出的信号,打开最近文件列表指定的文件。
现在,重新编译、运行应用程序。