这一章的例子是对《Qt Creator快速人门》基础应用篇各章节知识的综合应用, 也是一个规范的实例程序。之所以说其规范,是因为在这个程序中,我们对菜单什么时候可用/什么时候不可用、关闭程序时应该先保存已修改且尚未保存的文件等细节都做了严格的约束。而一个真正实用的应用程序,也就应该如此。

  本章应用了基础篇的众多知识点,但这里只是讲解程序流程与框架,没有涉及太多知识细节的讲解。这个实例主要是对主窗口部件的应用,所以可以学完《Qt Creator快速入门》的前5章再来学习本章,这样可以达到更好的效果。该实例是基于Qt中的MDI Example示例程序 的,它在Main Windows分类下。这个程序就是以QMainWindow类为主窗口,以QMdiArea类为多文档区域,以QTextEdit类为子窗口部件,从而实现了一个多文档 编辑器的应用。最终的运行效果如图1-1所示。


图1-1 多文档编辑器界面

1.1 界面设计

  先进行界面的设计,这里主要是对主窗口菜单栏和工具栏的设计。打开Qt Creator,创建新的项目。(项目源码路径:src\1 \1-1\myMdi)新建Qt Gui应用,项目名称myMdi,类名默认为MainWindow,基类默认为QMainWindow都不做改动。 完成后双击mainwindow. ui文件进人设计模式,然后添加各个菜单,所有的菜单动作如图1-2所示,最终的菜单栏和工具栏如图1-3所示。设计菜单时,如果将来触发这个菜单会弹出一个对话框进行详细设置,那么就在这个菜单文本后面添加"..."号,例如这里的“打开文件”菜单和“另存为”菜单。这里还要注意,添加动作时,一定要使动作名称和这里的Action编辑器中所使用的名称保持一致,因为在后面的程序中还要用到它们。添加工具栏的工具是用鼠标把Action编辑器的Action拖动到工具栏做到的,图片资源文件来自工程目录下的image文件夹。


图1-2 Action编辑器


图1-3 菜单栏与工具栏

  设计完菜单栏与工具栏后,向主窗口中心区域拖入一个MdiArea部件,并单击主窗口界面,按下Ctrl + G快捷键,使其处于栅格布局之中。可以看一下对象列表窗口,确保MdiArea部件的objectName是mdiArea,而文件菜单、编辑菜单、窗口菜单和帮助菜单的objectName分别是menuF、menuE、menuW和menuH;如果不是,需 要在属性栏中更改,因为后面的程序中要用到。

1.2 创建子窗口类

 

1.2.1 mdichild. h具体代码及说明

  为了实现多文档操作,需要向QMdiArea中添加子窗口,而为了可以更好地操作子窗口,必须子类化子窗口的中心部件。因为这里子窗口的中心部件使用了QTextEdit类,所以要实现自己的类,它必须继承自QTextEdit,然后在其中添加我们的功能函数。
  (项目源码路径:src\1\1 -2\myMdi)往项目中添加新文件,模板选择“C+ + 类”,类名为MdiChild,基类为QTextEdit,类型信息选择“继承自QWidget”。完成后在mdichild. h文件中添加代码:

#include <QWidget>
#include <QTextEdit>

class MdiChild:public QTextEdit
{
Q_OBJECT

public:
    explicit MdiChild(QWidget *parent = nullptr);

    void newFile(); // 新建文件
    bool loadFile(const QString &filePath); // 加载文件
    bool save(); // 保存操作
    bool saveAs(); // 另存为操作
    bool saveFile(const QString &filePath); // 保存文件
    QString getFileNameFromPath(); // 从文件路径中提取出文件名
    QString getCurFileName() { return curFile; } // 获得返回的当前文件名称(路径)

protected:
    void closeEvent(QCloseEvent *event);    // 关闭事件

private slots:
    void documentWasModified(); //文档被更改时,窗口显示更改状态标志

private:
    bool maybeSave();                       // 判断是否需要保存
    void setCurrentFile(const QString &filePath);  // 设置当前文件

    QString curFile; // 保存新建文件时自动产生的当前文件路径(名称)
    bool isUnsaved_flag; //该标志位判断文件是否为“未保存状态”,若是,则打开文件对话框执行“另存为”操作,否则直接保存
};

  这里在头文件中声明了11个函数,定义了两个变量。其中,currentFile()函数 返回当前的文件路径,只有一行代码,就直接在这里定义了。所以真正需要设计的只有10个函数,还有curFile与isUnsaved_flag两个变量,分别用于保存当前文件的路径和作为文件是否被保存过的标志。因为对于所有的应用程序,只有涉及新建、保存和关闭等操作时,都是使用的这些函数进行设置的,它们是一个整体,所以这里要将它们同时罗列出来。这些函数主要完成了下面几个操作:

  1. 新建文件操作newFile()

    • 设置窗口编号;
    • 设置文件未被保存过“isUnsaved_flag = true;”;
    • 保存文件路径,给curFile赋初值;
    • 设置子窗口标题;
    • 关联文档内容改变信号到显示文档更改状态标志槽documentWasModified()。
  2. 加载文件操作loadFile()
    • 打开指定的文件,并读取文件内容到编辑器;
    • 设置当前文件setCurrentFile(),该函数可以获取文件路径,完成文件和窗口状态的设置;
    • 关联文档内容改变信号到显示文档更改状态标志槽documentWasModified()。
  3. 保存操作save()
    • 如果文件没有被保存过(用isUnsaved_flag判断),执行另存为操作saveAs() ;
    • 否则直接保存文件saveFile(),该函数先打开指定文件,然后将编辑器的内容写入该文件,最后设置当前文件setCurrentFile()。
  4. 另存为操作saveAs()
    • 从文件对话框获取文件路径;
    • 如果路径不为空,则保存文件saveFile()。
  5. 关闭操作 closeEvent()
    • 如果maybeSave()函数返回为真,则关闭窗口。maybeSave()函数判断文档是否被更改过,如果被更改过,则弹出对话框,让用户选择是否保存更改,或者取消关闭操作。如果用户选择保存更改,则返回保存操作save()的结果,如 果选择取消,则返回false。否则,直接返回true。
    • 如果maybeSave()函数返回为假,则忽略该事件。
       

