11.1 QWidget 多窗口使用

本节介绍基于 QWidget 自定义的多窗口程序,定制子窗口并弹窗显示,在多窗口之间使用信号和槽机制进行多窗口之间的数据传递。多窗口程序可以采用新建子窗口的方式,也可以集成之前开发的窗口模块 作为子窗口,本节通过新建和集成两种方式展示子窗口的使用。

11.1.1 QWidget 类

QWidget 是用户界面所有控件和窗口的基类,涵盖控件和窗口所有基本的功能,比如界面绘制显示、鼠标键盘事件处理等,本节仅介绍一部分窗口显示常用的功能函 数。
(1)构造函数
QWidget(QWidget * parent = 0, Qt::WindowFlags f = 0)
参数 parent 如果为默认值 0,也就是 NULL,那么新建的 QWidget 对象就是独立的窗口,独立窗口有自己的标题栏和边框;
如果指定 parent 为已有的窗口或控件,那么新建的 QWidget 就是子控件,子控件的显示受制于父窗口或父控件。
本节就是专门讲解 QWidget 独立窗口的工作模式,在新建窗口时,不设置 parent 指针或者设为 NULL。
Qt::WindowFlags 可以控制 QWidget 对象的多种工作类型,比如作为窗口、对话框、控件、菜单显示等等,通常不需要修改窗口标志位,每个功 能控件都会自行设置合适的工作类型。

(2)显示与隐藏函数
我们通过 QtCreator 新建的窗口会有默认的标题栏文本和默认尺寸,可以直接在主窗口调用子窗口的显示函数:
void QWidget::show()   //槽函数,显示窗口
void QWidget::hide()    //槽函数,隐藏窗口
bool    isVisible() const  //是否处于显示状态
virtual void    setVisible(bool visible)  //控制窗口显示或隐藏
show() 是显示窗口槽函数,hide() 是隐藏窗口槽函数,可以通过信号和槽的关联方便控制窗口显示状态,setVisible(bool visible) 一样可以控制窗口显示或隐藏,参数 true 代表显示, false 代表隐藏。
通常新的子窗口会显示到主窗口的上层,多个窗口的显示可能出现区域重叠,上层的窗口会遮挡下层窗口显示,例如:
multi01
窗口本身是二维显示的,横轴 X, 纵轴 Y,多个窗口的上下层叠加,属于 Z 轴排列,上层的显示会遮挡下层窗口。
控制窗口在 Z 轴的叠加使用下面两个函数:
void QWidget::raise()      //槽函数,将本窗口置于顶层显示
void QWidget::lower()    //槽函数,将本窗口置于底层显示
raise() 是将窗口放在本程序所有窗口的 Z 轴最上面显示,lower()  则是放到最底层显示。
用户鼠标点击也会自动切换窗口的 Z 轴位置,鼠标最新点击的窗口通常优先在 Z 轴顶层显示。
程序的多个窗口通常同一时刻只有一个窗口处于活跃状态,就是用户鼠标键盘输入的焦点,鼠标点击哪个窗口,哪个窗口自动显示到最顶层,使用下面函数可以 激活窗口,获 取输入焦点:
void QWidget::​activateWindow()  //激活本窗口,注意本窗口必须处于显示状态才有用
bool    isActiveWindow() const    //判断是否为活跃状态
也可以使用 raise() 激活窗口获取输入焦点,处于顶层显示。

对于窗口,如果用户点击标题栏的关闭按钮 X,或者程序调用关闭函数:
bool QWidget::close()
窗口就会被关闭,关闭窗口函数首先会隐藏窗口,然后会触发 QCloseEvent,在这个事件里面可以处理关闭窗口的事务。close() 函数默认情况下不会销毁窗口,当Qt 图形界面程序的所有窗口被关闭时,程序会自动结束,这时才会销毁所有窗口。子窗口调用 close() 函数之后,子窗口对象默认情况下仍保留内存空间,还是一直存在的,成员变量和成员函数可以照常使用,可以通过 show() 函数重新显示。

QWidget 窗口标题栏默认有最小化、最大化和关闭按钮,用户点击这些按钮会控制窗口的显示形态,通过函数也可以控制窗口的最大化最小化显示:
void QWidget::​showMinimized()  //槽函数,最小化显示
void QWidget::​showMaximized() //槽函数,最大化显示,标题栏和窗口主体都显示,占据桌面最大显示区域
void QWidget::​showNormal()      //槽函数,正常尺寸显示
void QWidget::​showFullScreen()  //槽函数,全屏显示,标题栏隐藏,将窗口主体铺满屏幕
 程序可以通过函数获取窗口的显示状态,如 isMinimized()、isMaximized() 、isFullScreen()、isVisible()、isHidden() 等。
默认情况下,程序的多个窗口之间是显示优先级是平等的(非模态显示),有一种特殊的显示方式,叫模态显示,这种窗口会独占显示焦点,强制显示在最上 层,不关闭模态窗口,就无法操作底层其他窗口。QWidget 有个特殊属性 windowModality 控制窗口的模态显示:
Qt::WindowModality    windowModality() const  //获取模态状态
void    setWindowModality(Qt::WindowModality windowModality)  //设置模态状态
bool    isModal() const   //是否为模态窗口
Qt::WindowModality 枚举类型有三种:

Qt::WindowModality 枚举常量 数值 描述
Qt::NonModal 0 非模态窗口,不会阻塞其他窗口输入。
Qt::WindowModal 1 窗口级模态,在兄弟级别、父级窗口及祖父以上级别窗口中,阻塞其他窗口,独占输入焦 点。
Qt::ApplicationModal 2 应用程序级模态,在本应用程序所有窗口中,阻塞其他窗口,独占输入焦点。

