QInputDialog源码分析(下)

QInputDialog源码分析(上) 里主要针对 QInputDialogPrivate 类的实现做了很详细的介绍,第二部分主要就是介绍 QInputDialog 和看源码过程中一些其他知识点的总结

这篇文章主要根据 QInputDialog 类提供的接口,按照我的理解将这些源码分成以下几个部分分析

枚举

InputMode

enum InputMode {
    TextInput,
    IntInput,
    DoubleInput
};
内容 描述
QInputDialog::TextInput 0 Used to input text strings.
QInputDialog::IntInput 1 Used to input integers.
QInputDialog::DoubleInput 2 Used to input floating point numbers with double precision accuracy.

InputMode 决定了需要初始化哪一种控件,而根据控件的不同,从 QInputDialog 获取值时的方法也是不同的

InputMode 类型 对应的获取值的接口 描述
QInputDialog::IntInput int int intValue() const; 返回值是从 QSpinBox 控件中获取值 d->intSpinBox->value(),默认是0
QInputDialog::DoubleInput double double doubleValue() const; 返回值是从 QDoubleSpinBox 空间中获取值d->doubleSpinBox->value(). 默认是 0
QInputDialog::TextInput QString QString textValue() const; 从控件 QLineEdit,QPlainTextEdit,QComboBox 中获取的都是 QString 类型
这些控件在值改变时,触发信号,改变一个QInputDialogPrivate的成员变量textValue, QInputDialog 获取值时 d->textValue

接下来主要看一下这个类型的 设置set获取get 的函数

setInputMode()

根据 InputMode 的类型,初始化对应的控件,最后在通过 QInputDialogPrivatesetInputWidget(widget) 来对 inputwidget 赋值

void QInputDialog::setInputMode(InputMode mode)
{
    Q_D(QInputDialog);

    QWidget *widget;
    switch (mode) {
    case IntInput:
        d->ensureIntSpinBox();
        widget = d->intSpinBox;
        break;
    case DoubleInput:
        d->ensureDoubleSpinBox();
        widget = d->doubleSpinBox;
        break;
    default:
        Q_ASSERT(mode == TextInput);
        d->chooseRightTextInputWidget();
        return;
    }

    d->setInputWidget(widget);
}

inputMode()

根据 inputWidget 的类型来决定返回的 QInputDialog::InputMode

QInputDialog::InputMode QInputDialog::inputMode() const
{
    Q_D(const QInputDialog);

    if (d->inputWidget) {
        if (d->inputWidget == d->intSpinBox) {
            return IntInput;
        } else if (d->inputWidget == d->doubleSpinBox) {
            return DoubleInput;
        }
    }

    return TextInput;
}

InputDialogOption

看一个类需要先简单看一下它的枚举

enum InputDialogOption {
    NoButtons                    = 0x00000001,
    UseListViewForComboBoxItems  = 0x00000002,
    UsePlainTextEditForTextInput = 0x00000004
};
Q_DECLARE_FLAGS(InputDialogOptions, InputDialogOption)
内容 描述
QInputDialog::NoButtons 0x00000001 Don’t display OK and Cancel buttons (useful for “live dialogs”).
QInputDialog::UseListViewForComboBoxItems 0x00000002 Use a QListView rather than a non-editable QComboBox for displaying the items set with setComboBoxItems().
QInputDialog::UsePlainTextEditForTextInput 0x00000004 Use a QPlainTextEdit for multiline text input. This value was introduced in 5.2.

QInputDialog源码分析的上篇里提到呈现 QString 的控件有4种 QListView,QComboBox,QPlainTextEdit,QLineEidt

  • 默认使用 QLineEidt
  • InputDialogOption::UseListViewForComboBoxItems 来区分 QListViewQComboBox,而区分这个这个还有一个前提,是通过 setComboBoxItems() 设置了条目
  • InputDialogOption::UsePlainTextEditForTextInput 来使用 QPlainTextEdit

setOptions

设置 QInputDialogPrivateopts; 然后根据 opts 是否改变以及设置的值才设置展示的控件