1.2.2 mdichild. cpp具体代码及说明

  下面一次性贴出了mdichild.cpp中的所有代码,没有像书中一样分步骤贴出,并进行说明:

#include "mdichild.h"
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QApplication>
#include <QFileInfo>
#include <QFileDialog>
#include <QCloseEvent>
#include <QPushButton>

MdiChild::MdiChild(QWidget *parent) :
    QTextEdit(parent)
{
    // 这样可以在子窗口关闭时销毁这个类的对象
    setAttribute(Qt::WA_DeleteOnClose);

    // 初始isUntitled为true
    isUnsaved_flag = true;
}

// 新建文件
void MdiChild::newFile()
{
    // 设置窗口编号,因为窗口一直被保存,所以需要使用静态变量
    static int windowNumber = 1; //窗口编号从1开始

    // 新建的文档没有被保存过
    isUnsaved_flag = true;

    // 将当前文件命名为:未命名文档加窗口编号,窗口编号先使用再加1
    curFile = tr("未命名文档%1.txt").arg(windowNumber++);

    // 设置窗口标题,使用[*]可以在文档被更改后在文件名称后才显示”*“号
    setWindowTitle(curFile + "[*]" + tr(" - 多文档编辑器"));

    // 当文档内容被更改时发射contentsChanged()信号,执行documentWasModified()槽函数,在标题栏上显示'*'
    connect(document(), SIGNAL(contentsChanged()),
            this, SLOT(documentWasModified()));
}

// 文档被更改时,窗口显示更改状态标志
void MdiChild::documentWasModified()
{
    // 根据文档的isModified()函数的返回值,判断我们编辑器内容是否被更改了
    // 如果被更改了,参数为true,则setWindowModified()就会在设置了[*]号的地方显示“*”号
    setWindowModified(document()->isModified()); //setWindowModified为库函数
}

// 加载文件
bool MdiChild::loadFile(const QString &filePath)
{
    // 新建QFile对象
    QFile file(filePath);

    // 只读方式打开文件,出错则打开消息提示对话框,并返回false
    if (!file.open(QFile::ReadOnly | QFile::Text))
    {
        // %1和%2分别可以被后面的arg()中的fileName和file.errorString()代替
        QMessageBox::warning(this, tr("多文档编辑器"),
                             tr("无法读取文件 %1:\n%2.")
                             .arg(filePath).arg(file.errorString()));
        return false;
    }

    // 新建文本流对象
    QTextStream in(&file);

    // 设置鼠标状态为等待状态
    QApplication::setOverrideCursor(Qt::WaitCursor);

    // 读取文件的全部文本内容,并添加到编辑器中
    setPlainText(in.readAll());

    // 恢复鼠标状态
    QApplication::restoreOverrideCursor();

    // 设置当前文件
    setCurrentFile(filePath);

    // 文档的“内容改变”信号,连接到“文档改变槽”,即当文档内容改变,则标题栏出现'*'
    connect(document(), SIGNAL(contentsChanged()),
            this, SLOT(documentWasModified()));

    return true;
}

// 设置当前文件,将加载文件的路径保存到filePath中
void MdiChild::setCurrentFile(const QString &filePath)
{
    // canonicalFilePath()可以除去路径中的符号链接,“.”和“..”等符号
    curFile = QFileInfo(filePath).canonicalFilePath();

    // 文件已经被保存过了
    isUnsaved_flag = false;

    // 文档没有被更改过
    document()->setModified(false);

    // 窗口不显示被更改标志-'*'
    setWindowModified(false);

    // 设置窗口标题,userFriendlyCurrentFile()返回文件名
    setWindowTitle(getFileNameFromPath() + "[*]");
}

// 从文件路径中提取出文件名
QString MdiChild::getFileNameFromPath()
{
    return QFileInfo(curFile).fileName(); // 从文件路径中提取文件名
}

// 保存操作
bool MdiChild::save()
{
    if (isUnsaved_flag)
    { // 如果文件未被保存过,则执行另存为操作
        return saveAs();
    }
    else
    {
        return saveFile(curFile); //否则直接保存文件
    }
}

// 另存为操作
bool MdiChild::saveAs()
{
    // 获取文件路径,如果为空,则返回false
    QString filePath = QFileDialog::getSaveFileName(this, tr("另存为"),curFile);
    if (filePath.isEmpty())
        return false;

    return saveFile(filePath); // 否则保存文件
}

// 保存文件:本质是根据参数-硬盘文件路径,打开硬盘文件然后写入软件上文档数据
bool MdiChild::saveFile(const QString &filePath)
{
    QFile file(filePath);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("多文档编辑器"),
                             tr("无法写入文件 %1:\n%2.")
                             .arg(filePath).arg(file.errorString()));
        return false;
    }

    QTextStream out(&file);
    QApplication::setOverrideCursor(Qt::WaitCursor);
    out << toPlainText(); // 以纯文本文件写入
    QApplication::restoreOverrideCursor();

    setCurrentFile(filePath);
    return true;
}

//关闭事件
void MdiChild::closeEvent(QCloseEvent *event)
{
    if (maybeSave()) { // 如果maybeSave()函数返回true,则关闭窗口
        event->accept();
    } else {   // 用户选择不保存修改,maybeSave返回false,则这个事件会被忽略掉,什么都不做
        event->ignore();
    }
}

