QInputDialog源码分析(上)

Posted 会偷懒的程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了QInputDialog源码分析(上)相关的知识,希望对你有一定的参考价值。

前几天的博客中介绍了如何更自由的调整 QInputDialog 类,当时留了一个坑,想完整的学习 QInputDialog 源码的实现,我回来填坑了, 整片文章可能比较冗长,主要是总结了阅读源码过程中一些感受和疑问

了解 QInputDialog 提供的功能

在看源码之前需要确定 QInputDialog 这个类提供了哪些功能?

The QInputDialog class provides a simple convenience dialog to get a single value from the user.
The input value can be a string, a number or an item from a list. A label must be set to tell the user what they should enter.
Five static convenience functions are provided: getText(), getMultiLineText(), getInt(), getDouble(), and getItem().

提供便捷的对话框,可以从用户的输入里获取某个值,这个值可以是字符串,可以是数字,也可以是列表中的一条
并且提供了5个静态函数,从5个静态静态函数的返回值中我们可以估摸出界面里配合显示的控件大概如下

静态函数 描述 搭配的控件
getText() 获取普通字符串 QLineEdit
getMultiLineText() 获取富文本字符串 QPlainTextEdit
getInt() 获取 int 型 QSpinBox
getDouble() 获取 double 型 QDoubleSpinBox
getItem() 获取给定条目中的某一条 QComboBox

为了组织布局,肯定是拥有 QVBoxLayout, 显示提醒用户的描述 QLabel, 以及 QDialogButtonBox 的按键组等等

我们设想了大概需要的控件,同时也设想一下大致的代码逻辑

  • 提供初始化各种控件的接口,根据用户需要对应初始化,比如5个静态函数的方式
  • 提供 获取int,获取double,获取QString 的接口,根据初始化控件的选择对应调用
  • 提供设置对应控件的接口,比如 QSpinBox 设置最大值最小值,以及设置当前值的接口等等

其实整体逻辑很清晰,接下可以针对源码看一些边边角角的东西

QInputDialogPrivate

QT 是使用 D指针 将真正的对象封装在对应的 *Private 中, 例如 QInputDialog 封装在 QInputDialogPrivate 中,先简单对 QInputDialogPrivate 提供给 QInputDialog 类的接口做一个简单的介绍,之后会针对 QInputDialog 的接口如何调用 QInputDialogPrivate 接口详细分析

class QInputDialogPrivate : public QDialogPrivate
{
    Q_DECLARE_PUBLIC(QInputDialog)
public:
    QInputDialogPrivate();
    // 初始化布局
    void ensureLayout();

    // 根据 InputDialogOption 是否是 UsePlainTextEditForTextInput 来决定初始化 QLineEidt 还是 QPlainTextEdit
    void ensureLineEdit();
    void ensurePlainTextEdit();

    // QInputDialog 使用 setComboBoxEditable(bool editable) 或者 setComboBoxItems(const QStringList &items) 时,会初始化 QComboBox 的布局,
    // 再根据 InputDialogOption 是否是UseListViewForComboBoxItems 来决定是否初始化 QListView
    void ensureComboBox();
    void ensureListView();

    // 初始化 QSpinBox
    void ensureIntSpinBox();

    // 初始化 QDoubleSpinBox
    void ensureDoubleSpinBox();

    // 相比于 QLineEdit,QPlainTextEdit 的默认值可以是空,仅仅只是返回一个用户给定的值, ok 按钮不需要状态
    // 但是 QSpinBox 和 QDoubleSpinBox(继承 QAbstractSpinBox )控件是存在一个有效值范围,检测当前值是否有效,来决定 ok 按钮的状态(值无效,OK按钮不可用,值有效,OK按钮可用)
    // 这个函数主要就是通过信号槽的建立,来实现此功能
    void ensureEnabledConnection(QAbstractSpinBox *spinBox);

    // 设置弹窗中最后真正显示的控件, 这个 widget 可以是 QLineEdit,QPlainTextEdit 等中的一个
    void setInputWidget(QWidget *widget);

    // QString 返回值的有3种, QLineEdit,QPlainTextEdit,QComboBox 这里会根据 InputDialogOptions 来选择具体选择哪种控件
    void chooseRightTextInputWidget();