void QInputDialog::setOptions(InputDialogOptions options)
{
    Q_D(QInputDialog);

    // 异或检测 options 是否改变
    InputDialogOptions changed = (options ^ d->opts);
    if (!changed)
        return;

    d->opts = options;

    // 初始化布局
    d->ensureLayout();

    // 根据影响对话框外观的各种选项 options, 来设置对话框
    if (changed & NoButtons)
        d->buttonBox->setVisible(!(options & NoButtons));
    if ((changed & UseListViewForComboBoxItems) && inputMode() == TextInput)
        d->chooseRightTextInputWidget();
    if ((changed & UsePlainTextEditForTextInput) && inputMode() == TextInput)
        d->chooseRightTextInputWidget();
}

chooseRightTextInputWidget() 函数会根据 opts 的值,选择初始化控件,以及展示的控件,具体的代码在上篇里有详细的介绍

options()

因为 QInputDialog 将很多数据对象都封装在 QInputDialogPrivate 中, 所以获取值的操作大部分都是从 D指针 中获取

QInputDialog::InputDialogOptions QInputDialog::options() const
{
    Q_D(const QInputDialog);
    return d->opts;
}

枚举的作用

  • InputMode 决定初始化 int, double 还是 string 类型的控件
  • InputDialogOption 主要是为了决定初始化具体的哪一种 string 的控件,其实也可以理解成呈现 string 的样式 (NoButtons也是一种样式)

初始化之后,设置最后呈现的控件函数是 QInputDialogPrivatesetInputWidget(QWidget *widget) ,为 inputWidget 赋值和一些信号槽的创建

  • setInputMode() 中调用了此函数
  • setOptions() 中的 chooseRightTextInputWidget() 函数中也调用了此函数

控件初始化和属性操作

这一部分的代码其实很简单,在需要对控件进行属性设置的时候,先确保控件已经初始化,然后直接对控件进行操作(设置属性或者获取值信息等)

关于控件的初始化和属性设置和获取其实可以分为3部分

  • inputwidget 显示值的设置,因为需要保证对应的控件已经初始化,需要调用 setInputMode() 函数(需要初始化控件并对inputwidget赋值), 主要就是以下3个函数
void QInputDialog::setIntValue(int value)
{
    Q_D(QInputDialog);
    setInputMode(IntInput);
    d->intSpinBox->setValue(value);
}

void QInputDialog::setDoubleValue(double value)
{
    Q_D(QInputDialog);
    setInputMode(DoubleInput);
    d->doubleSpinBox->setValue(value);
}

void QInputDialog::setTextValue(const QString &text)
{
    Q_D(QInputDialog);

    setInputMode(TextInput);
    if (d->inputWidget == d->lineEdit) {
        d->lineEdit->setText(text);
    } else if (d->inputWidget == d->plainTextEdit) {
        d->plainTextEdit->setPlainText(text);
    } else if (d->inputWidget == d->comboBox) {
        d->setComboBoxText(text);
    } else {
        d->setListViewText(text);
    }
}
  • QLabel, QDialogButtonBox, QVBoxLayout 这类界面上必须呈现的控件属性设置, 调用 ensureLayout() 后设置属性
// 设置
void QInputDialog::setOkButtonText(const QString &text)
{
    Q_D(const QInputDialog);
    d->ensureLayout();
    d->buttonBox->button(QDialogButtonBox::Ok)->setText(text);
}
// 获取
QString QInputDialog::okButtonText() const
{
    Q_D(const QInputDialog);
    d->ensureLayout();
    return d->buttonBox->button(QDialogButtonBox::Ok)->text();
}
  • 只对显示值的控件属性操作,例如 QSpinBoxQDoubleSpinBox 最大值最小值属性设置等,调用对应的初始化接口, 例如 ensureIntSpinBox(), ensureDoubleSpinBox() 等后直接对控件设置
// 设置
void QInputDialog::setIntStep(int step)
{
    Q_D(QInputDialog);
    d->ensureIntSpinBox();
    d->intSpinBox->setSingleStep(step);
}

// 获取只需要判断对应的插件是否初始化了即可
// 因为开发者可能初始化了text的控件,却错误的调用int控件的属性接口,此时没有必要初始化int接口,直接判断是否存在后,返回默认值即可
int QInputDialog::intStep() const
{
    Q_D(const QInputDialog);
    if (d->intSpinBox) {
        return d->intSpinBox->singleStep();
    } else {
        return 1;
    }
}