模态窗口一般设置为 Qt::ApplicationModal 即可,就是独占本程序的输入焦点,模态窗口不关闭,其他窗口都不能使用。
设置模态窗口时,要注意先设置模态属性,然后再显示窗口;
如果窗口已经显示了,再设置模态不会即刻生效,需要调用 hide() 隐藏窗口,然后重新 show() 显示窗口。
模态窗口显示举例:
    pModWnd->setWindowModality(Qt::ApplicationModal);  //模态窗口,会阻塞其他窗口
    pModWnd->show(); //模态窗口总是显示在最顶层,并且独占输入焦点

非模态窗口显示举例:
    pNormalWnd->show();  //普通窗口显示,多个窗口都能操作,不会阻塞其他窗口
    pNormalWnd->raise();   //将窗口置于顶层显示,方便用户操作

(3)标题栏设置函数
窗口标题栏可以设置文本、图标,标题栏文本也就是窗口名称,方便直接说明窗口功能:
QString    windowTitle() const    //获取标题栏文本
void    setWindowTitle(const QString &) //设置标题栏文本
QIcon    windowIcon() const      //获取标题栏图标
void    setWindowIcon(const QIcon & icon)  //设置标题栏图标
QString    windowIconText() const   //获取图标的文本
void    setWindowIconText(const QString &) //设置图标的文本

(4)窗口尺寸位置设置函数
用户使用鼠标拉动窗口边框可以改变窗口尺寸,拖动标题栏可以控制窗口位置,通过函数也可以获取或修改窗口尺寸位置,相关函数如下面两个表格所示:

窗口尺寸函数 描述
QSize    size() const 获取窗口客户区尺寸。
void    resize(int w, int h) 根据宽度高度设置窗口尺寸。
void    resize(const QSize &) 设置窗口尺寸。
int    width() const 获取当前宽度。
int    height() const 获取当前高度。
QSize    minimumSize() const 获取最小尺寸。
void    setMinimumSize(const QSize &) 设置最小尺寸。
void    setMinimumSize(int minw, int minh) 设置最小尺寸。
int    minimumWidth() const 获取最小宽度。
void    setMinimumWidth(int minw) 设置最小宽度。
int    minimumHeight() const 获取最小高度。
void    setMinimumHeight(int minh) 设置最小高度。
QSize    maximumSize() const
获取最大尺寸。
void    setMaximumSize(const QSize &) 设置最大尺寸。
void    setMaximumSize(int maxw, int maxh)
设置最大尺寸。
int    maximumWidth() const 获取最大宽度。
void    setMaximumWidth(int maxw)
设置最大宽度。
int    maximumHeight() const 获取最大高度。
void    setMaximumHeight(int maxh)
设置最大高度。
QRect    rect() const 获取窗口客户区矩形,等同于  QRect(0, 0, width(), height()) 。

QSize 包括两个数值:宽度 width() ,高度 height() 。
QRect 矩形包括四个数值:左上角起点横坐标 x(),左上角起点纵坐标 y() ,矩形宽度 width() ,矩形高度 height() 。

移动窗口或获取窗口位置的函数:

窗口位置函数 描述
QPoint    pos() const 获取窗口左上角起点位置坐标。
void    move(int x, int y) 移动窗口位置,使左上角起点到指定坐标。
void    move(const QPoint &) 移动窗口位置,使左上角起点到指定坐标。
int    x() const 窗口左上角起点位置的横轴坐标。
int    y() const 窗口左上角起点位置的纵轴坐标。
const QRect &    geometry() const 窗口客户区几何矩形(不含标题栏边框)。
void setGeometry(int x, int y, int w, int h) 设置窗口客户区几何矩形,同时设置坐标和尺寸。
void    setGeometry(const QRect &) 设置窗口客户区几何矩形。
QRect    frameGeometry() const 窗口包含标题栏边框的整体几何矩形。
QSize    frameSize() const 窗口包含标题栏边框的整体尺寸。

窗口尺寸位置有些函数的计算包含标题和边框,例如:
x(),  y(),  frameGeometry(),  pos(),  move() 。
另一些函数计算不包括标题栏和边框,就是单指客户区尺寸位置:
geometry(),  width(),  height(),  rect(), size() 。


通过下图直观说明尺寸位置计算:
geo
绿色箭头包含标题栏边框,是完整的窗口矩形计算,紫色的箭头是窗口客户区矩形计算。
一般来说,移动窗口位置时,我们按照窗口整体来计算;
而缩放窗口尺寸时,按照窗口客户区来计算。

11.1.2 新建窗口类方式

下面我们通过一个密码管理工具例子,学习新建窗口类的方式,使用多个窗口。
主窗口显示用户名和密码哈希值列表,通过新建的子窗口来完成修改用户密码的功能。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 passwordtool,创建路径 D:\QtProjects\ch11,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,注意修改主窗口类名为 MainWidget,这 样与后续的子窗口类名作区分,然后点击下一步;
④项目管理不修改,点击完成。
主窗口类名为 MainWidget,对应的界面文件、头文件、源文件名就是 mainwidget.ui、mainwidget.h、mainwidget.cpp 。
接下来我们在项目管理器右击项目根 passwordtool ,右键菜单选择“添加新文件...”,进入新建文件对话框:
new1
在上面对话框左边一栏选择 “Qt”,然后在中间一栏选择“Qt设计师界面类”,然后点击右下角“Choose...”按钮,进入Qt设计师界面类的选择界面:
new2
在中间上半区选择“Widget”,然后点击“下一步”按钮,进入类名编辑界面:
new3
我们只需要把类名修改为 FormChangePassword ,下面的头文件、源文件、界面文件名自动修改,不需要手动调整,修改类名之后点击“下一步”,
new4
项目管理界面不用动,直接点击“完成”按钮。这样项目就增加了新类的文件:
formchangepassword.cpp,formchangepassword.h,formchangepassword.ui。
现在我们有两套界面类,一套是 MainWidget 类主窗口的文件,另一套是 FormChangePassword 类子窗口的文件。
我们从主窗口开始编辑,我们打开 mainwidget.ui 文件,然后拖入控件:
mainui01
第一行控件分别是标签“用户名”,单行编辑器 lineEditUser,标签“密码”,单行编辑器 lineEditPassword,“添加新用户”按钮 pushButtonAddUser 。
第二行左边是列表控件 listWidgetShow ,右边是两个按钮,“修改用户密码”按钮 pushButtonChangePassword,“删除选定用户”按钮 pushButtonDelUser 。“修改用户密码”按钮就是负责弹出子窗口的功 能按钮。
第一行控件按照水平布局;
第二行先将 pushButtonChangePassword、pushButtonDelUser  两个按钮垂直布局,然后再与 listWidgetShow 进行水平布局,如下图所示:
mainui02
然后我们选中主窗口 MainWidget,点击上面垂直布局按钮,主窗口的主布局器是垂直布局:
mainui03
布局完成后,我们修改窗口的尺寸为 518* 300。
然后我们依次右键点击三个按钮,在右键菜单为它们添加 clicked() 信号对应槽函数:
slot
添加好主界面三个按钮的槽函数之后,我们保存并关闭 mainwidget.ui 文件。

