网络聊天项目(1)使用QT创建聊天室客户端


Note: 此项目原博客地址

https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2eIZbBf2pkVGQG1oPdRLDtTDLo0

1、创建项目

首先使用QT软件创建Qt Widgets Application,生成的基类使用QMainWindow

001_Creat_Project

2、添加资源文件并更换图标

首先将准备的图片文件添加到res文件夹下,然后将res文件夹放下项目根目录下;然后在Qt软件中点中项目名称,选择添加新文件,
选择Qt Resource Files,添加Qt的资源文件,名字设为rc,添加成功后右键rc.qrc选择添加现有文件,然后选中res文件夹下的所有图片,
这样res文件夹下的所有图片资源都导入到项目里面了。

然后将QMainWindow的界面长宽改为300x500

002_QMainWindow长宽大小

之后修改项目左上角的图标,在QMainWindow的界面找到windowIcon,选择添加res文件下的ICNO.ico

003_LeftAndHigh_Image

修改项目左上面的标题,将MainWindow修改为Chat,在QMainWindow的构造函数添加以下代码即可

1
setWindowTitle("Chat");

3、添加登录页面

右键项目名称并选择添加新项目,点击Qt设计师界面类

004_Qt设计师界面类

选择Dialog Without Buttons

005_DialogWithoutButtons

将名字起为LoginDialog,创建完成后点击ui界面并将ui界面修改为以下布局:

006_LoginDialogUi

在QMainWindow的类里添加LoginDialog的指针成员,并在QMainWindow构造函数里面设置为中心部件

1
2
3
_login_dialog = new LoginInDialog();
setCentralWidget(_login_dialog);
_login_dialog->show();

4、添加样式表

在项目的根目录下创建一个名为style的文件夹,打开style文件夹建立一个.text文件,将这个.text文件名称改为stylesheet.qss,然后右键点击Qt项目中的rc.qrc,选择添加现有文件,选择刚刚创建的style文件夹即可,这样stylesheet.qss就被导入到项目中了

打开stylesheet.qss并写下如下代码

1
2
3
QDialog#LoginInDialog{
background-color:rgb(255,255,255)
}

这样LoginInDialog界面的背景就被美化成白色了

然后在主函数main里面添加以下代码用来启动qss文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char *argv[])
{
QApplication a(argc, argv);

QFile qss(":/style/stylesheet.qss");

if(qss.open(QFile::ReadOnly)){
qDebug("open success!");
QString style = QLatin1String(qss.readAll());
a.setStyleSheet(style);
qss.close();
}else{
qDebug("open failed!");
}

MainWindow w;
w.show();

return a.exec();
}

5、添加注册页面

跟添加登录页面的方式添加注册页面,将名字设置为RegisterDialog,添加完成之后打开注册页面的ui,将注册页面的ui修改成以下画面

007_Register_dialog

然后在注册页面的构造函数里面将code_lineedit和confirm_lineedit置为密码模式

1
2
ui->code_lineedit->setEchoMode(QLineEdit::Password);
ui->confirm_lineedit->setEchoMode(QLineEdit::Password);

我们在qss里面添加tips的样式,正确状态下tips里面的文字显示为绿色,错误状态下tips的文字显示为红色

1
2
3
4
5
6
7
#tips[state = 'normal']{
color: green;
}

#tips[state = 'error']{
color: red;
}

然后我们来实现tips的刷新功能,这个刷新功能函数repolish打算做成全局函数来实现,因此我们添加global.cpp和global.h文件

global.h文件的声明:

1
2
3
4
5
6
7
8
9
10
#ifndef GLOBAL_H
#define GLOBAL_H

#include <QWidget>
#include <functional>
#include "QStyle"

extern std::function<void(QWidget*)> repolish;

#endif // GLOBAL_H

global.cpp文件里repolish函数的实现:

1
2
3
4
std::function<void(QWidget*)> repolish=[](QWidget* w){
w->style()->unpolish(w);
w->style()->polish(w);
};

在RegisterDialog的构造函数里面添加tips的样式设置

1
2
ui->tips->setProperty("state", "normal");
repolish(ui->tips);

