这篇文章内容主要来自 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()
。这当然可以,然而却与调用一个简单的函数所有不同。
我们先从 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 的实现。
4 评论
今天的就很好,在GR里全文输出,特意过来反馈。
技术问题,今天太忙,以后再慢慢交流,还请多指教。
😛
没有MAC开发经验,就目前讲解的内容而言,我没有听太明白,什么是窗口级别的模态和应用程序级别的模态;我的理解的是:
1、exec()调用--应用程序级别的模态
2、对话框设置父对象,setModality();这样的窗口就是窗口级别的模态;
可以理解为,窗口级别的模态只是把这个窗口给阻塞了,应用程序级别的模态是把整个应用阻塞了。例如,一个应用的对话框又弹出了子窗口,这个子窗口可以仅仅阻塞这个对话框(并不会阻塞应用其他部分),也可以把整个应用给阻塞掉。