接下来我们打开子窗口的界面文件 formchangepassword.ui ,拖入如下控件:
childui01
第一行是标签“用户名”,单行编辑器 lineEditUser;
第二行是标签“旧密码”,单行编辑器 lineEditOldPassword;
第三行是标签“新密码”,单行编辑器 lineEditNewPassword;
第四行是标签“新密码确认”,单行编辑器 lineEditNewPassword2;
第五行是“修改密码”按钮 pushButtonChange,按钮尺寸拉宽,与上一行两个控件的累计宽度差不多 。
在右边对象树,选中窗口 FormChangePassword ,然后点击上面的 网格布局器,该窗口使用一个网格布局即可,设置布局后把窗口大小设置为 280 * 240 ,如下图所示:
childui02
布局设置完成后,我们右击子窗口的 “修改密码”按钮,为该按钮也添加 clicked() 信号对应槽函数 。
我们右击添加的槽函数自动归属于各自的窗口类,在主窗口右击添加的槽函数自动放到主窗口类,在子窗口右击添加的槽函数自动放到子窗口类。
按钮的槽函数都添加完成后,我们保存 formchangepassword.ui 文件并关闭该文件。

下面我们先从主窗口代码开始编辑,首先是 mainwidget.h 头文件:
#ifndef MAINWIDGET_H
#define MAINWIDGET_H

#include <QWidget>
#include <QCryptographicHash> //计算密码哈希值
#include <QMap>  //保存用户名-密码映射
#include "formchangepassword.h"  //子窗口类
#include <QListWidget>
#include <QListWidgetItem>

namespace Ui {
class MainWidget;
}

class MainWidget : public QWidget
{
    Q_OBJECT

public:
    explicit MainWidget(QWidget *parent = 0);
    ~MainWidget();    

    //更新列表控件的显示
    void UpdateListShow();

signals:
    //发送旧的用户名密码哈希给子窗口,用于修改密码
    void SendOldUserPassword(QString strUser, QByteArray baOldHash);

private slots:
    void on_pushButtonAddUser_clicked();

    void on_pushButtonChangePassword_clicked();

    void on_pushButtonDelUser_clicked();

    //手动添加的槽函数,从子窗口接收新的密码哈希值
    void RecvNewUserPassword(QString strUser, QByteArray baNewHash);

private:
    Ui::MainWidget *ui;
    //保存用户名和密码哈希值
    QMap<QString, QByteArray> m_mapUserAndHash;
    //子窗口对象指针
    FormChangePassword *m_pFormChild;
    //初始化设置
    void Init();

};

#endif // MAINWIDGET_H
我们添加了多个类的头文件包含,QCryptographicHash 类专门用于密码哈希值计算,涵盖常见的密码哈希算法,比如 MD5、SHA256 等,密码一般不建议明文直接存储,容易泄密,所以都是将密码通过哈希算法生成哈希值存储。哈希算法是单向计算函数,可以从明文推算哈希值密文,但是反过来难以计算,从而实 现较为安全的密码存储方式。密码比对时,不需要比对明文密码,只需要验证两个密码的哈希值是否一致。
我们添加 QMap 模板类声明,用于保存用户名和密码哈希值的键值对;formchangepassword.h 是子窗口类的头文件;QListWidget 和 QListWidgetItem 是列表控件和列表条目的类。
在 MainWidget 类声明中,我们手动添加更新列表控件显示的函数 UpdateListShow()。
手动添加信号 SendOldUserPassword(QString strUser, QByteArray baOldHash),用于给修改密码子窗口发送用户名和旧密码哈希值。
三个按钮的槽函数是之前界面文件编辑时添加的,后面我们手动添加槽函数 RecvNewUserPassword(QString strUser, QByteArray baNewHash),用于从子窗口接收用户名和修改后的新密码哈希值。
我们添加 m_mapUserAndHash 模板类对象,保存用户名和密码哈希值的键值对;
m_pFormChild 是子窗口的指针,用于弹窗修改用户的密码;
Init() 是用于窗口初始化的代码,在该函数里面新建子窗口。

例子中主窗口和子窗口通过信号和槽函数交互数据,如下图所示:
sig-slot
需要发送数据时,通过 emit 触发信号即可。
下面我们分段编辑主窗口的源文件 mainwidget.cpp,首先是构造函数和初始化函数内容:
#include "mainwidget.h"
#include "ui_mainwidget.h"
#include <QMessageBox>
#include <QDebug>

MainWidget::MainWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MainWidget)
{
    ui->setupUi(this);
    //初始化设置
    Init();
}

