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 代表隐藏。
通常新的子窗口会显示到主窗口的上层,多个窗口的显示可能出现区域重叠,上层的窗口会遮挡下层窗口显示,例如:
窗口本身是二维显示的,横轴 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() 。
通过下图直观说明尺寸位置计算:
绿色箭头包含标题栏边框,是完整的窗口矩形计算,紫色的箭头是窗口客户区矩形计算。
一般来说,移动窗口位置时,我们按照窗口整体来计算;
而缩放窗口尺寸时,按照窗口客户区来计算。
11.1.2 新建窗口类方式
下面我们通过一个密码管理工具例子,学习新建窗口类的方式,使用多个窗口。
主窗口显示用户名和密码哈希值列表,通过新建的子窗口来完成修改用户密码的功能。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 passwordtool,创建路径 D:\QtProjects\ch11,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,注意修改主窗口类名为 MainWidget,这
样与后续的子窗口类名作区分,然后点击下一步;
④项目管理不修改,点击完成。
主窗口类名为 MainWidget,对应的界面文件、头文件、源文件名就是
mainwidget.ui、mainwidget.h、mainwidget.cpp 。
接下来我们在项目管理器右击项目根 passwordtool ,右键菜单选择“添加新文件...”,进入新建文件对话框:
在上面对话框左边一栏选择 “Qt”,然后在中间一栏选择“Qt设计师界面类”,然后点击右下角“Choose...”按钮,进入Qt设计师界面类的选择界面:
在中间上半区选择“Widget”,然后点击“下一步”按钮,进入类名编辑界面:
我们只需要把类名修改为 FormChangePassword ,下面的头文件、源文件、界面文件名自动修改,不需要手动调整,修改类名之后点击“下一步”,
项目管理界面不用动,直接点击“完成”按钮。这样项目就增加了新类的文件:
formchangepassword.cpp,formchangepassword.h,formchangepassword.ui。
现在我们有两套界面类,一套是 MainWidget 类主窗口的文件,另一套是 FormChangePassword 类子窗口的文件。
我们从主窗口开始编辑,我们打开 mainwidget.ui 文件,然后拖入控件:
第一行控件分别是标签“用户名”,单行编辑器 lineEditUser,标签“密码”,单行编辑器 lineEditPassword,“添加新用户”按钮
pushButtonAddUser 。
第二行左边是列表控件 listWidgetShow ,右边是两个按钮,“修改用户密码”按钮
pushButtonChangePassword,“删除选定用户”按钮 pushButtonDelUser 。“修改用户密码”按钮就是负责弹出子窗口的功
能按钮。
第一行控件按照水平布局;
第二行先将 pushButtonChangePassword、pushButtonDelUser 两个按钮垂直布局,然后再与
listWidgetShow 进行水平布局,如下图所示:
然后我们选中主窗口 MainWidget,点击上面垂直布局按钮,主窗口的主布局器是垂直布局:
布局完成后,我们修改窗口的尺寸为 518* 300。
然后我们依次右键点击三个按钮,在右键菜单为它们添加 clicked() 信号对应槽函数:
添加好主界面三个按钮的槽函数之后,我们保存并关闭 mainwidget.ui 文件。
接下来我们打开子窗口的界面文件 formchangepassword.ui ,拖入如下控件:
第一行是标签“用户名”,单行编辑器 lineEditUser;
第二行是标签“旧密码”,单行编辑器 lineEditOldPassword;
第三行是标签“新密码”,单行编辑器 lineEditNewPassword;
第四行是标签“新密码确认”,单行编辑器 lineEditNewPassword2;
第五行是“修改密码”按钮 pushButtonChange,按钮尺寸拉宽,与上一行两个控件的累计宽度差不多 。
在右边对象树,选中窗口 FormChangePassword ,然后点击上面的 网格布局器,该窗口使用一个网格布局即可,设置布局后把窗口大小设置为
280 * 240 ,如下图所示:
布局设置完成后,我们右击子窗口的 “修改密码”按钮,为该按钮也添加 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() 是用于窗口初始化的代码,在该函数里面新建子窗口。
例子中主窗口和子窗口通过信号和槽函数交互数据,如下图所示:
需要发送数据时,通过 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,
添加新用户:
密码 123 很简单,但是计算的哈希字符串很复杂,避免密码明文的泄露。
我们点击选中 a 用户的行,然后点击“修改用户密码”,弹出子窗口:
这时我们把三个密码框都输入 1 ,点击“修改密码”按钮,出现提示:
旧密码输入错误,不能修改密码。然后我们将旧密码修改为正确的 123 ,再点击“修改密码”按钮:
旧密码输入正确,就能修改为新密码了,并且修改密码的子窗口自动隐藏了,回到了主窗口的界面。
密码管理工具通常还需要加入复杂密码验证,比如需要字母、数字、标点符号、大小写、密码长度等检查,帮助用户设置更加复杂更安全的密码。本例子只是简单演示,现实中都应该使用安全性高的密码。
这个例子的内容介绍到这,下面我们学习将已有的窗口类集成到项目里,作为子窗口弹出显示。
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,右键菜单选择“添加现有文件...”,
弹出添加现有文件对话框:
我们选中 tabpreview.cpp、tabpreview.h、tabpreview.ui 三个文件,然后点击“打开”按钮,这三个已有类的文件就添加给了本项目。
添加文件之后,QtCreator 如果出现左下角三角箭头变为灰色,不能构建项目的情况,就点击三角箭头上面的 Debug 按钮,将 Debug 和 Release 构建来回切换一下即可。
将 TabPreview 类文件添加到项目之后,该类三个文件的内容我们一个字母都不用修改。
下面我们打开主窗口的 fileattrwidget.ui 界面文件,拖入如下控件:
界面第一行是标签“文件全名”,单行编辑器 lineEditFullName,“选择文件”按钮 pushButtonSelectFile;
第二行是标签“文件短名”,单行编辑器 lineEditShortName,“预览文件”按钮 pushButtonPreview;
第三行是标签“文件大小”,单行编辑器 lineEditFileSize;
第四行是标签“创建时间”,单行编辑器 lineEditTimeCreated;
第四行是标签“访问时间”,单行编辑器 lineEditTimeRead;
第四行是标签“修改时间”,单行编辑器 lineEditTimeModified 。
将这些控件拖动排列整齐,然后在右侧对象树一栏选择窗口根 FileAttrWidget,
点击上面网格布局按钮,将所有控件按照网格布局,如下图所示:
完成布局后,我们依次右键点击两个按钮,为每个按钮添加 clicked() 信号的槽函数:
两个按钮槽函数添加完成后,我们保存并关闭界面文件。
下面我们编辑主窗口头文件 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 子窗口是否已经显示,如果已经显示就先隐藏该窗口;
设置子窗口为应用程序级别的模态窗口类型,然后显示子窗口。
模态窗口的显示需要子窗口在未显示的时候设置,这样下次显示的时候就是模态窗口。
模态窗口显示之后,会强制占据前台焦点,只有用户关闭模态窗口后,才能回到原来的主窗口。
本示例的代码就编辑完成了,子窗口类的文件是复制过来的,集成到项目里,不用修改。
我们直接生成项目,运行例子:
我们点击“选择文件”按钮,打开了 ui_tabpreview.h 文件,可以看到该文件的大小和时间等信息。
然后我们点击“预览文件”按钮,弹出模态子窗口:
子窗口功能与 10.3.2 小节文件属性例子中的预览功能一样,只是从标签页子控件换成了独立的子窗口。
模态子窗口弹出显示时,如果点击主窗口,会发现无法切回主窗口操作,因为前台焦点被模态窗口占据。
关闭模态子窗口后,才能回到主窗口操作。
本示例内容介绍到这,留两个小练习,请读者自己完成,下一节我们学习对话框。
|
练习 |
① 给密码管理工具 passwordtool 示例中的密码输入框增加检测功能,要求密码字符串长度至少为 6,并且同时有字母和数字。
② 将 passwordtool 示例子窗口改为模态显示,将 fileattrshow 示例子窗口改为非模态显示。