QDIALOG 窗口级别模态

这篇文章内容主要来自 QtQuarterly30 里面的 New Ways of Using QDialog,介绍的是使用QDialog::open()(这个函数是Qt 4.5 引入的),而不是传统的exec()来实现一个窗口级别的模态对话框。所谓模态对话框,就是对话框会阻塞用户与父窗口的交互,直到对话框关闭,在 Mac OS X 中则称为 Sheet。这里牵扯到很多细节问题,值得我们注意。

对话框和模态

Qt 文档中有这么一段描述:“对话框是用于短期任务和简单交互的顶层窗口。QDialog可以是模态的,也可以是非模态的。”

对于模态对话框,传统上会使用如下代码:

MyQDialogSubclass dialog;
// 初始化操作
if (dialog.exec() == QDialog::Accept) {
    // 获取返回值等
}

这段代码首先创建一个QDialog子类的对象,然后调用exec()函数。exec()函数会阻止代码继续运行,直到其返回,最后根据对话框返回值来决定进行哪些操作。这种模态被称为“应用程序级别模态”。在这种级别的模态下,用户输入(鼠标和键盘)只能派发给模态对话框,其他窗口则不能接收到。

另一种是非模态对话框,我们使用show()函数来打开一个非模态对话框。注意,非模态对话框不会阻塞用户与程序其他窗口进行交互。当然,如果要根据其返回值进行一些操作,我们也有另外的方法。

还有一种交互被称为“窗口级别模态”或者“文档级别模态”。这种模态只阻塞与其父窗口的交互,而不会阻塞与应用程序其他窗口的交互。例如,一个程序中每个文档都使用一个独立的窗口打开,如果使用应用程序级别的模态对话框进行保存,所有文档窗口都会被阻塞;如果使用窗口级别模态对话框,则只有打开它的那个窗口(也就是其父窗口)被阻塞。这种情况显示出,有时候,窗口级别模态要比原来那种一下子阻塞掉整个程序要方便得多。当然,使用这种模态要求对话框必须有一个父窗口。

Qt 4.1 起,QWidget引入了一个新的属性windowModality,用于设置窗口是哪种类型的模态。当窗口被创建出来,windowModality会被设置为Qt::NonModal,因此默认都是非模态的。在调用QDialog::exec()之后,windowModality会被设为Qt::ApplicationModal,当exec()返回后又被设回Qt::NonModal。如果我们不自己修改windowModality的值,那么可以简单的认为其值就是由show()和exec()设置的:

  • QDialog::show()=>Qt::NonModal
  • QDialog::exec()=>Qt::ApplicationModal

注意,上面的映射关系并没有包括Qt::WindowModal这个值。也就是说,如果我们要设置窗口级别的模态,就要手动设置windowModality,然后再调用show()或者exec()。这当然可以,然而却与调用一个简单的函数所有不同。

Sheet in Mac OS X

我们先从 Max OS X 的 Sheet 开始看起。上面是 Sheet 的一个示例。在 Apple 的人机交互规范 Apple Human Interface Guidelines 中,对 Sheet 是这么描述的:“Sheet 是关联到特定文档或窗口的模态对话框,确保用户不会丢失 sheet 所关联到的窗口的信息。Sheet 也可以用于让用户在 sheet 消失前完成一些小的任务,而不会产生系统被应用程序‘劫持’了的感觉。”我们仔细研读一下这段描述就会发现,sheet 实际上就是一个特殊的窗口级别模态对话框,之所以特殊,是因为 sheet 可以让用户很明显的看出来它阻塞的是哪一个窗口。

在 Qt 4.0 版本中,窗口级别模态对话框也是被支持的,只不过需要开发者做更多的操作:

void MainWindow::maybeSave()
{
    if (!messageBox) {
        messageBox = new QMessageBox(tr("SDI"),
            tr("The document has been modified.\n"
                "Do you want to save your changes?"),
            QMessageBox::Warning,
            QMessageBox::Yes | QMessageBox::Default,
            QMessageBox::No,
            QMessageBox::Cancel | QMessageBox::Escape,
            this, Qt::Sheet);
        messageBox->setButtonText(QMessageBox::Yes,
            isUntitled ? tr("Save...") : tr("Save"));
        messageBox->setButtonText(QMessageBox::No,
            tr("Don’t Save"));
        connect(messageBox, SIGNAL(finished(int)),
                this, SLOT(finishClose(int)));
    }
    messageBox->show();
}

这里,我们创建一个Qt::Sheet类型的对话框,并将其关闭的信号连接到程序的一个 slot 上以便进行关闭后的处理。对于 Mac OS X 的开发者而言,这段代码很清楚;然而对于其他平台的开发者,他们更愿意使用QMessageBox::show()这种 static 函数,而不是这么一堆代码。不过,这段代码还是正确地创建了一个窗口级别模态对话框:当对话框显示出来之后,函数需要立刻返回,并且不能阻塞。对于对话框返回值的处理则是在 slot 里面完成。