// 判断是否需要保存: 在窗口关闭时判断文件是否需要保存,并让用户选择
bool MdiChild::maybeSave()
{
    // 如果文档被更改过,弹出警告框,让用户做出选择
    if (document()->isModified())
    {
        QMessageBox box;
        box.setWindowTitle(tr("多文档编辑器"));
        box.setText(tr("是否保存对“%1”的更改?")
                    .arg(getFileNameFromPath()));
        box.setIcon(QMessageBox::Warning);

        // 添加按钮,QMessageBox::YesRole可以表明这个按钮的行为
        QPushButton *yesBtn = box.addButton(tr("是(&Y)"),QMessageBox::YesRole);

        box.addButton(tr("否(&N)"),QMessageBox::NoRole);
        QPushButton *cancelBtn = box.addButton(tr("取消"),
                                               QMessageBox::RejectRole);
        box.exec(); // 弹出对话框,让用户选择是否保存修改,或者取消关闭操作
        if (box.clickedButton() == yesBtn)  // 如果用户选择是,则返回保存操作的结果
            return save();
        else if (box.clickedButton() == cancelBtn) // 如果选择取消,则返回false
            return false;
    }

    return true; // 如果文档没有更改过,则直接返回true
}

  下面是对上面代码的补充说明:

  • 新建文件函数( newFile() ):在设置窗口标题时添加了“[ * ]”字符,它可以保证编辑器内容被更改后,在相应位置显示“* ”号。而判断编辑器内容是否被更改,可以使用 QTextDocument 类对象的 isModified() 函数获知,这里使用了 QTextEdit 类的 documen() 函数来获取它的 QTextDocument 类对象。然后使用 setWindowModified() 函数设置窗口的更改状态标志。如果参数为true,那么就会在标题中的设置了“[ * ]”号的地方显示“* ”号,表示该文件已经被修改。
  • 加载文件函数( loadFile ):建立 QMessageBox 时使用了tr()函 数,其中的“1%”和“2%”分别可以被后面的 arg() 中的 fileName 和 file.errorString() 代替,这样就可以在字符串中使用变量了。
  • 提取文件名函数( getFileNameFromPath() ):该函数是从文件路径中提取出文件名,这样使标题显得更加清晰和友好,这就是这个函数名的含义。
  • 保存操作函数( save() ): 先使用 isUnsaved_flag 判断文件是否被保存过,如果没有,则要先进行另存为操作,如果已经保存过了,那么直接写入文件就可以了。
  • 另存为操作函数( saveAs() ):先打开文件对话框,若不在"文件名"一栏修改,则会将从标题栏获得的文件名称作为默认保存名称返回,赋给 fileName,如果文件名称(路径)不为空,则会再调用saveFile()。
  • 保存函数( saveFile() ): 这个函数进行文件的写人操作,可以看到,它与 loadFile() 函数是相对应的两个操作。
  • 关闭事件函数( closeEvent() ):这里的关闭事件函数会在窗口被关闭或者调用 close() 函数时执行。若maybeSave()返回true,则会执行 accept() 函数,那么窗口将会关闭, 这里需要说明,其实窗口关闭,默认只是将窗口隐藏起来了,并没有将它销毁掉。但是因为在前面的构造函数中使用了“setAttribute(Qt::WA_DeleteOnClose);”这行代码,所以当关闭窗口时,它会被销毁掉。而如果用户选择不保存修改,maybeSave()返回false,则会执行 ignore() 函数,那么这个事件会被忽略掉,什么都不做。
  • maybeSave()函数:它在窗口关闭时判断文件是否需要保存,并让用户进行选择。另外为了使警告框中的按钮可以显示中文,所以自定义了按钮。
     

  这就是整个操作的过程,读者可以好好分析一下各个函数及其联系,因为以后写相似的应用程序时,这几个操作都是必须的,这里搞明白了,以后直接使用即可。下面对这个类进行简单的测试。

  进入设计模式,在Action编辑器中“新建文件”动作上右击,转到它的触发信号 triggered() 的槽,并更改如下:

// 新建文件菜单
void MainWindow::on_actionNew_triggered()
{
    // 创建MdiChild
    MdiChild *child = new MdiChild;
    // 多文档区域添加子窗口
    ui->mdiArea->addSubWindow(child);
    // 新建文件
    child->newFile();
    // 显示子窗口
    child->show();
}

  在这里新建了子窗口,并且以MdiChild类对象为中心部件,然后新建文件并且显示出来。这里大家要在mainwindow. cpp文件中添加# include "mdichild. h”头文 件。最后运行程序,然后按下Ctrl + N新建文件,并更改内容,然后进行关闭,看一下程序的运行效果。

  自己新加了一个思维导图,如图1-4所示


图1-4 MdiChild类-思维导图

1.3 实现菜单的功能

   上一节建立了自己子窗口的中心部件MdiChild类,它继承自QTextEdit类。 下面便可以使用这个类,来完成主窗口上的各个菜单的功能。
 

1.3.1 更新菜单状态与新建文件操作

  (项目源码路径:srC\1\1-3\myMdi)首先更新菜单状态,使一些菜单在开始时处于不可用状态。然后再更改新建文件的操作。
 

1. 更新菜单状态
  在 mainwindow. h 文件中添加如下代码:

#include <QMainWindow>

//增加类MdiChild的前置声明
class MdiChild;

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_actionNew_triggered();
    void updateMenus();           //更新菜单

private:
    Ui::MainWindow *ui;
    QAction *actionSeparator;    //分隔符
    MdiChild *activeMdiChild(); //活动窗口
};

  上面的 actionSeparator 动作用于创建一个间隔器,将来在“窗口”菜单中显示子窗口列表时,可以用它与前面的菜单动作分隔开。下面在mainwindow. cpp文件中添加代码:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mdichild.h"
#include <QMdiSubWindow>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //在“窗口”菜单中显示子窗口列表时,可以用它与前面的菜单动作“前一个”分隔开。
    actionSeparator = new QAction(this); //创建间隔器动作
    actionSeparator->setSeparator(true); //在其中设置间隔器

    updateMenus();                       //更新菜单
    connect(ui->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)),
            this, SLOT(updateMenus()));           //每当更换活动窗口时,更新菜单状态
}

MainWindow::~MainWindow()
{
    delete ui;
}

// 新建文件菜单
void MainWindow::on_actionNew_triggered()
{
    // 创建MdiChild
    MdiChild *child = new MdiChild;
    // 多文档区域添加子窗口
    ui->mdiArea->addSubWindow(child);
    // 新建文件
    child->newFile();
    // 显示子窗口
    child->show();
}

