8.8 多文档

在每一个主窗口中只提供一个文档的应用程序被称为单文档界面( SDI)应用程序。基 于 SDI 的应用程序只提供了一个单一主窗口,并且在同一时间只能处理一个文档。如果想 让它在同一时间具有处理多个文档的能力,就需要同时启动多个应用程序实例。但是这对于 用户来讲是很不方便的。针对这种情况,我们可以使用基于多文档界面( MDI)的应用程 序,它也只有一个主窗口,但可以产生和管理多个文档窗口。基于多文档的应用程序也是很 常见的,比如 FireFox 浏览器就是典型的例子,它允许用户在同一时间内打开多个浏览器 窗口。

Qt 可以在它所支持的所有平台上创建 SDI 和 MDI 应用程序。 根据笔者的经验,在通常的工程项目中,使用 SDI 的应用程序占到了大多数的比例,但我们仍然需要对 MDI 有所了解。

8.8.1 创建多文档

在 Qt4 中创建 MDI 应用程序主要有以下方法:

1. 多实例实现主窗口的多文档

在一个应用程序中实例化多个主窗口,即当打开或新建文档的时候,文本编辑器应用 程序新建一个主窗口,这个主窗口单独加载和编辑文档。这种情况下,多个主窗口属于同一 个应用程序,当关闭所有的主窗口的时候,文本编辑器应用程序也就结束了运行。这种方法 称为多实例实现主窗口的多文档。

下面将修改应用程序,以使它可以处理多个文档。首先,需要对 File 菜单做一些简单 改动:

  • 利用 File→New 创建一个空文档窗口,而不是再次使用已经存在的主窗口。
  • 利用 File→Close 关闭当前主窗口。
  • 利用 File→Exit 关闭所有窗口。

在 File 菜单的最初版本中,并没有 Close 选项,这只是因为当时它还和 Exit 一样具 有相同的功能。新的 File 菜单如图 8-27 所示。

新的 main()函数为:

int main(int argc,char *argv[])
{
    QApplication app(argc,argv); MainWindow *mainWin = new MainWindow; mainWin->show();
    return app.exec();
}

具有多窗口功能后,现在就需要使用菜单中的 new 来创建 MainWindow。考虑到节省内 存,可以再工作完成之后使用 delete 操作来删除主窗口。

这是新的 MainWindow::newFile()槽:

void MainWindow::newFile()
{
    MainWindow *mainWin = new MainWindow; mainWin->show();
}

我们只创建了一个新的 MainWindow 实例。这看起来有些奇怪,因为没有保留指向这个 新窗口的任何指针,但实际上这并不是什么问题,因为 Qt 会对所有的窗口进行跟踪。

以下是用于 Close 和 Exit 的动作:

void MainWindow::createActions()
{
    ...
    closeAct = new QAction(tr("Cl&ose"), this); closeAct->setShortcut(tr("Ctrl+F4"));
    closeAct->setStatusTip(tr("Close the active window"));
    connect(closeAct, SIGNAL(triggered()),mdiArea, SLOT(closeActiveSubWindow()));
    exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcut(tr("Ctrl+Q"));
    exitAct->setStatusTip(tr("Exit the application"));
    connect(exitAct, SIGNAL(triggered()), qApp, SLOT(closeAllWindows()));
}

到此,我们就实现了从 SDI 到 MDI 的“转变”,组合好你的工程文件,编译、链接、 运行程序即可。

小贴士:在 Mac OS X 系统中,通常采用这种方法实现 MDI 应用程序。

2.使用 QWorkSpace

使用 QWorkSpace 作为主窗口的中心部件,在 QWorkspace 中打开多个子窗口,每一个 子窗口可以单独对文档进行加载和编辑。

QWorkspace 类继承自 QWidget 类,利用它可以很方便的实现多文档的应用。使用它的 方法有如下步骤:

第 1 步,在主窗口的头文件(如 mainwindow.h)中加入 QWorkspace 类的头文件,代码 如下:

#include <QWorkspace>

也可以采用类的前置声明,代码如下:

class QWorkspace;

第 2 步,在主窗口头文件中声明一个 QWorkspace 类对象,代码如下:

QWorkspace *workspace;

第 3 步,在构造函数中实例化该对象,并把该对象设置为主窗口的中心窗口部件,代 码如下:

MainWindow::MainWindow()
{
    workspace = new QWorkspace; setCentralWidget(workspace);
    ...
}