下面,我们来解释一下,为什么在 sheet 显示之后,这段代码需要立即返回,并且不能阻塞。阻塞函数,并且要继续派发事件的典型做法是,创建一个局部的QEventLoop对象,然后在窗口关闭时退出这个事件循环。相比而言,一个不好的实现是:

// 不要这么实现 Sheet!
void MainWindow::maybeSave()
{
    if (!messageBox) {
        messageBox = new QMessageBox(tr("SDI"),
            tr("The document has been modified.\n"
                "Do you want to save your changes?"),
            QMessageBox::Warning,
            QMessageBox::Yes | QMessageBox::Default,
            QMessageBox::No,
            QMessageBox::Cancel | QMessageBox::Escape,
            this, Qt::Sheet);
        messageBox->setButtonText(QMessageBox::Yes,
            isUntitled ? tr("Save...") : tr("Save"));
        messageBox->setButtonText(QMessageBox::No,
            tr("Don’t Save"));
    }
    QEventLoop eventLoop;
    connect(messageBox, SIGNAL(closed()),
            &eventLoop, SLOT(quit()));
    messageBox->show();
    eventLoop.exec();
    finishClose(messageBox->result());
}

这段代码仅仅在一些情况下起作用,而不是所有情况。下面考虑用户有两个没有保存的文档。如果你是用上面的代码,用户点击关闭按钮,一个 sheet 弹出来;当用户点击另一个文档的关闭按钮时,这个文档的窗口同样弹出 sheet。然后用户回到第一个窗口,并点击第一个 sheet 的“Don't Save” 按钮。这个 sheet 消失了,但窗口不会被关闭,因为它还被第二个 sheet 的事件循环阻塞到那里。显然,这不是我们所期望的。

另一个可能会误用 sheet 的地方是把它们当做应用程序级别的 sheet。Qt 的早期版本的确允许这种情况(事实上,Qt 的 static 函数就是这么干的),但这不符合 Apple 人机交互规范。这也引起一个容易引起困惑的地方,因为其他应用程序不会这么做。换句话说,Qt 对话框的 static 函数不应该用作 sheet 的实现。

QDialog::open()

考虑到前文所述的问题,我们对比一下应用程序级别模态的实现。当windowModality是Qt::NonModal的时候,只需调用QDialog::exec()就可以很容易实现。那么,为什么不提供一个类似的函数,只需简单的调用一下就可以实现一个窗口级别的模态对话框呢?答案就是QDialog::open()。QDialog::open()可以打开一个窗口级别模态对话框。在 Mac OS X 上就是一个 sheet。为了正确实现事件循环,当函数调用之后会立即返回。这意味着你必须通过信号槽机制来实现对返回结果的处理。好在QDialog提供了finished()信号,该信号会正确设置对话框的返回值,类似accept()和reject()。当然,你也可以直接继承QDialog从而使用自己的 signal。那么,对于前面所说的QMessageBox的问题,我们只需使用QMessageBox::open()而不是show(),就可以简单的实现窗口级别模态对话框。即便是在 Mac OS X 平台上,我们也不需要指定Qt::Sheet参数。open()函数是跨平台的,这意味着它能够在所有平台上都以我们所期望的方式运行。现在,我们有了一个更加完善的映射关系:

  • QDialog::show()=>Qt::NonModal
  • QDialog::exec()=>Qt::ApplicationModal
  • QDialog::open()=>Qt::WindowModal

这么一来,选择模态类型比以前简单很多了。下面我们将讲解另外一个令人困惑的问题。

子类的 static 函数

提供新的open()函数的另外一个目的是,避免对 sheet 的误用。这意味着你无需手动设置Qt::Sheet参数。同时,如果你需要打开一个应用程序级别模态的 sheet,那么获得的是一个普通的应用程序级别模态的对话框;这意味着,打开一个窗口级别模态的对话框,系统会为你打开一个 sheet。这是因为在 Cocoa 中,除了 sheet,没有办法打开一个窗口级别模态的普通对话框。

所有应用程序级别模态的对话框都不能使用 sheet 这一事实会影响到所有QDialog子类的 static 函数,例如QColorDialog, QFontDialog,QFileDialog,QInputDialog和QMessageBox。我们考虑其中一个函数,例如QColorDialog::getColor()。这个函数将创建一个QColorDialog对话框,然后模态地显示出来。每个函数的返回值都是用户从对话框中选择的颜色;如果用户没有选择,则会返回一个非法颜色值。这些函数的问题在于,它们提供的是应用程序级别的模态,因此不能使用 sheet。这也许会让一些使用QFileDialog的用户感到困惑,因为在 Mac OS X 下,Qt 的QFileDialog的确是一个 sheet。这是因为 Mac OS X 下,应用程序可以将QFileDialog作为 sheet 使用。