void MainWindow::updateMenus() //更新菜单
{
    bool hasMdiChild = (activeMdiChild() != nullptr); //是否有活动窗口
    ui->actionSave->setEnabled(hasMdiChild);    //设置各个动作是否可用
    ui->actionSaveAs->setEnabled(hasMdiChild);
    ui->actionPaste->setEnabled(hasMdiChild);
    ui->actionClose->setEnabled(hasMdiChild);
    ui->actionCloseAll->setEnabled(hasMdiChild);
    ui->actionTile->setEnabled(hasMdiChild);
    ui->actionCascade->setEnabled(hasMdiChild);
    ui->actionNext->setEnabled(hasMdiChild);
    ui->actionPrevious->setEnabled(hasMdiChild);
    actionSeparator->setVisible(hasMdiChild);   //设置间隔器是否显示

    bool hasSelection = (activeMdiChild()
                         && activeMdiChild()->textCursor().hasSelection());

    // 有活动窗口且有被选择的文本,剪切复制才可用
    ui->actionCut->setEnabled(hasSelection);
    ui->actionCopy->setEnabled(hasSelection);

    // 有活动窗口且文档有撤销操作
    ui->actionUndo->setEnabled(activeMdiChild()
                          && activeMdiChild()->document()->isUndoAvailable());

    // 有活动窗口且文档有恢复操作
    ui->actionRedo->setEnabled(activeMdiChild()
                          && activeMdiChild()->document()->isRedoAvailable());
}

MdiChild * MainWindow::activeMdiChild() //活动窗口
{
    // 如果有活动窗口,则将其内的中心部件转换为MdiChild类型
    if (QMdiSubWindow *activeSubWindow = ui->mdiArea->activeSubWindow())
        return qobject_cast<MdiChild *>(activeSubWindow->widget());
    return nullptr; // 没有活动窗口,直接返回0
}
  • 构造函数: 初始化了 actionSeparator 动作,然后执行更新菜单函数,并关联多文档区域的活动子窗口信号到更新菜单槽上,每当更换活动子窗口时,都会更新菜单状态。
  • updateMenus(): 在更新菜单函数中根据是否有活动子窗口,设置了各个菜单动作是否可用。这里剪切复制操作和撤销恢复操作的设置还要进行特殊情况的判断。
  • activeMdiChild(): 这个函数中使用了 QMdiArea 类的 activeSubWindow() 函数来获得多文档区域的活动子窗口,然后使用了 T qobjeCt_Cast ( QObject * object ) 函数来进行类型转 换。这个函数是 QObject 类中的函数,它将 object 对象指针转换为T类型的对象指 针,这里将活动窗口的中心部件 QWidget 类型指针转换为 MdiChild 类型指针。这里的T类型必须是直接或者间接继承自QObject类,而且在其定义中要有Q_OB- JECT宏变量。

  现在运行程序,效果如图1-5所示。


图1-5 初始化菜单界面

 

2. 实现新建文件操作
  首先在 mainwindow. h 文件中添加 private slots:

    MdiChild *createMdiChild(); //创建子窗口

  然后在 mainwindow. cpp 文件中添加该槽的定义:

MdiChild * MainWindow::createMdiChild() //创建子窗口部件
{
    MdiChild *child = new MdiChild; //创建MdiChild部件
    ui->mdiArea->addSubWindow(child); //向多文档区域添加子窗口,child为中心部件
    connect(child,SIGNAL(copyAvailable(bool)),ui->actionCut,
            SLOT(setEnabled(bool)));

    // 根据QTextEdit类的是否可以复制信号设置剪切复制动作是否可用
    connect(child,SIGNAL(copyAvailable(bool)),ui->actionCopy,
            SLOT(setEnabled(bool)));

    // 根据QTextDocument类的是否可以撤销恢复信号设置撤销恢复动作是否可用
    connect(child->document(),SIGNAL(undoAvailable(bool)),
            ui->actionUndo,SLOT(setEnabled(bool)));
    connect(child->document(),SIGNAL(redoAvailable(bool)),
            ui->actionRedo,SLOT(setEnabled(bool)));

    return child;
}

  在这个函数中创建了 MdiChild 部件,并将它作为子窗口的中心部件,然后添加到多文档区域。下面关联了编辑器的信号和我们的菜单动作,让它们可以随着文档 的改变而改变状态。最后返回了 MdiChild 对象指针。这里之所以要添加这样一个 函数,是因为在下面的打开操作中还要使用到这个函数中的功能,所以将它们从新建 文件菜单的触发信号槽中提取出来,另写了这样一个函数。下面更改在上一节中添加的新建文件菜单的触发信号槽:

// 新建文件菜单
void MainWindow::on_actionNew_triggered()
{
    MdiChild *child = createMdiChild();    //创建MdiChild
    child->newFile();                      //新建文件
    child->show();                         //显示子窗口
}

  因为添加子窗口的操作放到了 createMdiChild() 函数中进行,这里只需要调用这个函数就可以了。现在运行程序添加新文件,然后编辑,选中一些字符,可以看到工具栏中"剪切"、"复制"不再是灰色的,如图1-6所示。但是,因为现在还没有实现这些动作的功能,所以它们并不可用。


图1-6 新建文件后菜单状态

1.3.2 实现文件打开操作

  现在来实现打开文件菜单的功能。当要打开一个文件时,要先判断这个文件是否已经被打开了,这样就需要遍历多文档区域子窗口中的文件,如果发现该文件已经打开,则直接设置该子窗口为活动窗口;否则直接加载要打开的文件,并添加新的子窗口。
  (项目源码路径:src\1\1 - 5\myMdi)首先在 mainwindow. h 文件中先添加类的前置声明 class QMdiSubWindow;然后添加 private 函数声明:

    QMdiSubWindow *findMdiChild(const QString &filePath); // 查找子窗口

  再添加私有槽声明private slots:

    void setActiveSubWindow(QWidget *window); // 设置活动子窗口

  现在从设计模式进入“打开文件”动作的触发信号 triggered() 的槽,更改如下:

void MainWindow::on_actionOpen_triggered() // 打开文件菜单
{
    QString filePath = QFileDialog::getOpenFileName(this); // 获取文件路径
    if (!filePath.isEmpty())
    {
        // 如果路径不为空,则查看该文件是否已经打开
        QMdiSubWindow *existing = findMdiChild(filePath);
        if (existing)
        {
            // 如果已经存在,则将对应的子窗口设置为活动窗口
            ui->mdiArea->setActiveSubWindow(existing);
            return;
        }

        MdiChild *child = createMdiChild(); // 如果没有打开,则新建子窗口
        if (child->loadFile(filePath))
        {
            ui->statusBar->showMessage(tr("打开文件成功"), 2000);
            child->show();
        }
        else
        {
            child->close();
        }
    }
}

  下面是査找子窗口函数的实现:

QMdiSubWindow * MainWindow::findMdiChild(const QString &fileName) // 查找子窗口
{
    QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();

    // 利用foreach语句遍历子窗口列表,如果其文件路径和要查找的路径相同,则返回该窗口
    foreach (QMdiSubWindow *window, ui->mdiArea->subWindowList()) {
        MdiChild *mdiChild = qobject_cast<MdiChild *>(window->widget());
        if (mdiChild->currentFile() == canonicalFilePath)
            return window;
    }
    return nullptr;
}

  这个函数中使用了 foreach 语句来遍历整个多文档区域的所有子窗口,这个函数在《Qt Creator快速入门》的第7章容器类部分讲到。下面是设置活动窗口的实现:

void MainWindow::setActiveSubWindow(QWidget *window) // 设置活动子窗口
{
    if (!window) // 如果传递了窗口部件,则将其设置为活动窗口
        return;
    ui->mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow *>(window));
}

  这个函数的作用就是将传递过来的窗口部件设置为活动窗口。

1.3.3 添加子窗口列表

  现在为窗口菜单添加显示子窗口列表的功能。我们想每添加一个子窗口就可以在窗口菜单中罗列出它的文件名,而且可以在这个列表中选择一个子窗口,将它设置为活动窗口。这个看似很好实现,只要为窗口菜单添加菜单动作,然后关联这个动作的触发信号到设置活动窗口槽上就可以了。但是,如果有很多个子窗口怎么办,难道要一个一个进行关联吗,那怎么获知是哪个动作?其实,Qt中提供了一个信号映射器QSignalMapper类,它可以实现对多个相同部件的相同信号进行映射,为其添加字符串或者数值参数,然后再发射出去。
  (项目源码路径:src\1\1 - 5\myMdi)首先在 mainwindow. h 文件中添加类的前置声明 class QSignalMapper;然后添加私有对象指针 private:

QSignalMapper *windowMapper;    // 信号映射器

  再添加私有槽声明 private slots:

void updateWindowMenu();    // 更新窗口菜单

  下面到 mainwindow. cpp 文件中添加代码。首先添加 #include 头文件,然后在 MainWindow 的构造函数中添加如下代码 (注意在最新的Qt中,QSignalMapper 这个类已经被弃用了)

    //注意在最新的Qt中,QSignalMapper 这个类已经被弃用了,没有删除是为了维护老代码
    windowMapper = new QSignalMapper(this); // 创建信号映射器
    connect(windowMapper, SIGNAL(mapped(QWidget*)), // 映射器重新发射信号
            this, SLOT(setActiveSubWindow(QWidget*))); // 设置活动窗口
    updateWindowMenu();
    // 更新窗口菜单,并且设置当窗口菜单将要显示的时候更新窗口菜单
    connect(ui->menuW,SIGNAL(aboutToShow()),this,SLOT(updateWindowMenu()));

  上面创建了信号映射器,并且将它的mappedO信号关联到设置活动窗口槽上。然后更新窗口菜单,并且将窗口菜单的将要显示信号关联到我们的更新菜单梢上,这样每当窗口菜单要显示时都会更新窗口菜单,更新窗口菜单的代码如下:

void MainWindow::updateWindowMenu() // 更新窗口菜单
{
    ui->menuW->clear(); // 先清空菜单,然后再添加各个菜单动作
    ui->menuW->addAction(ui->actionClose);
    ui->menuW->addAction(ui->actionCloseAll);
    ui->menuW->addSeparator();
    ui->menuW->addAction(ui->actionTile);
    ui->menuW->addAction(ui->actionCascade);
    ui->menuW->addSeparator();
    ui->menuW->addAction(ui->actionNext);
    ui->menuW->addAction(ui->actionPrevious);
    ui->menuW->addAction(actionSeparator);

    QList<QMdiSubWindow *> windows = ui->mdiArea->subWindowList();
    actionSeparator->setVisible(!windows.isEmpty());
    // 如果有活动窗口,则显示间隔器
    for (int i = 0; i < windows.size(); ++i)
    {
        // 遍历各个子窗口
        MdiChild *child = qobject_cast<MdiChild *>(windows.at(i)->widget());

        QString text;
        if (i < 9)
        {
            // 如果窗口数小于9,则设置编号为快捷键
            text = tr("&%1 %2").arg(i + 1)
                               .arg(child->getFileNameFromPath());
        }
        else
        {
            text = tr("%1 %2").arg(i + 1)
                              .arg(child->getFileNameFromPath());
        }
        QAction *action  = ui->menuW->addAction(text); // 添加动作到菜单
        action->setCheckable(true); // 设置动作可以选择

        // 设置当前活动窗口动作为选中状态
        action ->setChecked(child == activeMdiChild());

        // 关联动作的触发信号到信号映射器的map()槽函数上,这个函数会发射mapped()信号
        connect(action, SIGNAL(triggered()), windowMapper, SLOT(map()));

        // 将动作与相应的窗口部件进行映射,在发射mapped()信号时就会以这个窗口部件为参数
        windowMapper->setMapping(action, windows.at(i));

    }
}

  更新窗口菜单函数中,先清空了窗口菜单动作,然后再动态添加。这里遍历了多文档区域的各个子窗口,然后以它们中的文件名为文本创建了动作,并将这些动作添加到窗口菜单中。我们将动作的触发信号关联到信号映射器的 map() 槽上,然后设置了动作与其对应的子窗口之间的映射,这样触发菜单时就会执行 map() 函数,而它又会发射 mapped() 信号,这个 mapped() 函数会以子窗口部件为参数,因为在构造函数中设置了这个信号与 setActiveSubWindow() 函数的关联,所以最终会执行设置活动子窗口函数,并且设置选择的动作指定的子窗口为活动窗口。这时运行程序,效果如图1 - 7所示。