MainWidget::~MainWidget()
{
    //删除子窗口
    delete m_pFormChild;    m_pFormChild = NULL;
    delete ui;
}

//初始化设置
void MainWidget::Init()
{
    //设置窗口标题栏
    setWindowTitle( tr("用户名密码管理工具") );
    //密码编辑框隐藏显示
    ui->lineEditPassword->setEchoMode(QLineEdit::Password);

    m_pFormChild = NULL;  //初始化为 NULL
    //新建子窗口,窗口构造时参数里的 parent 必须是 0  NULL
    m_pFormChild = new FormChangePassword(NULL);

    //关联信号和槽
    //主窗口发送旧的用户名密码哈希给子窗口
    connect(this, SIGNAL(SendOldUserPassword(QString,QByteArray)),
            m_pFormChild, SLOT(RecvOldUserPassword(QString,QByteArray)) );
    //子窗口发送新的用户名密码哈希给主窗口
    connect(m_pFormChild, SIGNAL(SendNewUserPassword(QString,QByteArray)),
            this, SLOT(RecvNewUserPassword(QString,QByteArray)) );
}
构造函数末尾添加了 Init() 函数调用,析构函数里面添加了一行删除子窗口对象的代码。
Init() 函数首先设置主窗口的标题栏文本为 "用户名密码管理工具" ;
将密码编辑框的回显模式修改为 QLineEdit::Password ,就是隐藏输入的字符,用星号或者黑圆点代替;
我们先将 m_pFormChild 初始化为 NULL 空指针,然后新建子窗口对象,保存到 m_pFormChild,新建窗口时,参数里的 parent 必须为 0 或 NULL;
然后我们将主窗口和子窗口的信号、槽函数进行关联,主窗口将用户名、旧密码传递给子窗口,子窗口修改密码后,再将用户名和新密码回传给主窗口。

接下来我们编辑更新列表控件显示的函数,当用户名密码列表变化时,调用这个函数更新显示:
//更新列表控件的显示
void MainWidget::UpdateListShow()
{
    //清除旧列表
    ui->listWidgetShow->clear();
    //根据映射重新显示
    int nCount = m_mapUserAndHash.count();
    //获取所有key,就是用户名
    QList<QString> listKeys = m_mapUserAndHash.keys();
    for(int i=0; i<nCount; i++)
    {
        QString curKey = listKeys[i];
        QString strTemp = curKey + QString("\t") + m_mapUserAndHash[curKey];
        ui->listWidgetShow->addItem( strTemp );
    }
}
我们先清空列表控件旧的内容,然后获取用户密码键值对的计数,获取所有的键,也就是用户名列表;
然后循环处理映射对象中每个键值对,将用户名和密码哈希值通过 "\t" 拼接成一个字符串,添加为列表控件的一行。
循环结束之后,就完成了列表控件的更新。

接下来我们编辑“添加新用户”按钮槽函数的代码:
//添加新用户
void MainWidget::on_pushButtonAddUser_clicked()
{
    //获取用户名和密码字符串
    QString strNewUser = ui->lineEditUser->text().trimmed();
    QString strPassword = ui->lineEditPassword->text().trimmed();
    if( strNewUser.isEmpty() || strPassword.isEmpty() )
    {
        QMessageBox::information(this, tr("用户名密码检查"), tr("用户名或密码为空,不能添加。"));
        return;
    }
    //判断是否已存在该用户
    if( m_mapUserAndHash.contains(strNewUser) )
    {
        QMessageBox::information(this, tr("用户名检查"), tr("已存在该用户名,不能再新增同名。"));
        return;
    }
    //新的用户名,计算哈希
    QByteArray baNewHash = QCryptographicHash::hash( strPassword.toUtf8(),
                                                     QCryptographicHash::Sha256 );
    //二进制哈希转为Hex字符串
    baNewHash = baNewHash.toHex();
    //新增用户名
    m_mapUserAndHash.insert( strNewUser, baNewHash );
    //更新显示
    UpdateListShow();
}
该函数先获取用户名和密码的字符串,字符串剔除两端的空白;
判断用户名 strNewUser 和密码 strPassword 字符串是否为空,如果有一个为空就弹出信息框提示,然后返回;
只有当用户名和密码字符串都不空时,进入后续的处理。
判断 strNewUser 是否为映射对象已有的键,就是检查用户名是否重复,如果重复就弹出信息框提示,然后返回;
当没有重复用户名时,才进行后续的添加操作。
我们调用静态函数 QCryptographicHash::hash() 计算密码的 SHA256 哈希值,直接得到的字节数组是二进制格式,不便于显示,因此转为 Hex 十六进制字符串,存到 baNewHash,例如原本一字节 0xa1 的二进制转为两个字符 "a1" 。
SHA256 哈希值原本 256 比特(32 字节),转换后就是 64 个十六进制字符。
我们将用户名和新的哈希字符串存到映射对象 m_mapUserAndHash,并调用 UpdateListShow() 更新列表控件显示。

接下来我们编辑“删除选定用户”按钮的槽函数:
//删除选中行的用户
void MainWidget::on_pushButtonDelUser_clicked()
{
    //列表当前行号
    int curIndex = ui->listWidgetShow->currentRow();
    if( curIndex < 0 )
    {
        return;
    }
    //当前行号的条目
    QListWidgetItem *curItem = ui->listWidgetShow->item( curIndex );
    if( curItem->isSelected() )  //处于选中状态,才删除该行
    {
        //该条目处于选中状态
        QString curLine = curItem->text();
        QStringList curKeyValue = curLine.split( '\t' );
        //删除键值对
        m_mapUserAndHash.remove( curKeyValue[0] );
        //卸下条目
        ui->listWidgetShow->takeItem(curIndex);
        //删除条目
        delete curItem; curItem = NULL;
    }
}
我们获取列表控件的当前行号,如果行号为 -1 就不处理;
行号合法时,我们获取该行的条目存到 curItem 指针;
判断该条目是否处于选中状态,只有选中该条目的情况才进行删除操作:
获取该行条目的文本,使用 '\t' 切分字符串,切分后形成两个字符串,第 0 段是用户名,第 1 段是密码哈希字符串;
我们根据用户名删除映射对象里匹配的键值对,就删除了该用户和哈希值;
从列表控件卸下该行条目,并手动删除卸下条目的内存,这样就完成了用户的删除和界面列表控件的更新。