父类的重载函数

因为真正展示的控件主要是 QLabel, QDialogButtonBox, QVBoxLayout, inputWidget 这4个
但是开发者不一定有对这些控件进行设置,也就是说没有 d->ensureLayout(); 初始化这些控件,为了保证最后可以正常展示,对父类的一些接口进行重载,保证控件进行过初始化

setVisible()

QInputDialog 不管是调用 exec()show() 还是 open() 来显示界面(开个新坑,之后会有一篇关于 QDialog 这3个显示方法的源码分析), 因为类的继承关系,最后都会调用 QWidget::show()

void QWidget::show()
{
    Qt::WindowState defaultState = QGuiApplicationPrivate::platformIntegration()->defaultWindowState(data->window_flags);
    if (defaultState == Qt::WindowFullScreen)
        showFullScreen();
    else if (defaultState == Qt::WindowMaximized)
        showMaximized();
    else
        setVisible(true);
}

因为子类重新实现了这个虚函数,最后会调用 QInputDialogsetVisible() 的函数

void QInputDialog::setVisible(bool visible)
{
    Q_D(const QInputDialog);
    if (visible) {
        d->ensureLayout();
        d->inputWidget->setFocus();
        if (d->inputWidget == d->lineEdit) {
            d->lineEdit->selectAll();
        } else if (d->inputWidget == d->plainTextEdit) {
            d->plainTextEdit->selectAll();
        } else if (d->inputWidget == d->intSpinBox) {
            d->intSpinBox->selectAll();
        } else if (d->inputWidget == d->doubleSpinBox) {
            d->doubleSpinBox->selectAll();
        }
    }
    QDialog::setVisible(visible);
}

setFocus() : 将键盘输入焦点赋予该窗口小部件
selectAll() : 选择所有文本然后将光标移到末尾

从这里可以看出,又调用了 d->ensureLayout(); , 确保控件都有正确初始化,因为 ensureLayout() 有判断 inputWidget 是否为空,为空的话默认创建 QLineEdit, 所有一定能保证界面可以正常显示,即使开发者仅仅只是创建了一个对象之后,未做任何设置之后,直接 show(), 也不会出现控件没有初始化的问题

QInputDialog *dialog = new QInputDialog{this, Qt::WindowCloseButtonHint};
dialog->exec();

open()

其实这个 open() 不是重载了父类的函数,而是实现了一个新的接口 void open(QObject *receiver, const char *member);

void QInputDialog::open(QObject *receiver, const char *member)
{
    Q_D(QInputDialog);
    connect(this, signalForMember(member), receiver, member);
    d->receiverToDisconnectOnClose = receiver;
    d->memberToDisconnectOnClose = member;
    QDialog::open();
}

这一部分的代码其实也挺有意思,connect 将一个信号和开发者输入的对象和槽函数绑定,并将这些信息存入 QInputDialogPrivate 的成员变量中,首先需要知道 connect 到底用的信号是什么

static const char *signalForMember(const char *member)
{
    QByteArray normalizedMember(QMetaObject::normalizedSignature(member));

    for (int i = 0; i < NumCandidateSignals; ++i)
        if (QMetaObject::checkConnectArgs(candidateSignal(i), normalizedMember))
            return candidateSignal(i);

    // otherwise, use fit-all accepted signal:
    return SIGNAL(accepted());
}

static bool checkConnectArgs(const char *signal, const char *method) 这个函数如果 signal 和 method 参数兼容,则返回true;否则返回false。

这里面还有一个 candidateSignal(i) 还需要看一下这一部分源码

enum CandidateSignal {
    TextValueSelectedSignal,
    IntValueSelectedSignal,
    DoubleValueSelectedSignal,

    NumCandidateSignals
};

static const char *candidateSignal(int which)
{
    switch (CandidateSignal(which)) {
    case TextValueSelectedSignal:   return SIGNAL(textValueSelected(QString));
    case IntValueSelectedSignal:    return SIGNAL(intValueSelected(int));
    case DoubleValueSelectedSignal: return SIGNAL(doubleValueSelected(double));

    case NumCandidateSignals:       ; // fall through
    };
    Q_UNREACHABLE();
    return Q_NULLPTR;
}