图1-7 子窗口列表

1.3.4 实现其它功能

  下面来实现其他一些菜单的功能。因为在前面已经把核心的功能都实现了,而且像剪切、复制等常用功能,QTextEdit类已经提供了,所以这里只需要调用相应的函数即可。
  (项目源码路径:src\1\1-6\myMdi)只需要在设计模式,进入相应动作的触发信号 triggered() 的槽,然后添加代码即可。下面是保存、另存为、撤销等菜单动作的触发信号槽函数代码:

void MainWindow::on_actionSave_triggered() // 保存菜单
{
    if(activeMdiChild() && activeMdiChild()->save())
        ui->statusBar->showMessage(tr("文件保存成功"),2000);
}

void MainWindow::on_actionSaveAs_triggered()  // 另存为菜单
{
    if(activeMdiChild() && activeMdiChild()->saveAs())
        ui->statusBar->showMessage(tr("文件保存成功"),2000);
}

void MainWindow::on_actionUndo_triggered() // 撤销菜单
{
    if(activeMdiChild()) activeMdiChild()->undo();
}

void MainWindow::on_actionRedo_triggered() // 恢复菜单
{
    if(activeMdiChild()) activeMdiChild()->redo();
}

void MainWindow::on_actionCut_triggered() // 剪切菜单
{
    if(activeMdiChild()) activeMdiChild()->cut();
}

void MainWindow::on_actionCopy_triggered() // 复制菜单
{
    if(activeMdiChild()) activeMdiChild()->copy();
}

void MainWindow::on_actionPaste_triggered() // 粘贴菜单
{
    if(activeMdiChild()) activeMdiChild()->paste();
}

void MainWindow::on_actionClose_triggered() // 关闭菜单
{
    ui->mdiArea->closeActiveSubWindow();
}

void MainWindow::on_actionCloseAll_triggered() // 关闭所有窗口菜单
{
    ui->mdiArea->closeAllSubWindows();
}

void MainWindow::on_actionTile_triggered() // 平铺菜单
{
    ui->mdiArea->tileSubWindows();
}

void MainWindow::on_actionCascade_triggered() // 层叠菜单
{
    ui->mdiArea->cascadeSubWindows();
}

void MainWindow::on_actionNext_triggered() // 下一个菜单
{
    ui->mdiArea->activateNextSubWindow();
}

void MainWindow::on_actionPrevious_triggered() // 前一个菜单
{
    ui->mdiArea->activatePreviousSubWindow();
}

void MainWindow::on_actionAbout_triggered() // 关于菜单
{
    QMessageBox::about(this,tr("关于本软件"),tr("欢迎访问我们的网站:www.yafeilinux.com"));
}

void MainWindow::on_actionAboutQt_triggered() // 关于Qt菜单
{
    qApp->aboutQt(); // 这里的qApp是QApplication对象的全局指针,
                     // 这行代码相当于QApplication::aboutQt();
}

1.4 完善程序功能

  应用程序在实现了主要的功能后,还要进行一些必要的设置,使它成为一个完善的应用程序。下面进行几个方面的优化。
 

1.4.1 保存窗口设置

  我们都希望自己的应用程序很友好,那么能保存用户对窗口的一些设置(比如大小、位置)等就显得很必要了。Qt中的 QSettings 类提供了平台无关的永久保存应用程序设置的方法。
  (项目源码路径:src\1\1 - 7\myMdi)首先在 mainwindow. h 文件中添加 private 函数声明:

    void readSetting();    //读取窗口设置
    void writeSetting();    //写入窗口设置

  然后再添加 protected 函数声明:

    void closeEvent(QCloseEvent *event);  // 关闭事件

  然后到 mainwindow. cpp 添加代码。先添加头文件:

#include <QSettings>
#include <QCloseEvent>

  然后在 MainWindow 类的构造函数中添加代码:

    readSettings(); // 初始窗口时读取窗口设置信息

  下面是关闭事件处理函数的定义:

void MainWindow::closeEvent(QCloseEvent *event) // 关闭事件
{
    ui->mdiArea->closeAllSubWindows(); // 先执行多文档区域的关闭操作
    if (ui->mdiArea->currentSubWindow()) {
        event->ignore(); // 如果还有窗口没有关闭,则忽略该事件
    } else {
        writeSettings(); // 在关闭前写入窗口设置
        event->accept();
    }
}

  可以看到我们是在构造窗口时进行窗口设置的读取,在窗口的关闭事件中进行 窗口设置的写入的。下面是窗口设置的写人和读取函数:

void MainWindow::writeSettings() // 写入窗口设置
{
    QSettings settings("yafeilinux", "myMdi");
    settings.setValue("pos", pos());   // 写入位置信息
    settings.setValue("size", size()); // 写入大小信息
}

void MainWindow::readSettings() // 读取窗口设置
{
    QSettings settings("yafeilinux", "myMdi");
    QPoint pos = settings.value("pos", QPoint(200, 200)).toPoint();
    QSize size = settings.value("size", QSize(400, 400)).toSize();
    move(pos);
    resize(size);
}

  这两个函数的使用这里不再详细介绍。下面从设计模式进入“退出”菜单动作的 触发信号槽,更改如下:

void MainWindow::on_actionExit_triggered() // 退出菜单
{
    qApp->closeAllWindows(); // 等价于QApplication::closeAllWindows();
}

  这里使用了 qApp 指针,它是 QApplication 对象的全局指针,因为在一个应用程序中只能定义一个 QApplication 对象。现在运行程序,改变窗口位置和大小,然后关闭程序,重新运行,测试一下运行效果。

1.4.2 自定义右键菜单

  现在运行程序时会发现子窗口中的右键菜单是英文的,需要将它改为中文。下面来定义右键菜单,这个需要在 MdiChild 类中进行,就是重新实现 QTextEdit 类的上下文菜单事件。首先在 mdichild. h 文件中添加 protected 函数声明:

void contextMenuEvent(QContextMenuEvent * e) ;  // 右键菜单事件

  然后到 mdichild. cpp 文件中添加头文件 #include ,再进行函数的定义:

  在这个函数中创建了一个菜单,然后给它添加了一些动作;在添加动作时,直接为这些动作 指定了触发信号triggeredO的槽。在为这些动 作添加快捷键时,我们使用了 QKeySequence类 提供的默认操作快捷键。这里还为一些动作设 置了是否可用的条件。最后在鼠标指针位置弹 出菜单,并在执行结束时销毁这个菜单。运行程序,测试一下效果,如图1-8所示。


图1-8 自定义右键菜单

1.4.3 其他功能

  最后还想要在状态栏中可以显示编辑器中光标所在的行号和列号,然后设置窗口的标题和状态栏的一些显示。首先在 mainwindow. h 文件中添加私有槽的声明 private slots:

void showTextRowAndCol() j  //显示文本的行号和列号

  然后添加一个私有函数的声明private:

    void initWindow();  // 初始化窗口

  先来看显示光标位置函数的定义:

void MainWindow::showTextRowAndCol() // 显示文本的行号和列号
{
    // 如果有活动窗口,则显示其中光标所在的位置
    if(activeMdiChild()){

        // 因为获取的行号和列号都是从0开始的,所以我们这里进行了加1
        int rowNum = activeMdiChild()->textCursor().blockNumber()+1;
        int colNum = activeMdiChild()->textCursor().columnNumber()+1;

        ui->statusBar->showMessage(tr("%1行 %2列")
                                   .arg(rowNum).arg(colNum),2000);
    }
}

  在这个函数中获取了活动窗口中光标的位置,并在状态栏中显示,为了每次编辑 器中的光标位置变化时都可以调用这个函数,需要在 createMdiChild() 函数中的“return child;”一行代码前添加一行代码:

    // 每当编辑器中的光标位置改变,就重新显示行号和列号
    connect(child,SIGNAL(cursorPositionChanged()),this,SLOT(showTextRowAndCol()));

  下面来看初始化窗口函数。首先添加头文件 #include ,然后在 MainWindow 类的构造函数中调用这个函数,即在最后添加代码:

    initWindow(); // 初始化窗口

  然后进行该函数的定义:

void MainWindow::initWindow() // 初始化窗口
{
    setWindowTitle(tr("多文档编辑器"));

    // 我们在工具栏上单击鼠标右键时,可以关闭工具栏
    ui->mainToolBar->setWindowTitle(tr("工具栏"));

    // 当多文档区域的内容超出可视区域后,出现滚动条
    ui->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    ui->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);

    ui->statusBar->showMessage(tr("欢迎使用多文档编辑器"));

    QLabel *label = new QLabel(this);
    label->setFrameStyle(QFrame::Box | QFrame::Sunken);
    label->setText(
          tr("<a href=\"http://www.yafeilinux.com\">yafeilinux.com</a>"));
    label->setTextFormat(Qt::RichText); // 标签文本为富文本
    label->setOpenExternalLinks(true);  // 可以打开外部链接
    ui->statusBar->addPermanentWidget(label);

    ui->actionNew->setStatusTip(tr("创建一个文件"));

    ui->actionOpen->setStatusTip(tr("打开一个已经存在的文件"));
    ui->actionSave->setStatusTip(tr("保存文档到硬盘"));
    ui->actionSaveAs->setStatusTip(tr("以新的名称保存文档"));
    ui->actionExit->setStatusTip(tr("退出应用程序"));
    ui->actionUndo->setStatusTip(tr("撤销先前的操作"));
    ui->actionRedo->setStatusTip(tr("恢复先前的操作"));
    ui->actionCut->setStatusTip(tr("剪切选中的内容到剪贴板"));
    ui->actionCopy->setStatusTip(tr("复制选中的内容到剪贴板"));
    ui->actionPaste->setStatusTip(tr("粘贴剪贴板的内容到当前位置"));
    ui->actionClose->setStatusTip(tr("关闭活动窗口"));
    ui->actionCloseAll->setStatusTip(tr("关闭所有窗口"));
    ui->actionTile->setStatusTip(tr("平铺所有窗口"));
    ui->actionCascade->setStatusTip(tr("层叠所有窗口"));
    ui->actionNext->setStatusTip(tr("将焦点移动到下一个窗口"));
    ui->actionPrevious->setStatusTip(tr("将焦点移动到前一个窗口"));
    ui->actionAbout->setStatusTip(tr("显示本软件的介绍"));
    ui->actionAboutQt->setStatusTip(tr("显示Qt的介绍"));
}

  这里设置了窗口的标题和工具栏的标题,然后为多文档区域设置了滚动条,添加了网站的链接。最后设置了各个动作的状态提示信息,将鼠标移动到这些动作上时, 在状态栏会显示这些提示信息。最终运行效果如图1 -9 所示。


图1-9 状态栏提示信息

  到这里,整个程序就设计完成了。可以按照《Qt Creator快速人门》的第2章的内容给这个程序添加程序图标,然后以 Release 的方式编译程序,最后打包发布。