QDialog::open()让我们能够简单地使用三种模态类型。那么,下面让我们看看还有没有另外的函数。的确,我们可以看到open()的很多重载的版本,用于实现一些在过去需要很多代码才能完成的功能。很多子类允许我们方便地向open()函数添加处理返回值的 slot。这些对话框可以进行合适的连接,无需我们处理。下面就是这些重载版本的列表:

  • QFileDialog::open(QObject *receiver, const char *slot);
  • QColorDialog::open(QObject *receiver, const char *slot);
  • QFontDialog::open(QObject *receiver, const char *slot);
  • QPrintDialog::open(QObject *receiver, const char *slot);
  • QPageSetupDialog::open(QObject *rec, const char *slot);
  • QInputDialog::open(QObject *receiver, const char *slot);
  • QProgressDialog::open(QObject *receiver, const char *slot);
  • QPrintPreviewDialog::open(QObject *rec, const char *slot);

提供这些函数的目的是,能够方便地连接最常见的处理函数。下面我们将看看这些连接如何进行连接:

  • QColorDialog将传递进来的 slot 连接到colorSelected(QColor)信号;
  • QFontDialog连接到fontSelected(QFont)信号;
  • QFileDialog连接到fileSelected(QString)或者filesSelected(QStringList)信号,这取决于其模式
  • QProgressDialog连接到canceled()信号。

你可以从 Qt 文档中获得更详细的信息。在open()中指定响应的 slot 可以很好的简化代码。利用这种方法,你可以直接打开一个 native 的对话框,现在提供这个功能的类是QFileDialog,QColorDialog,QFontDialog和QPrintDialog。

新的交互形式

既然我们可以使用open()打开一个 native 的对话框,我们就应该可以类似的使用show()打开一个 native 的非模态对话框。初看起来,这么做并没有什么用,但是它可以在 Mac OS X 下打开一个标准的“live feedback”对话框。这在 Qt 中相当简单。

QDialog子类提供的 static 函数鼓励开发者使用这么一种方式:阻止用户继续工作,并且询问一个问题(例如,“你想用哪个字体?”)。但是,有一定程度上,这将影响到用户的工作,甚至惹恼用户。想象一下用户要使用QColorDialog选择颜色的情景。他需要打开对话框,点击选择颜色,关闭对话框,然后才能看到效果如何。如果他们不满意选择的颜色,则不得不重新进行上面的工作。很显然,这种繁复的工作通常很讨厌,为什么不设计成对话框一直显示在那里,让用户选择好颜色之后就可以马上看到效果呢?一种解决方案是,自己创建一个非模态对话框,实现前面所说的工作。例如在字体选择时,Qt 有一个QFontComboBox类,允许以非模态方式选择字体。但它不可能实现QFontDialog所能做的所有事情。使用对话框更为直观。为了达到这一目的,QFontDialog类提供了QFontDialog::currentFontChanged()信号。我们可以连接到这个信号,然后使用show()提供非模态对话框。这样,我们就有了一个不会打扰用户操作的对话框,能够立刻将相应返回给窗口。QColorDialog同样有类似的方法。这种实现可以让用户感觉程序更加友好。我们以颜色选择为例,看看如何实现这种方式。

class MainWindow
{
    Q_OBJECT
    //...
private:
    // ...
    QColorDialog *globalColorDialog;
    // ...
};
class PaintArea
{
    Q_OBJECT
    //...
public slots:
    void setBrushColor(const QColor &color);
    // ...
};

我们不能使用QColorDialog提供的 static 函数,而是保持一个QColorDialog的指针。为此,我们在主窗口添加一个指针,并且要在组件中添加 slot 函数(这里就是QPaintAreas::setBrushColor())。

void MainWindow::brushColor()
{
  if (!globalColorDialog) {
    globalColorDialog = new QColorDialog(this);
    globalColorDialog->setCurrentColor(paintArea->brushColor());
    globalColorDialog->setOption(
        QColorDialog::NoButtons, true);
    connect(globalColorDialog,
            SIGNAL(currentColorChanged(QColor)),
            paintArea,
            SLOT(setBrushColor(QColor)));
  }
  globalColorDialog->show();
}

我们需要将选择的颜色设置给当前画笔。使用QColorDialog::NoButtons以避免 OK 和 Cancel 按钮出现。这主要是因为在这种实现方式中,它们并没有什么意义,因为我们要将选择的颜色立即返回给画笔(取消按钮是不能撤销颜色选择的)。但是,在 X11 的某些窗口管理器上,没有关闭按钮的窗口会变得很奇怪,这一点值得注意。最后,我们创建currentColorChanged()和setBrushColor()的信号槽连接。然后调用show()函数显示对话框。如果对话框已经显示,则简单地将其放置在窗口最顶层。

对于QFontDialog也是类似的,没有什么区别。我们创建一个字体对话框,通过保持其指针来做信号槽连接。

结论

这篇文章阐述了一些使用QDialog的新的方法。我们着重讲述了各种模态的实现,以及一些有用的技巧。这里提到的函数有些是 Qt 4.5 之后新增加的,例如QDialog::open(),这些函数都很有用,所以我们应该在自己的程序中多多使用,而不是固守于旧的接口。毕竟,我们的程序也需要与时俱进的~

本文福利,费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块等等)↓↓↓↓↓↓见下面↓↓文章底部点击费领取↓↓

猜你喜欢

转载自blog.csdn.net/hw5230/article/details/132701143