接下来实现获取验证码的逻辑,在RegisterDialog.ui里面右键点击获取按钮,选择转到槽,在槽函数里面写下如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
//使用正则表达式验证邮箱地址
auto email=ui->email_lineedit->text();
//邮箱地址的正则表达式
QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
bool match = regex.match(email).hasMatch();
if(match){
ShowTips("邮箱地址正确", true);
//发送Http请求发送验证码

}
else{
ShowTips("邮箱地址不正确", false);
}

下面实现ShowTips函数,这个函数用来显示tips的文字和状态

1
2
3
4
5
6
7
8
9
10
void RegisterDialog::ShowTips(QString str, bool b_ok){
if(b_ok){
ui->tips->setProperty("state", "normal");
}
else{
ui->tips->setProperty("state", "error");
}
ui->tips->setText(str);
repolish(ui->tips);
}

6、添加单例类

我们编写网络通讯需要确保一个类只生成一个实例,所以我们编写一个单例模板类,之后的网络通讯类都继承这个单例类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <mutex>
#include <memory>
#include <iostream>
#include "QDebug"

template <typename T>
class SingleTon{
protected:
SingleTon() = default;
SingleTon(const SingleTon<T>&) = delete;
SingleTon& operator = (const SingleTon<T>&) = delete;

static std::shared_ptr<T> _instance;

public:
static std::shared_ptr<T> GetInstance(){
static std::once_flag _flag;
std::call_once(_flag,[&](){
_instance = std::shared_ptr<T>(new T);
});
return _instance;
}
void PrintAdress(){
std::cout<< _instance.get() <<std::endl;
}

};

template <typename T>
std::shared_ptr<T> SingleTon<T>::_instance = nullptr;

7、添加Http管理类

Http管理类主要用于管理HTTP接收发送等请求,首先我们需要在pro文件里面添加网络库

1
QT  +=  core gui network

在global.h里面添加一些Http管理类用到的头文件

1
2
3
4
5
6
7
8
9
10
11
12
#include <QByteArray>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <qDebug>
#include <QString>
#include <QUrl>
#include <QObject>
#include <QNetworkAccessManager>
#include <memory>
#include <QJsonObject>
#include <QJsonDocument>
#include <QMap>

添加httpMgr.cpp和httpMgr.h,httpMgr.h头文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef HTTPMGR_H
#define HTTPMGR_H

#include "singleton.h"
#include "global.h"

class HttpMgr: public QObject,public SingleTon<HttpMgr>, public std::enable_shared_from_this<HttpMgr>
{
Q_OBJECT
friend class SingleTon<HttpMgr>;
public:
~HttpMgr();
void PostHttpRequest(ReqID id, Modules modules, QUrl url, QJsonObject json);

signals:
void SignalHttpFinished(ReqID id, Modules modules, ErrorCode ec_code, QString res);
//
void SignalRegisterFinished(ReqID id, ErrorCode ec_code, QString res);

private slots:
void SlotHttpFished(ReqID id, Modules modules, ErrorCode ec_code, QString res);

private:
HttpMgr();

QNetworkAccessManager _manager;
};

#endif // HTTPMGR_H

PostHttpRequest函数用来接收HTTP发送过来的消息,参数id和modules用来区分HTTP信息的类型,url是请求的地址,json是请求的数据

我们在global.h里面定义ReqID枚举类型

1
2
3
4
enum ReqID{
ID_GET_VARIFY_CODE = 1001,
ID_REGISTER_USER = 1002
}

ReqID有两个作用,一个作用是定位接收的是哪一部份数据;第二个作用是存储数据的key值使用,ReqID=1001时,说明此时处于注册状态,我们需要将ReqID和验证码存入到map里,ReqID就作为验证码的key值使用;同理,当ReqID=1002时,ReqID就作为密码的key值所使用

在global.h里面定义ErrorCode

1
2
3
4
5
enum ErrorCode{
SUCESS = 0,
ERROR_JSON = 1,
ERROR_NETWORK = 2
};

ErrorCode是错误的类型,用于帮助定位项目运行过程中在哪一部分出现问题

在global.h里面定义Modules

1
2
3
4
enum Modules{
REGISTER_MOD = 0,
};

Modules也是用来定位接收的是哪一部分数据,用来说明项目此时处于的状态,目前只将注册状态设为0,但之后还会有登录状态和忘记密码的状态