    // 设置窗口弹出时控件上显示的值
    // QLineEdit 可以直接调用 setText(const QString &text)
    // QPlainTextEdit 直接调用 setPlainText(const QString &text)
    // QSpinBox 调用 setValue(int val)
    // QDoubleSpinBox 调用 setValue(double val)
    // QComboBox 存在一个先 findText 是否存在的过程,对于 QComboBox 和 QListView 所以单独封装成2个函数
    void setComboBoxText(const QString &text);
    void setListViewText(const QString &text);

    // 获取 listview 中的值
    QString listViewText() const;

    void ensureLayout() const const_cast<QInputDialogPrivate *>(this)->ensureLayout(); }
    bool useComboBoxOrListView() const return comboBox && comboBox->count() > 0; }

    // 这2个函数是 QLineEdit 和 QPlainTextEdit 控件 textChanged(QString) 信号绑定的槽函数
    void _q_textChanged(const QString &text);
    void _q_plainTextEditTextChanged();

    // QListView 行发生改变的时候,触发的槽函数
    void _q_currentRowChanged(const QModelIndex &newIndex, const QModelIndex &oldIndex);

    // 控件对象
    mutable QLineEdit *lineEdit;
    mutable QPlainTextEdit *plainTextEdit;
    mutable QSpinBox *intSpinBox;
    mutable QDoubleSpinBox *doubleSpinBox;
    mutable QComboBox *comboBox;
    mutable QListView *listView;

    // 最后界面呈现时的布局, QLabel, QWidget 以及按钮组 QDialogButtonBox
    mutable QVBoxLayout *mainLayout;
    mutable QLabel *label;
    mutable QWidget *inputWidget;
    mutable QDialogButtonBox *buttonBox;

    // 保存 InputDialogOptions
    QInputDialog::InputDialogOptions opts;

    // QTextEdit, QPlainTextEdit, QComboBox, QListView 当前显示的值
    QString textValue;

    // QInputDialog open(QObject *receiver, const char *member) 的接口有重载,这2个变量就是保存窗口accepted()关闭后,会触发的信号槽中的对象和接口信息
    QPointer<QObject> receiverToDisconnectOnClose;
    QByteArray memberToDisconnectOnClose;
};

里面对每个函数都做了简单的介绍,一定要对里面描述有个印象,不然后面的介绍可能会看的有点懵

初始化控件相关的函数

ensureLayout()

  • 初始化默认的 QLabel *label
  • 初始化默认的显示的控件 QWidget *inputWidget, 默认显示的是 QLineEdit
  • 初始化按钮组 QDialogButtonBox *buttonBox,默认开始和取消2个按钮
  • 初始化默认布局, QVBoxLayout *mainLayout, 将 label, inputWidget, buttonBox 依次加入布局中,展示

源码如下:

void QInputDialogPrivate::ensureLayout()
{
    Q_Q(QInputDialog);

    if (mainLayout)
        return;

    if (!inputWidget) {
        ensureLineEdit();
        inputWidget = lineEdit;
    }

    if (!label)
        label = new QLabel(QInputDialog::tr("Enter a value:"), q);
#ifndef QT_NO_SHORTCUT
    // 设置 QLabel 快捷键跳转的对象
    label->setBuddy(inputWidget);
#endif
    label->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);

    buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, q);
    QObject::connect(buttonBox, SIGNAL(accepted()), q, SLOT(accept()));
    QObject::connect(buttonBox, SIGNAL(rejected()), q, SLOT(reject()));

    mainLayout = new QVBoxLayout(q);
    //we want to let the input dialog grow to available size on Symbian.
    mainLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
    mainLayout->addWidget(label);
    mainLayout->addWidget(inputWidget);
    mainLayout->addWidget(buttonBox);
    // 因为不确定 inputBox 具体的哪种控件, 所以这里需要对 SpinBox 建立信号槽
    ensureEnabledConnection(qobject_cast<QAbstractSpinBox *>(inputWidget));
    inputWidget->show();
}

ensureLineEdit()

判断 QLineEdit 是否存在后创建,建立信号槽,因为真正展示是交给 inputWidget, 所以 QLineEdit 创建的时候先 hide()

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)));
    }
}

qt_widget_private(lineEdit) 获取 D指针, 设置 QLineEdit 键盘输入的属性 Qt::InputMethodHints

Q_WIDGETS_EXPORT QWidgetPrivate *qt_widget_private(QWidget *widget)
{
    return widget->d_func();
}

ensurePlainTextEdit(), ensureComboBox(), ensureListView(), ensureIntSpinBox()ensureDoubleSpinBox() 代码结构几乎是一样