接下来我们编辑“修改用户密码”按钮的槽函数代码:
//弹出子窗口,进行密码修改
void MainWidget::on_pushButtonChangePassword_clicked()
{
    //列表当前行号
    int curIndex = ui->listWidgetShow->currentRow();
    if( curIndex < 0 )
    {
        return;
    }
    //当前行号的条目
    QListWidgetItem *curItem = ui->listWidgetShow->item( curIndex );
    if( curItem->isSelected() )  //处于选中状态,才会修改该行密码
    {
        //该条目处于选中状态
        QString curLine = curItem->text();
        QStringList curKeyValue = curLine.split( '\t' );
        QString strUser = curKeyValue[0];
        QByteArray baOldHash = m_mapUserAndHash[strUser];
        //发送用户名和哈希给子窗口
        emit SendOldUserPassword(strUser, baOldHash);

        //显示修改密码的子窗口
        m_pFormChild->show();
        //如果子窗口被最小化,显示原本尺寸的窗口
        if( m_pFormChild->isMinimized() )
        {
            m_pFormChild->showNormal();
        }
        m_pFormChild->raise();
    }
}
该函数开头也是获取列表控件当前行序号 curIndex,并判断如果行号为 -1,直接返回不处理。
行号合法时才获取该行的条目 curItem,并判断条目处于选中状态时,才进行修改密码的操作:
我们获取该行条目的文本,并按照 '\t' 进行拆分,得到两段字符串,前半截 curKeyValue[0] 是用户名字符串 strUser, 后半截是密码哈希字符串,我们这里从映射对象读取哈希字符串,存到 baOldHash,这样得到的哈希数据类型是 QByteArray 。
得到用户名和旧密码哈希之后,我们触发信号 SendOldUserPassword(strUser, baOldHash) ,将数据交给子窗口槽函数处理。
然后我们弹出子窗口 m_pFormChild 显示,如果子窗口之前被用户操作最小化了,那么使用 m_pFormChild->showNormal() 显示还原正常尺寸的子窗口;最后调用 m_pFormChild->raise() ,让子窗口显示到最前面,成为用户操作焦点。后续的密码修改操作由子窗口处理。

当子窗口的密码修改操作完成后,会发送用户名和新的密码哈希给主窗口, 我们在主窗口通过 RecvNewUserPassword() 槽函数接收这些数据,主窗口这个槽函数代码如下:
//手动添加的槽函数,从子窗口接收新的密码哈希值
void MainWidget::RecvNewUserPassword(QString strUser, QByteArray baNewHash)
{
    //修改用户的密码哈希值
    m_mapUserAndHash[strUser] = baNewHash;
    //更新显示
    UpdateListShow();

    //修改完成,隐藏子窗口
    m_pFormChild->hide();
    //提示修改完成
    QMessageBox::information(this, tr("修改密码"), tr("修改密码成功。"));
}
我们根据收到的用户名和新密码哈希,修改映射对象的键值对,然后更新列表控件显示;
因为修改操作完成了,所以我们调用 m_pFormChild->hide() 隐藏子窗口;
最后弹窗提示,修改密码成功。

以上就是主窗口的代码,下面我们编辑子窗口的头文件 formchangepassword.h 代码:
#ifndef FORMCHANGEPASSWORD_H
#define FORMCHANGEPASSWORD_H

#include <QWidget>
#include <QCryptographicHash> //计算密码哈希值

namespace Ui {
class FormChangePassword;
}

class FormChangePassword : public QWidget
{
    Q_OBJECT

public:
    explicit FormChangePassword(QWidget *parent = 0);
    ~FormChangePassword();

signals:
    //发送新的密码哈希给主窗口
    void SendNewUserPassword(QString strUser, QByteArray baNewHash);

private slots:
    void on_pushButtonChange_clicked();

    //手动添加的槽函数,从主窗口接收旧的用户名和密码哈希
    void RecvOldUserPassword(QString strUser, QByteArray baOldHash);

private:
    Ui::FormChangePassword *ui;
    //初始化设置
    void Init();
    //保存用户名和密码哈希值
    QString m_strUser;
    QByteArray m_baOldHash;
};

#endif // FORMCHANGEPASSWORD_H
子窗口头文件也添加 QCryptographicHash 密码哈希计算类的包含,后面要计算密码哈希值。
子窗口类的声明里面,我们手动添加了信号 SendNewUserPassword(QString strUser, QByteArray baNewHash), 用于回传用户名新密码哈希值给主窗口;
手动添加了槽函数 RecvOldUserPassword(QString strUser, QByteArray baOldHash),用于从主窗口接收用户名和旧密码哈希值。
然后添加了子窗口初始化函数 Init(),以及两个成员变量 m_strUser、m_baOldHash,存储用户名和旧密码哈希值。

接下来我们分段编辑子窗口的源文件 formchangepassword.cpp 代码,首先是构造函数和初始化部分:
#include "formchangepassword.h"
#include "ui_formchangepassword.h"
#include <QMessageBox>
#include <QDebug>

FormChangePassword::FormChangePassword(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::FormChangePassword)
{
    ui->setupUi(this);
    //初始化
    Init();
}

FormChangePassword::~FormChangePassword()
{
    delete ui;
}