candidateSignal(i) 就是获取一个信号

所以 signalForMember() 这个函数主要作用就是从 textValueSelected(QString), intValueSelected(int), doubleValueSelected(double), accepted() 这4个信号里选出一个参数值和开发者输入槽函数中参数是一致的信号,进行绑定, 现在最后的问题就是这些信号何时才会触发,看下一部分的源码可以得到答案

done()

当窗口关闭时触发时,调用的函数

我简单介绍一下流程,这一部分源码展开有点多

当点击确定按钮会触发按钮的 accepted() 的信号,QDialog 会调用 accept() 的槽函数

enum DialogCode { Rejected, Accepted };
void QDialog::accept()
{
    done(Accepted);
}

因为 QInputDialog 对于 done(int result) 的重新实现,所以会触发下面一段代码

void QInputDialog::done(int result)
{
    Q_D(QInputDialog);
    QDialog::done(result);
    if (result) {
        InputMode mode = inputMode();
        switch (mode) {
        case DoubleInput:
            emit doubleValueSelected(doubleValue());
            break;
        case IntInput:
            emit intValueSelected(intValue());
            break;
        default:
            Q_ASSERT(mode == TextInput);
            emit textValueSelected(textValue());
        }
    }
    if (d->receiverToDisconnectOnClose) {
        disconnect(this, signalForMember(d->memberToDisconnectOnClose),
                   d->receiverToDisconnectOnClose, d->memberToDisconnectOnClose);
        d->receiverToDisconnectOnClose = 0;
    }
    d->memberToDisconnectOnClose.clear();
}

先简单看一下父类 QDialog 的这个函数的实现

void QDialog::done(int r)
{
    Q_D(QDialog);
    hide();
    setResult(r);

    d->close_helper(QWidgetPrivate::CloseNoEvent);
    d->resetModalitySetByOpen();

    emit finished(r);
    if (r == Accepted)
        emit accepted();
    else if (r == Rejected)
        emit rejected();
}

因为 r 就是枚举里的 Accepted 也就是 1(true), 父类 QDialog 会触发 accepted() 信号
然后我们在看回 QInputDialog::done(int result), 根据 inputMode 的类型,发送对应的信号 因为窗口关闭,最后断开信号槽,清空数据

举个具体的例子

    QInputDialog *dialog = new QInputDialog{this, Qt::WindowCloseButtonHint};
    dialog->setInputMode(TextInput);
    dialog->open(this, SLOT(test(QString)));

例子中的槽函数是 text(QString), 根据跟信号的参数匹配,绑定信号 textValueSelected(QString), 当窗口点击确定关闭的时候就会触发 text(QString) 这个函数 如果 dialog->open(this, SLOT(test())); 此时绑定信号 accepted(), 如果想要从 dialog 中获取值, 此时的槽函数实现应该如下:

void MainWindow::test()
{
    QInputDialog * dialog = qobject_cast<QInputDialog *> (sender());
    qDebug() << dialog->textValue();
    // 这个 dialog 的资源创建在堆上,关闭窗口后需要 delete
    dialog->deleteLater();
}

提供的信号

信号 描述
void textValueChanged(const QString &text); QString 值改变的时候触发的信号
void textValueSelected(const QString &text); 当窗口 accpted() 时并且 inputMode() 等于 TextInput 类型时触发
void intValueChanged(int value); int 值改变的时候触发的信号
void intValueSelected(int value); 当窗口 accpted() 时并且 inputMode() 等于 IntInput 类型时触发
void doubleValueChanged(double value); double 值改变的时候触发的信号
void doubleValueSelected(double value); 当窗口 accpted() 时并且 inputMode() 等于 DoubleInput 类型时触发

源码读到这里,QInputDialog 的源码基本算是全部看完了,后面只剩下一个静态函数,这一部分的代码很简单,就不做介绍了

额外的知识

mutable

QInputDialogPrivate 的成员变量都使用了 mutable 来修饰,简单了解一下 mutable 的作用

mutable 修饰的成员变量( mutable 只能用于修饰类的非静态数据成员),那么它就可以突破 const 的限制,在被 const 修饰的函数里面也能被修改。

Q_PRIVATE_SLOT