判断是否存在->不存在创建->设置基础属性->建立信号槽

唯一不同的是信号槽的建立的内容存在差别,简单整理下面一张表

初始化函数 初始化对象 信号 对应的槽函数或信号
ensureLineEdit() QLineEdit textChanged(QString) _q_textChanged(QString)
ensurePlainTextEdit() QPlainTextEdit textChanged() _q_plainTextEditTextChanged()
ensureComboBox() QComboBox editTextChanged(QString)
currentIndexChanged(QString)
_q_textChanged(QString)
ensureListView() QListView currentRowChanged(QModelIndex,QModelIndex) _q_currentRowChanged(QModelIndex,QModelIndex)
ensureIntSpinBox() QInputDialogSpinBox valueChanged(int) SIGNAL(intValueChanged(int))
ensureDoubleSpinBox() QInputDialogDoubleSpinBox valueChanged(double) SIGNAL(doubleValueChanged(double)

intSpinBox 和 doubleSpinBox 的初始化

这2个控件其实只是值的类型不一样,一个是 int 一个是 double, 所以我只以 QSpinBox *intSpinBox 控件为例

void QInputDialogPrivate::ensureIntSpinBox()
{
    Q_Q(QInputDialog);
    if (!intSpinBox) {
        intSpinBox = new QInputDialogSpinBox(q);
        intSpinBox->hide();
        QObject::connect(intSpinBox, SIGNAL(valueChanged(int)),
                         q, SIGNAL(intValueChanged(int)));
    }
}

从头文件里我们知道成员变量 intSpinBox 的类是 QSpinBox, 但是却是用 QInputDialogSpinBox 来初始化的
并且之前我提到 QSpinBox 需要判断值是否合法来设置 OK 按钮的状态,所以我们还需要看一下 QInputDialogSpinBox 的代码

class QInputDialogSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    QInputDialogSpinBox(QWidget *parent)
        : QSpinBox(parent) {
        connect(lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(notifyTextChanged()));
        connect(this, SIGNAL(editingFinished()), this, SLOT(notifyTextChanged()));
    }

signals:
    void textChanged(bool);

private slots:
    void notifyTextChanged() emit textChanged(hasAcceptableInput()); }

// ... 省略了一些键盘鼠标事件函数
};

QSpinBox 的值有改变以及输入结束之后,会触发一个槽函数 notifyTextChanged(), hasAcceptableInput() 检测当前值是否有效, 并发出一个 textChanged(bool) 的信号

前面介绍 ensureLayout() 创建布局的时候,函数的最后手动调用了这个函数 ensureEnabledConnection 这个函数

void QInputDialogPrivate::ensureEnabledConnection(QAbstractSpinBox *spinBox)
{
    if (spinBox) {
        QAbstractButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
        QObject::connect(spinBox, SIGNAL(textChanged(bool)), okButton, SLOT(setEnabled(bool)), Qt::UniqueConnection);
    }
}

从这里我们可以看出,当 spinBox 值有效的时候, okButton 设置为可用,反之则设置成不可用

控件对应的槽函数

上面整理一张关于信号槽的表,可以在比照看一下,所有的槽函数

_q_textChanged(const QString &text)

QLineEditQComboBox 控件使用的槽函数,判断 textValue 值是否改变,并赋值和发送值改变的信号

void QInputDialogPrivate::_q_textChanged(const QString &text)
{
    Q_Q(QInputDialog);
    if (textValue != text) {
        textValue = text;
        emit q->textValueChanged(text);
    }
}

_q_plainTextEditTextChanged()

QPlainTextEdit 控件使用的槽函数

void QInputDialogPrivate::_q_plainTextEditTextChanged()
{
    Q_Q(QInputDialog);
    QString text = plainTextEdit->toPlainText();
    if (textValue != text) {
        textValue = text;
        emit q->textValueChanged(text);
    }
}

_q_currentRowChanged(QModelIndex,QModelIndex)

QListView 控件使用的槽函数

void QInputDialogPrivate::_q_currentRowChanged(const QModelIndex &newIndex,
                                            const QModelIndex & /* oldIndex */)
{
    _q_textChanged(comboBox->model()->data(newIndex).toString());
    buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}

剩下的2个主要函数

介绍到这里,其实只剩下2个比较关键的函数 setInputWidget(QWidget *widget)chooseRightTextInputWidget()

setInputWidget(QWidget *widget)

设置弹窗中 inputWidget 最后展示的控件