第 4 步,创建多个子窗口,并创建它们各自的中心窗口部件 。

    QMainWindow *window1 = new QMainWindow; 
    window1->setWindowTitle(tr("window 1")); 
    QTextEdit *textEdit1 = new QTextEdit; 
    textEdit1->setText(tr("Window 1")); 
    window1->setCentralWidget(textEdit1);

    QMainWindow *window2 = new QMainWindow;
    window2->setWindowTitle(tr("window 2")); 
    QTextEdit * textEdit2 = new QTextEdit; 
    textEdit2->setText(tr("Window 2")); 
    window2->setCentralWidget(textEdit2);

    QMainWindow *window3 = new QMainWindow; 
    window3->setWindowTitle(tr("window 3")); 
    QTextEdit * textEdit3 = new QTextEdit;

    textEdit3->setText(tr("Window 3")); 
    window3->setCentralWidget(textEdit3);

第 5 步,在 workSpace 对象中加入这几个子窗口,对它们进行管理,代码如下:

workSpace->addWindow(window1); 
workSpace->addWindow(window2); 
workSpace->addWindow(window3);

第 6 步,组织好你的工程文件,编译、链接、运行程序即可。 完整的示例程序在本章目录下的 workspace 文件夹下面。

小贴士:在 Qt4.5 版推出以后,不推荐采用上述方法来创建 MDI 应用程序。来自 Qt 的 官方说法是,QWorkspace 是被废弃的类,它的存在就是为了使采用以前版本的 Qt 开发的程 序能够正常运行。所以,如果你使用的是 Qt4.5 及以后的版本,我们强烈建议你使用 QMdiArea 来创建 MDI 应用程序。

3.使用 QMdiArea

这种方法的核心主要是掌握两个类的用法: QMdiArea 和 QMdiSubWindow,前者主要用 于创建程序主窗口的中心窗口部件,后者用于创建主窗口的各个子窗口。具体的做法是把 QMdiArea 类的实例作为主窗口的中心部件,把 QMdiSubWindow 类的实例作为子窗口,并由 QMdiArea 实现对多个子窗口的管理。

QMdiArea 类继承自 QAbstractScrollArea,它是 Qt 4.3 以后新增加的类。在创建 MDI 应用程序时,QMdiArea 类的实例通常被用作主窗口的中心窗口部件,但也可以被放置于一 个布局中。实际上,QMdiArea 是 MDI 应用程序的窗口管理器。它建立、绘制、管理在它之 上的子窗口,并可采用层叠或者平铺的方式排列它们。

QMdiSubWindow 继承自 QWidget,它的作用是为 QMdiArea 创建子窗口。它代表了在 QMdiArea 中创建的顶层窗口。它主要包含一个标题栏、一个内部窗口( Internal Widget)、一个窗口框架和一个大小控制手柄。 QMdiSubWindow 有自己的布局(Layout), 在其中包含窗口标题栏以及内部窗口的中心窗口区域。一个典型的 QMdiSubWindow 实例如 图 8-27 所示。

图 8-27 QMdiSubWindow 子窗口

听了上面的介绍,大家是不是有点“晕”呢?别着急,请看图 8-28,它示意了采用这 种方法创建 MDI 应用程序的窗口布局以及创建的方法。图中的主窗口是子类化 QMainWindow 类创建的;主窗口的中心窗口部件使用 QMdiArea 的实例创建;子窗口是子类化 QWidget 实 现的,而子窗口的内部窗口部件是使用 QMdiSubWindow 的实例创建的。

它们之间的关系总结如下:QMdiArea 是所有子窗口的容器和管理器,QMdiArea 中的子 窗口都是 QMdiSubWindow 类的实例。我们通过 addSubWindow()方法把它们加入到 MDI 应用 程序中。使用时,通常先建立一个 QWidget 或其子类的实例,然后把它作为参数调用 addSubWindow()函数,addSubWindow()函数将把它作为子窗口的内部窗口,并填充中心窗口 区域。由于 QMdiSubWindow 是 QWidget 的子类,所以你可以像使用以前我们介绍过的常见 顶层窗口那样使用它,如可以调用基类 QWidget 的 show(), hide(), showMaximized(), 以及 setWindowTitle()等方法对窗口实例进行设置。

看着这张图再对照笔者上面的讲解,应该就很清楚了吧。

图 8-28 使用 QMdiArea 类创建 MDI 时的窗口框架

小贴士:为 QMdiSubWindow 创建内部窗口有两种方法,一种是调用 addSubWindow(widget),其中 widget 参数将作为内部窗口部件;另一种是先创建一个继承 自 QWidget 的窗口实例,然后调用 setWidget ( QWidget * widget )方法,把 widget 作为子 窗口的内部窗口部件即可,这个内部窗口部件将被显示在子窗口的中心区域。注意, QMdiArea 会对其内部的子窗口进行管理,你不必使用代码显式的管理它们。