//初始化设置
void FormChangePassword::Init()
{
    //修改窗口标题栏
    setWindowTitle( tr("修改用户密码") );
    //设置密码隐藏
    ui->lineEditOldPassword->setEchoMode( QLineEdit::Password );
    ui->lineEditNewPassword->setEchoMode( QLineEdit::Password );
    ui->lineEditNewPassword2->setEchoMode( QLineEdit::Password );

    //用户名为只读
    ui->lineEditUser->setReadOnly(true);
    ui->lineEditUser->setStyleSheet( "background-color: rgb(200,200,255);" );

    //工具提示
    ui->lineEditOldPassword->setToolTip( tr("旧密码验证成功才能修改为新密码。") );
}
构造函数末尾添加了初始化函数 Init() 调用。
Init() 函数首先修改子窗口标题栏文本为 "修改用户密码",直观说明这个子窗口的功能;
然后把三个密码编辑框的回显模式修改为密码模式,就是将输入字符用星号或黑圆点代替显示;
接着把用户名的编辑框设置为只读,并设置用户名编辑框的背景为浅蓝色,这个子窗口功能只是改密码,不能修改用户名。
通常用户名是唯一的,不能随便修改,所以这里设置用户名为只读的。
我们设置旧密码编辑框的工具提示信息,说明只有旧密码输入对了,才能修改新密码,加了一层验证,只有知道原先的密码才能改新密码。这个功能一般是防止非法用户偷偷使用别人电脑修改别人的密码。

接下来我们编辑从主窗口接收数据的槽函数:
//手动添加的槽函数,从主窗口接收旧的用户名和密码哈希
void FormChangePassword::RecvOldUserPassword(QString strUser, QByteArray baOldHash)
{
    //存到成员变量
    m_strUser = strUser;
    m_baOldHash = baOldHash;
    //设置用户名
    ui->lineEditUser->setText(m_strUser);
    //清空密码编辑框
    ui->lineEditOldPassword->clear();
    ui->lineEditNewPassword->clear();
    ui->lineEditNewPassword2->clear();
}
从主窗口信号收到用户名和旧密码哈希值后,我们保存这些数据到成员变量 strUser 和 baOldHash ;
然后通过代码设置用户名编辑框的内容,编辑框的只读属性是指用户不能用鼠标键盘操作修改,程序员可以通过代码修改编辑框内容。
设置用户名之后,我们清空三个密码编辑框的内容,让用户输入这三个密码框内容。

子窗口只有一个“修改密码”按钮,我们现在编辑这个按钮的槽函数:
//修改密码操作
void FormChangePassword::on_pushButtonChange_clicked()
{
    //先获取三个密码字符串
    QString strOldPassword = ui->lineEditOldPassword->text().trimmed();
    QString strNewPassword = ui->lineEditNewPassword->text().trimmed();
    QString strNewPassword2 = ui->lineEditNewPassword2->text().trimmed();
    //判断密码字符串是否为空
    if( strOldPassword.isEmpty() || strNewPassword.isEmpty()
            || strNewPassword2.isEmpty() )
    {
        QMessageBox::information(this, tr("密码框检查"), tr("三个密码都不能为空。"));
        return;
    }
    if( strNewPassword != strNewPassword2 )
    {
        QMessageBox::information(this, tr("新密码检查"), tr("两个新密码不一致。"));
        return;
    }
    //根据旧密码计算旧的哈希值
    QByteArray baOldHashCheck = QCryptographicHash::hash(strOldPassword.toUtf8(),
                                                    QCryptographicHash::Sha256 );
    //转为 Hex 字符串
    baOldHashCheck = baOldHashCheck.toHex();
    if( baOldHashCheck != m_baOldHash )
    {
        QMessageBox::information(this, tr("旧密码检查"), tr("输入的旧密码不正确,不能修改密码。"));
        return;
    }
    //旧密码正确了
    QByteArray baNewHash = QCryptographicHash::hash(strNewPassword.toUtf8(),
                                         QCryptographicHash::Sha256 );
    //转为Hex字符串
    baNewHash = baNewHash.toHex();
    //发送信号,后面交给主窗口处理
    emit SendNewUserPassword(m_strUser, baNewHash);
}
该函数先获取三个密码框输入的字符串,剔除字符串两端的空白;
判断三个密码字符串是否有空的,如果有空的旧弹窗提示,直接返回;
三个密码字符串都是非空,才继续后面操作。
然后判断两个新密码字符串是否相等,如果不相等,说明输入有误,弹窗提示并返回;
只有两个新密码字符串相等,才进行后续操作。
我们根据用户输入的旧密码字符串计算哈希,并转为 Hex 字符串,存到 baOldHashCheck,将这个字符串与成员变量保存的旧密码哈希字符串对比,如果二者不相等,旧密码输入错误,直接弹窗提示并返回;
如果二者相等,说明用户输入的旧密码验证是对的,进入后面的修改密码操作:
根据新密码计算 SHA256 哈希值,并转为 Hex 字符串,就是新的密码哈希字符串;
触发信号SendNewUserPassword(),将用户名和新密码哈希字符串发送给主窗口,
主窗口槽函数 RecvNewUserPassword() 会进行后面的操作。

主窗口和子窗口通过两对信号-槽函数,完成了相互的数据传递。代码讲解到这,我们生成项目,运行这个例子,添加一个用户名 a,密码用简单的 123, 添加新用户:
run01
密码 123 很简单,但是计算的哈希字符串很复杂,避免密码明文的泄露。
我们点击选中 a 用户的行,然后点击“修改用户密码”,弹出子窗口:
run02
这时我们把三个密码框都输入 1 ,点击“修改密码”按钮,出现提示:
run03
旧密码输入错误,不能修改密码。然后我们将旧密码修改为正确的 123 ,再点击“修改密码”按钮:
run04
旧密码输入正确,就能修改为新密码了,并且修改密码的子窗口自动隐藏了,回到了主窗口的界面。
密码管理工具通常还需要加入复杂密码验证,比如需要字母、数字、标点符号、大小写、密码长度等检查,帮助用户设置更加复杂更安全的密码。本例子只是简单演示,现实中都应该使用安全性高的密码。
这个例子的内容介绍到这,下面我们学习将已有的窗口类集成到项目里,作为子窗口弹出显示。