这里需要 移除旧布局里 inputWidget,以及旧 inputWidget 的信号槽关系,以及为新的 inputWidget 创建新的信号槽关系
同时根据控件的类型,设置了当前显示的值

void QInputDialogPrivate::setInputWidget(QWidget *widget)
{
    Q_ASSERT(widget);
    if (inputWidget == widget)
        return;

    if (mainLayout) {
        Q_ASSERT(inputWidget);
        mainLayout->removeWidget(inputWidget);
        inputWidget->hide();
        mainLayout->insertWidget(1, widget);
        widget->show();

        // disconnect old input widget
        QAbstractButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
        if (QAbstractSpinBox *spinBox = qobject_cast<QAbstractSpinBox *>(inputWidget))
            QObject::disconnect(spinBox, SIGNAL(textChanged(bool)), okButton, SLOT(setEnabled(bool)));

        // connect new input widget and update enabled state of OK button
        QAbstractSpinBox *spinBox = qobject_cast<QAbstractSpinBox *>(widget);
        ensureEnabledConnection(spinBox);
        okButton->setEnabled(!spinBox || spinBox->hasAcceptableInput());
    }

    inputWidget = widget;

    // synchronize the text shown in the new text editor with the current
    // textValue
    if (widget == lineEdit) {
        lineEdit->setText(textValue);
    } else if (widget == plainTextEdit) {
        plainTextEdit->setPlainText(textValue);
    } else if (widget == comboBox) {
        setComboBoxText(textValue);
    } else if (widget == listView) {
        setListViewText(textValue);
        ensureLayout();
        buttonBox->button(QDialogButtonBox::Ok)->setEnabled(listView->selectionModel()->hasSelection());
    }
}

chooseRightTextInputWidget()

我们知道 int 时, 直接初始化 intSpinBox, double 则直接初始化 doubleSpinBox, 但是 QString 的控件一共有4种

  • QListView
  • QComboBox
  • QPlainTextEdit
  • QLineEidt

所以根据 opts, 来区分应该真正初始化哪一种控件

void QInputDialogPrivate::chooseRightTextInputWidget()
{
    QWidget *widget;

    // bool useComboBoxOrListView() const { return comboBox && comboBox->count() > 0; }
    if (useComboBoxOrListView()) {
        if ((opts & QInputDialog::UseListViewForComboBoxItems) && !comboBox->isEditable()) {
            ensureListView();
            widget = listView;
        } else {
            widget = comboBox;
        }
    } else if (opts & QInputDialog::UsePlainTextEditForTextInput) {
        ensurePlainTextEdit();
        widget = plainTextEdit;
    } else {
        ensureLineEdit();
        widget = lineEdit;
    }

    setInputWidget(widget);

    if (inputWidget == comboBox) {
        _q_textChanged(comboBox->currentText());
    } else if (inputWidget == listView) {
        _q_textChanged(listViewText());
    }
}

总结

源码分析上篇主要介绍 QInputDialogPrivate 提供的所有接口,这里有几个重点需要重新记录一下

  • QInputDialogPrivate 并不是在初始化出控件后直接显示到布局,而是通过 setInputWidget(QWidget *widget) 函数将控件赋值给 QWidget *inputWidget 在最后显示
  • intdouble 可以从 intSpinBoxdoubleSpinBox 控件中直接获取,但是 QString 对应的控件种类有点多,为了避免 QInputDialog 获取值需要判断具体应该从哪一种控件中取值, QInputDialogPrivateQString 的值存储到成员变量 QString textValue 中, 简洁了获取值的判断
  • 获取值的方式简单,但是具体区分使用哪种显示 QString 控件,还是引入了很多枚举(这一部分会在下一篇博客中介绍)
    • 区分是 intdoubleQStringInputMode
    • 区分 QString 具体使用哪种控件的 InputDialogOption, 而且从 chooseRightTextInputWidget() 可以看出,区分 QComboBox 和 其他控件( plainTextEdit, lineEdit) 竟然使用 combobox 是否初始化和列表中是否有值来判断的,既然使用了枚举,为何不完全区分开?

在源码分析下篇主要介绍 QInputDialog 类, 和我当时阅读源码过程中难度大的部分进行分析


以上是关于QInputDialog源码分析(上)的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段

鼠标事件中的 QInputDialog

从在非 GUI 线程中运行的 C 代码获取 QInputDialog::getText() 结果

如何将 QInputDialog 设置为模态

《Docker 源码分析》全球首发啦!