QMdiSubWindow 还有许多专门对应 MDI 应用类型而设置的方法和属性,大家可以在 Qt Assistant 中获得详尽的介绍。

好了,我们详细解说一下如何采用 QMdiArea 和 QMdiSubWindow 类来创建 MDI 应用程 序,这个例子在本章目录 mdi 文件夹下面。

第 1 步,包含用到的类

在主窗口的头文件(如 mainwindow.h)中包含程序中使用到的类,有两种方法,一是可 以加入头文件;二是当情况比较简单时,也可以采用类的前置声明。

加入头文件:

#include <QMdiArea>
#include <QMdiSubWindow>
...

或者采用类前置声明:

class QMdiArea; 
class QMdiSubWindow;
...

第 2 步,声明一个 QMdiArea 类对象

在主窗口的头文件中声明一个 QMdiArea 类对象,在后面还需要声明一个你的子窗口类 的对象,代码如下:

QMdiArea *mdiArea;
...
MdiChild *child;//声明子窗口类的对象
...

第 3 步,设置中心窗口部件

在主窗口类的实现文件中(如 mainwindow.cpp,通常在其构造函数中)实例化该对 象,并把它设置为主窗口的中心窗口部件,代码如下:

MainWindow::MainWindow()
{
    mdiArea = new QMdiArea; setCentralWidget( mdiArea );
    ...
}

第 4 步,创建子窗口

新建一个子窗口类,它可派生自 QWidget 或其子类,比如 QTextEdit。这个类的实例将 作为子窗口的内部窗口部件。这个子窗口类的创建与我们前面讲到的子类化对话框和子类化 QWidget 的方法相同,只是它没有菜单栏、工具栏和状态栏。

另外记得在主窗口的头文件中加入该子窗口类的声明。

第 5 步,实例化子窗口类,并使用 QMdiArea 对它进行管理,代码如下:

child = new MdiChild;
QMdiSubWindow *subWindow = mdiArea->addSubWindow(child); subWindow->show();
...

小贴士:QMdiArea::addSubWindow()函数创建一个新的 QMdiSubWindow,把作为参数传递的 该窗口部件放进子窗口中,并且返回该子窗口。最后一行代码调用 show()方法,使该子窗口可见。

第 6 步,创建并显示子窗口

这通常是在用户点击 File->NewFile 时完成的,代码如下:

void MainWindow::newFile()
{
    child->newFile(); child->show();
}

整个程序的实现过程可以用图描述。

图 8-29 程序的实现过程

小贴士:在 MDI 应用程序中,主窗口类并不需要对文档进行具体处理,这些工作是在子窗 口类中完成的,相当于在 SDI 应用程序中实现的文档处理功能。

第 7 步,创建 main.cpp 文件 这步没有什么好说的,代码如下:

#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
    Q_INIT_RESOURCE(mdi);//使用 Qt 资源系统
    QApplication app(argc, argv); MainWindow mainWin; mainWin.show();
    return app.exec();
}

做完这些后,组织好你的工程内的文件(头文件、实现文件、资源文件、资源集文件 等),编译、链接、运行程序即可。一个典型的 MDI 应用程序界面如图 8-30 所示。

图 8-30 使用 QMdiArea 建立的 MDI 应用程序界面

到这里,关于如何使用 Qt4 创建 MDI 应用程序就讲解完了。采用 QMdiArea 的方法是笔 者重点讲解的,一是因为它是 Qt4.5 版以后官方所推荐采用的方法,用于替代 QWorkspace;二是因为它也是实现 MDI 的方法中使用起来最为复杂和令人困惑的一个。即 使是让 Qt“老鸟”来解释清楚什么是“主窗口的中心窗口”、“子窗口的内部窗口”、 “子窗口的中心区域”等等这些名词以及用法,也不是一件容易的事。

相对而言,使用 QWorkspace 创建 MDI 是比较容易的,但 Qt 以后将不再对这个类继续更新和支持,而“多实例实现多文档”的方法在 Mac OS X 上应用很广泛,它实质上就是使 用了多个顶层窗口,也是比较容易掌握的。

熟练使用 Qt4 建立 MDI 应用程序所涉及的内容远远不止本书所介绍的这些,比如子窗 口如何响应键盘与鼠标事件,如何同步所有主窗口的 “最近打开文件列表”等等问题都需要 费些力气解决。对于初学者而言,这是一个比较复杂的话题。大家在阅读到此处时,建议仍 然采用“知道、会用即可”的原则,不必深究它们背后的机理。随着学习进程的逐步深入, 一些开始接触时觉得困难的内容,就会在你心中逐渐明晰了。