11.1.3 集成已有窗口类方式

下面的例子,我们通过集成已有窗口类的方式,使用多个窗口。我们对 10.3.2 小节文件属性的例子进行重新设计,使用主窗口显示文件名称、大小、修改时间等内容,通过一个按钮弹出子窗口,用子窗口预览文件内容,子窗口就是 10.3.2 小节文件属性例子中的 TabPreview 类文件,我们复制过来, TabPreview 类文件一个字母都不用修改,就可以集成到新项目里面使用。信号和槽机制就完美实现了多个窗口之间的数据交互,而且是松耦合的,模块独立性很强,在新项目集成原有的类,原有类的代码都不需要做修改。下面我们开始这个例子。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 fileattrshow,创建路径 D:\QtProjects\ch11,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,注意修改主窗口类名为 FileAttrWidget,这样与其他窗口类名作区分,然后点击下一步;
④项目管理不修改,点击完成。
主窗口类名为 FileAttrWidget,对应的界面文件、头文件、源文件名就是 fileattrwidget.ui、fileattrwidget.h、fileattrwidget.cpp 。

我们把 10.3.2 小节文件属性的例子里面的三个文件 tabpreview.cpp、tabpreview.h、tabpreview.ui 复制粘贴到本示例 fileattrshow 项目文件夹中。
接下来我们在项目管理器右击项目根 fileattrshow,右键菜单选择“添加现有文件...”, 弹出添加现有文件对话框:
add01
我们选中 tabpreview.cpp、tabpreview.h、tabpreview.ui 三个文件,然后点击“打开”按钮,这三个已有类的文件就添加给了本项目。
添加文件之后,QtCreator 如果出现左下角三角箭头变为灰色,不能构建项目的情况,就点击三角箭头上面的 Debug 按钮,将 Debug 和 Release 构建来回切换一下即可。
将 TabPreview 类文件添加到项目之后,该类三个文件的内容我们一个字母都不用修改。

下面我们打开主窗口的 fileattrwidget.ui 界面文件,拖入如下控件:
main01
界面第一行是标签“文件全名”,单行编辑器 lineEditFullName,“选择文件”按钮 pushButtonSelectFile;
第二行是标签“文件短名”,单行编辑器 lineEditShortName,“预览文件”按钮 pushButtonPreview;
第三行是标签“文件大小”,单行编辑器 lineEditFileSize;
第四行是标签“创建时间”,单行编辑器 lineEditTimeCreated;
第四行是标签“访问时间”,单行编辑器 lineEditTimeRead;
第四行是标签“修改时间”,单行编辑器 lineEditTimeModified 。
将这些控件拖动排列整齐,然后在右侧对象树一栏选择窗口根 FileAttrWidget, 点击上面网格布局按钮,将所有控件按照网格布局,如下图所示:
main02
完成布局后,我们依次右键点击两个按钮,为每个按钮添加 clicked() 信号的槽函数:
slot
两个按钮槽函数添加完成后,我们保存并关闭界面文件。

下面我们编辑主窗口头文件 fileattrwidget.h 内容:
#ifndef FILEATTRWIDGET_H
#define FILEATTRWIDGET_H

#include <QWidget>
#include <QFile>
#include <QFileInfo>
#include <QFileDialog>
#include "tabpreview.h"

namespace Ui {
class FileAttrWidget;
}

class FileAttrWidget : public QWidget
{
    Q_OBJECT

public:
    explicit FileAttrWidget(QWidget *parent = 0);
    ~FileAttrWidget();

signals:
    //文件名变化时触发该信号
    void fileNameChanged(const QString &fileName);

private slots:
    void on_pushButtonSelectFile_clicked();

    void on_pushButtonPreview_clicked();

private:
    Ui::FileAttrWidget *ui;
    //子窗口指针    
    TabPreview *m_pPreviewWnd;
    //文件全名
    QString m_strFileName;
    //文件信息查看对象
    QFileInfo m_fileInfo;

    //初始化函数
    void Init();
};

#endif // FILEATTRWIDGET_H
我们添加了文件、文件信息、文件对话框等类的头文件包含,并添加了子窗口类头文件 tabpreview.h 包含。
在主窗口类声明里,添加信号 fileNameChanged(const QString &fileName),用于给子窗口发送文件名,不管是给子控件还是子窗口发数据,信号和槽函数的使用是一模一样的。
主窗口类声明里两个槽函数是通过界面文件编辑时为按钮添加的。
我们添加了子窗口的指针 m_pPreviewWnd,文件名字符串 m_strFileName,文件信息查看对象 m_fileInfo。
最后添加了一个初始化函数,用于新建子窗口、关联信号和槽等功能。

下面我们分段编辑主窗口源文件 fileattrwidget.cpp 内容,首先是构造函数和初始化部分:
#include "fileattrwidget.h"
#include "ui_fileattrwidget.h"
#include <QMessageBox>
#include <QDebug>
#include <QDateTime>

FileAttrWidget::FileAttrWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::FileAttrWidget)
{
    ui->setupUi(this);
    //初始化函数
    Init();
}

