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()
又是怎样在电脑上展示出来的? - 等等等
问题还是有点多的,我在开发过程中大多都是知其然,具体怎么用其实都很了解,但是不知其所以然,想在自己的公众号里以写成文章的形式一步一步记录下来,做一些完整的知识输出,让自己有更深的理解
望加油!