qinputdialog.cpp 的类 QInputDialogPrivate 中的接口 ensureLineEdit() 信号槽上篇中没有详细介绍,这里单独拿出来说一下

void QInputDialogPrivate::ensureLineEdit()
{
    Q_Q(QInputDialog);
    if (!lineEdit) {
        lineEdit = new QLineEdit(q);
#ifndef QT_NO_IM
        qt_widget_private(lineEdit)->inheritsInputMethodHints = 1;
#endif
        lineEdit->hide();
        QObject::connect(lineEdit, SIGNAL(textChanged(QString)), q, SLOT(_q_textChanged(QString)));
    }
}

QObject::connect() 现在的需求是连接 lineEdittextChanged(QString) 信号 和 QInputDialogPrivate_q_textChanged(QString)

但是按照 Qt4 的风格, 槽函数必须放在由 slots 修饰的代码块中,并且要使用访问控制符进行访问控制。

QInputDialogPrivate 里并没有对 _q_textChanged(QString) 做任何的修饰,所以 Qt4 的风格没办法直接建立信号槽,所以这里绑定的是 qSLOT(_q_textChanged(QString)); 并且在 qinputdialog.cpp 文件中添加

#include "moc_qinputdialog.cpp" // 开个新坑,关于 Qt 的 MOC 会在下一篇博客中详细的介绍

为了保证信号槽能正确使用,必须要在 qinputdialog.h 里用到 Q_PRIVATE_SLOT 的宏

Q_PRIVATE_SLOT(d_func(), void _q_textChanged(const QString&))

大致意思就是在 qinputdialog.h 里生成一个 _q_textChanged(QString) 的槽函数
执行 d_func()->_q_textChanged(QString);

其实如果使用 Qt5 的 connect 方式是很简单的

QObject::connect(lineEdit, &QLineEdit::textChanged, this, &QInputDialogPrivate::_q_textChanged);
QObject::connect(lineEdit, &QLineEdit::textChanged, [this](QString text){_q_textChanged(text);});

Q_PRIVATE_SLOT 的宏是仅仅只是为了兼容 Qt4 。对于 Qt5 是没有必要的

总结

  • QInputDialog 的源码其实阅读起来感觉还行,主要是因为这个类整体的难度不是很大,没有用到一些特别复杂的代码,之后有时间会在选择一些难度比较大的类
  • QInputDialog 里其实有很多的宏我是不明白具体的用处的,比如 QT_NO_PROPERTIES,QT_NO_SHORTCUT,QT_NO_IM 等,可能是跨平台开发时用的?
  • 对于 QInputDialogInputDialogOption 这个枚举的设计还是不太喜欢,在我的理解里,根据展示的数据类型引入 InpputMode 的枚举,再根据 QString 类型展示控件的选择引入枚举 InputDialogOption, 并且将一些其他的样式也归并在一起,但是既然都区分了,为什么不将 QComboBox ,QListView, QLineEditQPlainTextEdit 完全枚举出来? 后来又看了看代码,这样设计原因可能有以下几点:
    • 默认 TextInput 下默认是使用 QLineEdit 控件,也就是说开发者不设置 InputDialogOption, 默认就用 QLineEdit 就完事了
    • QComboBoxQListView 使用的时候必须要初始化下拉框里的条目,这样自然就和 QLineEditQPlainTextEdit 区分开了,所以只需要额外添加一个 InputDialogOption::UseListViewForComboBoxItems 枚举就行
    • 再此基础上也就在添加一个枚举 InputDialogOption::UsePlainTextEditForTextInput 区分 QLineEditQPlainTextEdit 就行

另外在看源码的过程中,又开始对其他部分的源码产生一些兴趣

  • Qt 的事件循环到底是怎样运行的?
  • Qt 的 MOC 到底作用是什么,它和信号槽的关系是什么?
  • 窗口 show() , exec(), open() 这个的区别都知道,但是代码到底是怎么实现的?
  • 窗口 show() 又是怎样在电脑上展示出来的?
  • 等等等

问题还是有点多的,我在开发过程中大多都是知其然,具体怎么用其实都很了解,但是不知其所以然,想在自己的公众号里以写成文章的形式一步一步记录下来,做一些完整的知识输出,让自己有更深的理解

望加油!