FileAttrWidget::~FileAttrWidget()
{
    //删除子窗口
    delete m_pPreviewWnd;   m_pPreviewWnd = NULL;
    delete ui;
}
//初始化函数
void FileAttrWidget::Init()
{
    //单行编辑器都设置为只读
    ui->lineEditFullName->setReadOnly(true);
    ui->lineEditShortName->setReadOnly(true);
    ui->lineEditFileSize->setReadOnly(true);
    ui->lineEditTimeCreated->setReadOnly(true);
    ui->lineEditTimeRead->setReadOnly(true);
    ui->lineEditTimeModified->setReadOnly(true);

    //指针初始化为 NULL
    m_pPreviewWnd = NULL;
    //新建子窗口
    m_pPreviewWnd = new TabPreview(NULL);
    //设置子窗口标题栏
    m_pPreviewWnd->setWindowTitle(tr("预览文件"));

    //关联信号和槽函数,传递文件名给子窗口
    connect(this, SIGNAL(fileNameChanged(QString)),
            m_pPreviewWnd, SLOT(onFileNameChanged(QString)) );
}
我们添加了信息框、调试输出和日期时间类的头文件包含。在构造函数末尾调用初始化函数 Init() 。
析构函数里面删除了新建的子窗口对象。
Init() 初始化函数中,我们先把 6 个编辑框设置为只读,因为程序不会修改文件的属性。
然后我们把 m_pPreviewWnd 初始化为 NULL,然后新建子窗口对象,保存到 pPreviewWnd ;
设置子窗口的标题文本为 "预览文件",直观显示子窗口功能;
然后将主窗口的 fileNameChanged(QString) 信号关联到子窗口槽函数 onFileNameChanged(QString),就可以实现窗口之间信息的传递。

接下来我们编辑“选择文件”按钮槽函数的代码:
//选择文件
void FileAttrWidget::on_pushButtonSelectFile_clicked()
{
    QString strName = QFileDialog::getOpenFileName(this, tr("选择文件"),
                      tr(""), tr("All files(*)") );
    strName = strName.trimmed();    //剔除空格
    if( strName.isEmpty() )
    {
        return; //名字为空
    }
    //获取了正常文件名
    m_strFileName = strName;
    m_fileInfo.setFile( m_strFileName );
    //文件大小
    qint64 nFileSize = m_fileInfo.size();

    //设置全名、短名、大小三个编辑框
    ui->lineEditFullName->setText( m_strFileName );
    ui->lineEditShortName->setText( m_fileInfo.fileName() );
    ui->lineEditFileSize->setText( tr("%1 字节").arg(nFileSize) );

    //三个时间字符串
    QString strTimeCreated = m_fileInfo.created().toString("yyyy-MM-dd  HH:mm:ss");
    QString strTimeRead = m_fileInfo.lastRead().toString("yyyy-MM-dd  HH:mm:ss");
    QString strTimeModified = m_fileInfo.lastModified().toString("yyyy-MM-dd  HH:mm:ss");
    //设置时间编辑框
    ui->lineEditTimeCreated->setText( strTimeCreated );
    ui->lineEditTimeRead->setText( strTimeRead );
    ui->lineEditTimeModified->setText( strTimeModified );

    //触发文件名称修改的信号
    emit fileNameChanged(m_strFileName);
}
我们使用文件打开对话框获取文件名存到 strName,剔除字符串两端的空格;
判断 strName 是否为空,如果为空就返回,不处理;
strName 不为空的时候,我们将文件名存到成员变量 m_strFileName;
设置文件信息查看对象 m_fileInfo 文件名为 strFileName;获取文件大小存到 nFileSize;
设置文件全名、文件短名和文件大小三个编辑框的内容;
我们使用 m_fileInfo 对象的三个函数获取文件的创建时间、最后访问时间、最后修改时间,
构造时间字符串存到 strTimeCreated、strTimeRead、strTimeModified ;
然后设置三个时间显示编辑框的内容,显示各自的时间字符串;
最后触发 fileNameChanged(m_strFileName) 信号,将文件名传递给子窗口。

下面我们编辑“预览文件”按钮的槽函数:
//预览文件
void FileAttrWidget::on_pushButtonPreview_clicked()
{
    if( m_strFileName.isEmpty() )
    {
        //没有文件名,没有预览
        return;
    }
    //模态窗口弹出示范
    if( m_pPreviewWnd->isVisible() )
    {
        //窗口处于已经显示,需要先隐藏,设置模态属性后重新显示
        m_pPreviewWnd->hide();
    }
    //应用程序级别的模态显示
    m_pPreviewWnd->setWindowModality( Qt::ApplicationModal );
    m_pPreviewWnd->show();
    //模态显示会强制占据前台焦点,只有用户关闭模态窗口才会回到主窗口
}
函数先检查文件名是否为空,如果为空就返回,不用预览。
判断 m_pPreviewWnd 子窗口是否已经显示,如果已经显示就先隐藏该窗口;
设置子窗口为应用程序级别的模态窗口类型,然后显示子窗口。
模态窗口的显示需要子窗口在未显示的时候设置,这样下次显示的时候就是模态窗口。
模态窗口显示之后,会强制占据前台焦点,只有用户关闭模态窗口后,才能回到原来的主窗口。

本示例的代码就编辑完成了,子窗口类的文件是复制过来的,集成到项目里,不用修改。
我们直接生成项目,运行例子:
run01
我们点击“选择文件”按钮,打开了 ui_tabpreview.h 文件,可以看到该文件的大小和时间等信息。
然后我们点击“预览文件”按钮,弹出模态子窗口:
run02
子窗口功能与  10.3.2 小节文件属性例子中的预览功能一样,只是从标签页子控件换成了独立的子窗口。
模态子窗口弹出显示时,如果点击主窗口,会发现无法切回主窗口操作,因为前台焦点被模态窗口占据。
关闭模态子窗口后,才能回到主窗口操作。
本示例内容介绍到这,留两个小练习,请读者自己完成,下一节我们学习对话框。

tip 练习
① 给密码管理工具 passwordtool 示例中的密码输入框增加检测功能,要求密码字符串长度至少为 6,并且同时有字母和数字。
② 将 passwordtool 示例子窗口改为模态显示,将 fileattrshow 示例子窗口改为非模态显示。


prev
contents
next