1.5 小结

  这个多文档编辑器程序只是实现了编辑器的一些最基本功能,还有很多功能没能实现,比如富文本的处理、打印和査找等。不过作为初学者的第一个综合实例,它 已经包含了太多的知识点,如果要做一个功能强大的编辑器,那么这一章也许要写几百页。如果还有兴趣扩展这个程序,可以看一下Qt中提供的 Text Edit 示例,或者去qter网站上査看多文档编辑器开源软件,那个程序更加综合。

  读者应该认真学习这一章,不仅是学习其中的知识点,更多的是学习一种方法, 编写综合程序的方法。可以看到,我们程序的功能是一点一点加上去的,再庞大的程序也是将功能模块一个个加上去的,不要设想一下子就写出一个功能强大的应用程序。而且在程序编写过程中,一定会出现各种问题,不要气馁,不要烦躁,因为这是正常现象,学会多使用qDebug()函数。

[Qt及Qt Quick开发实战精解] 第1章 多文档编辑器的更多相关文章

  1. 《Cocos2d-x游戏开发实战精解》学习笔记4--实战一个简单的钢琴

    上一节学习了使用Cocos2d-x播放音乐的方法,但是那种方法一般只适合于播放较大的音乐,而一般比较短小的音乐(如游戏中的打斗.按键音效等)则要通过playEffect来播放.本节使用该方法以及之前学 ...

  2. 《Cocos2d-x游戏开发实战精解》学习笔记3--在Cocos2d-x中播放声音

    <Cocos2d-x游戏开发实战精解>学习笔记1--在Cocos2d中显示图像 <Cocos2d-x游戏开发实战精解>学习笔记2--在Cocos2d-x中显示一行文字 之前的内 ...

  3. 《Cocos2d-x游戏开发实战精解》学习笔记1--在Cocos2d中显示图像

    Cocos2d-x中的图像是通过精灵类来显示的.在Cocos2d-x中游戏中的每一个角色.怪物.道具都可以理解成是一个精灵,游戏背景作为一种特殊的单位将其理解成是一个精灵也没有什么不妥.在源文件本章目 ...

  4. Python开发技术详解(视频+源码+文档)

    Python, 是一种面向对象.直译式计算机程序设计语言.Python语法简捷而清晰,具有丰富和强大的类库.它常被昵称为胶水语言,它能够很轻松的把用其他语言制作的各种模块(尤其是C/C++)轻松地联结 ...

  5. QT开发实战精解

    无法打开包括文件<QApplication> No such file or directory  这一问题 解决办法,使用QApplication时必须在项目pro文件中添加一句 QT ...

  6. 《Cocos2d-x游戏开发实战精解》学习笔记2--在Cocos2d-x中显示一行文字

    在Cocos2d-x中要显示文字就需要用到Label控件.在3.x版本的Cocos2d中,舍弃了之前版本所使用的LabelTTF.LabelAtlas.LabelBMFont 3个用于显示文字的类,而 ...

  7. 《React 与 Redux 开发实例精解》出版了!

    <React 与 Redux 开发实例精解>出版了! <React 与 Redux 开发实例精解>出版了! 关于 React 与 Redux React 与 Redux, 一个 ...

  8. 《Android NFC 开发实战详解 》简介+源码+样章+勘误ING

    <Android NFC 开发实战详解>简介+源码+样章+勘误ING SkySeraph Mar. 14th  2014 Email:skyseraph00@163.com 更多精彩请直接 ...

  9. 《Node.js开发实战详解》学习笔记

    <Node.js开发实战详解>学习笔记 ——持续更新中 一.NodeJS设计模式 1 . 单例模式 顾名思义,单例就是保证一个类只有一个实例,实现的方法是,先判断实例是否存在,如果存在则直 ...

随机推荐

  1. PHP 策略模式

    策略模式:定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换.本模式使得算法可独立于使用它的客户而变化.策略模式把对象本身和运算规则区分开来,其功能非常强大,因为这个设计模式本身的核心思想 ...

  2. nth-of-type在选择class的时候需要注意的一个小问题

    查了下w3和MDN的手册,没发现有这个说明,写篇随笔记下. 1..class:nth-of-type(n)在选择class的时候,如果在class前面插入x个同类型标签,n需要加上x <!DOC ...

  3. 什么是ODBO---OLE DB for OLAP

      我怎么一步一步追到ODBO了?   mondrian核心api->olap4j->jedox也在用olap4j->ODBO? ODBO是什么呢? OLE DB for OLAP ...

  4. Xamarin iOS开发中的编辑、连接、运行

    Xamarin iOS开发中的编辑.连接.运行 创建好工程后,就可以单击Xamarin Studio上方的运行按钮,如图1.37所示,对HelloWorld项目进行编辑.连接以及运行了.运行效果如图1 ...

  5. Hibernate第四篇【集合映射、一对多和多对一】

    前言 前面的我们使用的是一个表的操作,但我们实际的开发中不可能只使用一个表的-因此,本博文主要讲解关联映射 集合映射 需求分析:当用户购买商品,用户可能有多个地址. 数据库表 我们一般如下图一样设计数 ...

  6. 《JavaScript Dom 编程艺术》读书笔记-第6章

    本章继续对图片库进行改进,主要内容包括三个方面: 1. 把事件处理函数移出文档: 2. 向后兼容: 3. 确保可访问. 之前代码的HTML片段,此时如果浏览器不支持JS,图片显示也能正常工作,只是体验 ...

  7. chart.js &amp; canvas

    chart.js & canvas https://www.chartjs.org/samples/latest/ https://www.chartjs.org/samples/latest ...

  8. Cookie和Session的区别?

    1.Cookie和Session都是会话技术,Cookie是运行在客户端,Session是运行在服务器端.      2.Cookie有大小限制以及浏览器在存cookie的个数也有限制,Session ...

  9. MVC FormCollection 无法获取值的问题

     把action定义为[HttpPost],并且ajax.beginform中ajaxoption中定义为Post,在提交表单时就可以获取FormCollection的值了.httpGet或者后台不定 ...

  10. window10下Docker安装

    首先window版本必须是10,如果是win7那么安装方法有所不同,win10是官方支持安装的.笔者安装的是Community社区版,版本信息如下: 1.去docker官网下载win10安装包: ht ...