在PostHttpRequest函数里面首先发送Post请求然后收到回复并解析,无论解析的数据如何都会发送SignalHttpFinished信号,然后数据传输到槽函数SlotHttpFished里面,槽函数SlotHttpFished再次发送信号SignalRegisterFinished,接收信号的槽函数SlotRegisterFinished被定义于RegisterDialog类里面

简单来讲,RegisterDialog类里面的函数调用HttpMgr类的函数发送HTTP信息,HttpMgr类的函数将接收的信息传送回RegisterDialog类里面的函数,数据的解析是在RegisterDialog类里进行,HttpMgr类只负责数据的发送和接收

PostHttpRequest函数的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void HttpMgr::PostHttpRequest(ReqID id, Modules modules, QUrl url, QJsonObject json)
{
QByteArray data = QJsonDocument(json).toJson();
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::ContentLengthHeader,QByteArray::number(data.length()));

auto self = shared_from_this();
//发送reques数据
QNetworkReply* reply = _manager.post(request, data);
//接受reply数据
QObject::connect(reply, &QNetworkReply::finished, [reply, self, id, modules](){
//发送错误
if(reply->error() != QNetworkReply::NoError){
qDebug()<<reply->errorString();
//发送信号说明数据传输错误
emit self->SignalHttpFinished(id, modules, ErrorCode::ERROR_NETWORK, "");
reply->deleteLater();
return;
}

//发送信号说明数据已经传输完成
QString res = reply->readAll();
emit self->SignalHttpFinished(id, modules, ErrorCode::SUCESS, res);
reply->deleteLater();
return;
});
}

槽函数SlotHttpFished的实现

1
2
3
4
5
6
void HttpMgr::SlotHttpFished(ReqID id, Modules modules, ErrorCode ec_code, QString res)
{
if(modules == Modules::REGISTER_MOD){
emit SignalRegisterFinished(id, ec_code, res);
}
}

在HttpMgr的构造函数里面将信号SignalHttpFinished与槽函数SlotHttpFished连接

1
connect(this, &HttpMgr::SignalHttpFinished, this, &HttpMgr::SlotHttpFished);

在RegisterDialog的构造函数里面将信号SignalRegisterFinished和槽函数SlotRegisterFinished连接

1
connect(HttpMgr::GetInstance().get(), &HttpMgr::SignalRegisterFinished, this, &RegisterDialog::SlotRegisterFinished);

然后实现槽函数SlotRegisterFinished

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void RegisterDialog::SlotRegisterFinished(ReqID id, ErrorCode ec_code, QString res)
{
if(ec_code != ErrorCode::SUCESS){
ShowTips("网络请求错误", false);
return;
}
//接收数据正常
else{
QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());

if(jsonDoc.isNull()){
ShowTips("JSON解析错误", false);
return;
}

if(jsonDoc.isObject()){
ShowTips("JSON解析错误", false);
return;
}

//处理相应请求
_handlers[id](jsonDoc.object());
return;
}
}

8、添加注册信息处理

我们需要对注册的信息进行处理,在RegisterDialog类的私有成员进行声明

1
2
// _handlers处理注册ID和验证码信息
QMap<ReqID, std::function<void(const QJsonObject&)>> _handlers;

对于注册信息的定义与声明

1
2
3
4
5
6
7
8
9
10
11
12
13
void RegisterDialog::InitHttpHandlers()
{
_handlers.insert(ReqID::ID_GET_VARIFY_CODE, [this](QJsonObject jsonobj){
int error = jsonobj["error"].toInt();
if(error != ErrorCode::SUCESS){
ShowTips("参数错误", false);
true;
}

auto email = jsonobj["email"].toString();
ShowTips("验证码已经发送到邮箱,请注意查收", true);
});
}

在槽函数SlotRegisterFinished里面添加根据id调用函数处理对应逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void RegisterDialog::SlotRegisterFinished(ReqID id, ErrorCode ec_code, QString res)
{
if(ec_code != ErrorCode::SUCESS){
ShowTips("网络请求错误", false);
return;
}
//接收数据正常
else{
//前面逻辑省略...
//处理相应请求
_handlers[id](jsonDoc.object());
return;
}
}