Note

对话框是程序与用户”短促对话”的窗口:选个文件、确认删除、输入一个名字。本篇讲清模态(exec)与非模态(show)的本质区别与选型标准,示范如何编写自定义 QDialog 子类并传递返回值,最后过一遍最常用的标准对话框:QFileDialog、QColorDialog、QFontDialog、QInputDialog 和 QMessageBox。适合已完成控件与主窗口学习的读者。

QDialog 是什么

QDialog 是所有对话框类的基类(QFileDialog、QMessageBox 都继承自它)。与普通窗口相比,它有两个外观特征:标题栏右上角没有最小化/最大化按钮;按 Esc 键总是触发 reject() 关闭对话框。

QDialog dialog(this);          // 传入父窗口,弹出时居中于父窗口
dialog.setWindowTitle("设置");

它最重要的属性不是外观,而是模态性——决定了对话框与父窗口的关系。

模态 exec() 与非模态 show()

两者的区别一句话概括:模态对话框不关闭,父窗口就点不动;非模态对话框两边可以自由切换

// 模态:程序阻塞在 exec() 这里,直到对话框关闭,返回结果
QDialog dialog(this);
int ret = dialog.exec();
if (ret == QDialog::Accepted) {
    // 用户点了"确定"
}
 
// 非模态:show() 立即返回,对话框与主窗口并存
QDialog *dlg = new QDialog(this);   // 注意:必须用 new,不能用栈对象
dlg->show();

这里藏着两个关键细节:

  1. 返回值约定exec() 返回 QDialog::Accepted(1)或 QDialog::Rejected(0)。对话框内部的”确定”按钮 connect 到 accept()、“取消” connect 到 reject(),这两个槽会关闭对话框并设置结果。
  2. 内存差异exec() 阻塞,栈上对象在函数返回前一直活着,安全;show() 立即返回,栈对象马上析构,对话框会一闪而过甚至崩溃——非模态必须 new 出来并指定父对象,让 Qt 父子机制管理其生命周期。

Warning

最常见的崩溃原因就是非模态对话框用了栈对象:QDialog dlg(this); dlg.show();——show 返回后 dlg 立刻销毁,界面瞬间消失。非模态对话框一律用 new 创建、传父对象。

选型标准:需要用户必须先回答才能继续的(打开文件、确认删除、错误提示)用模态;需要与主窗口保持互动的(查找替换、浮动调参面板)用非模态。Qt 的通用对话框内部都是模态的。

自定义 QDialog 子类与数据返回

真正的项目里对话框都要自己定制。套路很固定:继承 QDialog,构造函数里搭控件和按钮,确定按钮 connect 到 accept(),对外提供读取用户输入的接口:

// namedialog.h
#include <QDialog>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QVBoxLayout>
 
class NameDialog : public QDialog
{
    Q_OBJECT
public:
    explicit NameDialog(QWidget *parent = nullptr) : QDialog(parent)
    {
        setWindowTitle("输入名称");
        m_edit = new QLineEdit(this);
        // 标准按钮盒:确定/取消自动绑定 accept()/reject()
        QDialogButtonBox *btns = new QDialogButtonBox(
            QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept);
        connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
 
        QVBoxLayout *lay = new QVBoxLayout(this);
        lay->addWidget(m_edit);
        lay->addWidget(btns);
    }
 
    QString name() const { return m_edit->text(); }  // 对外读取接口
 
private:
    QLineEdit *m_edit;
};

调用方拿到返回值后读取数据:

NameDialog dlg(this);
if (dlg.exec() == QDialog::Accepted) {
    qDebug() << "用户输入:" << dlg.name();
}

Success

QDialogButtonBox 是对话框按钮的标配:自动处理按钮顺序的平台差异(Windows 与 macOS 确认/取消位置相反),并自动连接 accept/reject。传数据给对话框用构造参数或 setter,取数据用 getter——让数据流向清晰单向。

标准对话框四件套

Qt 把高频需求做成了静态函数,一行代码弹窗取值,无需自建类。

QFileDialog 选文件

QString file = QFileDialog::getOpenFileName(
    this, tr("打开图片"), QString(),
    tr("图片文件 (*.png *.jpg);;所有文件 (*)"));
if (!file.isEmpty()) { /* 用户确实选了文件 */ }

第三个参数是初始目录,第四个是过滤器,多种类型用 ;; 分隔。同类还有 getSaveFileName(保存)和 getExistingDirectory(选目录)。

QColorDialog 取颜色

QColor color = QColorDialog::getColor(Qt::white, this);
if (color.isValid()) {          // 用户按了取消会返回非法颜色,必须判断
    widget->setStyleSheet(QString("background: %1").arg(color.name()));
}

QFontDialog 取字体

bool ok = false;
QFont font = QFontDialog::getFont(&ok, this);
if (ok) { editor->setFont(font); }   // 用 ok 标志判断用户是否取消

QInputDialog 输入简短内容:整数、浮点数、文本、下拉项各有对应静态函数:

bool ok = false;
QString text = QInputDialog::getText(
    this, tr("重命名"), tr("新名称:"), QLineEdit::Normal, "默认值", &ok);
int count = QInputDialog::getInt(this, tr("数量"), tr("份数:"), 1, 1, 99);
if (ok) { /* 使用 text / count */ }

Warning

取消操作的判断方式三件套各不相同:QColorDialog 用 isValid(),QFontDialog 和 QInputDialog 用 bool *ok 指针,QFileDialog 用返回字符串是否为空。不判断就使用返回值,用户按取消时会写入非法数据。

QMessageBox:消息与确认

QMessageBox 是 QDialog 的派生类,用于提示信息或让用户做选择。静态函数最常用,返回用户点击的按钮枚举:

// 提示信息
QMessageBox::information(this, tr("完成"), tr("文件已保存"));
 
// 询问:三个按钮,判断用户点了哪个
int ret = QMessageBox::question(
    this, tr("确认删除"),
    tr("确定要删除这条记录吗?此操作不可恢复。"),
    QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
    QMessageBox::No);                       // 默认焦点在 No,防误触
if (ret == QMessageBox::Yes) { deleteRecord(); }
 
// 警告与错误
QMessageBox::warning(this, tr("警告"), tr("磁盘空间不足"));
QMessageBox::critical(this, tr("错误"), tr("无法打开文件"));

四个静态函数 information / question / warning / critical 只是图标和默认按钮不同,参数一致。需要更复杂的消息框(附加复选框、详细文本)时,用对象方式逐步设置:

QMessageBox box(this);
box.setWindowTitle(tr("保存更改"));
box.setText(tr("文档已被修改。"));
box.setInformativeText(tr("是否保存修改后的内容?"));
box.setStandardButtons(QMessageBox::Save | QMessageBox::Discard
                       | QMessageBox::Cancel);
box.setDefaultButton(QMessageBox::Save);
int ret = box.exec();   // 与 QDialog 的 exec 同一套返回值体系

完整可运行示例

把自定义对话框与标准对话框串起来,Qt5 / Qt6 兼容:

#include <QApplication>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QLabel>
#include <QFileDialog>
#include <QColorDialog>
#include <QFontDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDialogButtonBox>
#include <QLineEdit>
 
// 自定义对话框:输入名称
class NameDialog : public QDialog
{
public:
    explicit NameDialog(QWidget *parent = nullptr) : QDialog(parent)
    {
        setWindowTitle("输入名称");
        m_edit = new QLineEdit(this);
        QDialogButtonBox *btns = new QDialogButtonBox(
            QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept);
        connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
        QVBoxLayout *lay = new QVBoxLayout(this);
        lay->addWidget(m_edit);
        lay->addWidget(btns);
    }
    QString name() const { return m_edit->text(); }
private:
    QLineEdit *m_edit;
};
 
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
 
    QWidget window;
    window.setWindowTitle("对话框示例");
    window.resize(320, 200);
    QLabel *status = new QLabel("点击下方按钮体验各种对话框");
    QPushButton *btnCustom = new QPushButton("自定义对话框(模态)");
    QPushButton *btnFile   = new QPushButton("标准对话框:选文件");
    QPushButton *btnColor  = new QPushButton("标准对话框:取颜色");
 
    // 模态:exec() 返回后才能继续
    QObject::connect(btnCustom, &QPushButton::clicked, [&status](){
        NameDialog dlg;
        if (dlg.exec() == QDialog::Accepted)
            status->setText(QString("你输入了:%1").arg(dlg.name()));
        else
            status->setText("你取消了输入");
    });
 
    // 标准对话框静态函数:一行弹窗
    QObject::connect(btnFile, &QPushButton::clicked, [&status](){
        QString f = QFileDialog::getOpenFileName(
            nullptr, "打开文件", QString(), "文本文件 (*.txt)");
        status->setText(f.isEmpty() ? "未选择文件" : QString("选中:%1").arg(f));
    });
 
    QObject::connect(btnColor, &QPushButton::clicked, [&window](){
        QColor c = QColorDialog::getColor(Qt::white, &window);
        if (c.isValid())
            window.setStyleSheet(
                QString("background: %1").arg(c.name()));
    });
 
    QVBoxLayout *lay = new QVBoxLayout(&window);
    lay->addWidget(status);
    lay->addWidget(btnCustom);
    lay->addWidget(btnFile);
    lay->addWidget(btnColor);
 
    window.show();
    return app.exec();
}

Warning

静态函数弹出的对话框都是模态的,会阻塞当前函数直到用户响应。不要在计算密集的后台线程里直接调它们——对话框属于界面线程,跨线程创建控件是 Qt 明令禁止的行为。

Success

判断该用哪种对话框的经验法则:一次性获取数据用模态 + exec()(大多数场景);持续调整参数用非模态 + 信号槽同步。拿不准时先模态,实际用起来别扭再改非模态,改造成本很低。