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 的类型,初始化对应的控件,最后在通过 QInputDialogPrivate 的 setInputWidget(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来区分QListView和QComboBox,而区分这个这个还有一个前提,是通过setComboBoxItems()设置了条目InputDialogOption::UsePlainTextEditForTextInput来使用QPlainTextEdit
setOptions
设置 QInputDialogPrivate 的 opts; 然后根据 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也是一种样式)
初始化之后,设置最后呈现的控件函数是 QInputDialogPrivate 的 setInputWidget(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();
}
- 只对显示值的控件属性操作,例如
QSpinBox,QDoubleSpinBox最大值最小值属性设置等,调用对应的初始化接口, 例如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);
}
因为子类重新实现了这个虚函数,最后会调用 QInputDialog 的 setVisible() 的函数
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() 现在的需求是连接 lineEdit 的 textChanged(QString) 信号 和 QInputDialogPrivate 的 _q_textChanged(QString)
但是按照 Qt4 的风格, 槽函数必须放在由 slots 修饰的代码块中,并且要使用访问控制符进行访问控制。
而 QInputDialogPrivate 里并没有对 _q_textChanged(QString) 做任何的修饰,所以 Qt4 的风格没办法直接建立信号槽,所以这里绑定的是 q 和 SLOT(_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等,可能是跨平台开发时用的?- 对于
QInputDialog的InputDialogOption这个枚举的设计还是不太喜欢,在我的理解里,根据展示的数据类型引入InpputMode的枚举,再根据QString类型展示控件的选择引入枚举InputDialogOption, 并且将一些其他的样式也归并在一起,但是既然都区分了,为什么不将QComboBox,QListView,QLineEdit和QPlainTextEdit完全枚举出来? 后来又看了看代码,这样设计原因可能有以下几点:- 默认
TextInput下默认是使用QLineEdit控件,也就是说开发者不设置InputDialogOption, 默认就用QLineEdit就完事了 QComboBox和QListView使用的时候必须要初始化下拉框里的条目,这样自然就和QLineEdit和QPlainTextEdit区分开了,所以只需要额外添加一个InputDialogOption::UseListViewForComboBoxItems枚举就行- 再此基础上也就在添加一个枚举
InputDialogOption::UsePlainTextEditForTextInput区分QLineEdit和QPlainTextEdit就行
- 默认
另外在看源码的过程中,又开始对其他部分的源码产生一些兴趣
- Qt 的事件循环到底是怎样运行的?
- Qt 的
MOC到底作用是什么,它和信号槽的关系是什么? - 窗口
show(),exec(),open()这个的区别都知道,但是代码到底是怎么实现的? - 窗口
show()又是怎样在电脑上展示出来的? - 等等等
问题还是有点多的,我在开发过程中大多都是知其然,具体怎么用其实都很了解,但是不知其所以然,想在自己的公众号里以写成文章的形式一步一步记录下来,做一些完整的知识输出,让自己有更深的理解
望加油!