简洁易懂:利用Qt实现简易聊天软件(小白入门)

简易版聊天软件的Qt实现

这个学期我们的C++实验课给出的课题是实现一个简易版的聊天软件,总言之,要求如下:
1、需要实现群组和好友的管理;
2、聊天平台的独立实现;
3、以及最关键的一点,实现消息的传输;

不得不说对于仅仅接触了一个学期C++的学生来讲这个单人实验课题的难度属实不小,但是非常值得挑战,经历一个半月的尝试与努力,我最终还是写出了一个至少能让自己满意的聊天平台,这里我想总结一下自己的编写过程,以及整个程序的设计思路:

开发软件的选择

这次的实验课题只要求开发语言是C++
比起大家都选择也最熟悉的控制台
我想将这次的课题做的更像一个“软件”,最终我选择了Qt
当然配置Qt的环境真的非常艰辛,尝试了若干次以后才终于成功
(不得不说Qt的安装包实在是太核善了)

软件设计的整体思路

设计思路在编写的过程中调整了很多次,用文字描述起来稍显繁琐,这里就用在结课时使用的UML图来呈现一下
UML画的不一定标准,但我认为能较为清晰地展现这个软件的设计框架
大二咸鱼,能力有限,如果有大佬发现错误,欢迎指正交流
(2020.4.1:当时自己还没有学UML,现在看看画的真的是一塌糊涂,大家就凑合着看吧)
UML图

软件数据存储

大二学生暂时还没有接触数据库
所以实验课的数据我们存放在本地的txt文件
以下给出一些关键的存储文件:
在这里插入图片描述
各类文件夹是每个用登录记录用户的独立信息
currentuser.txt是当前登录用户的信息
groupqq.txt , groupweix.txt是所有已创建的群组信息(对应不同的服务
newaccount.txt对应下一次随机新生成的用户账号
user.txt存储每个用户的密码和账号,便于登录时的匹配
userinform.txt存储每个用户的完整信息

软件的具体实现

软件主要涉及以下3个界面:
· 登录界面
· 用户注册界面
· 用户使用界面

除此以外还包含若干提示界面和引导界面,当然这些界面的代码行数相对较少
有些可以直接使用QMessageBox实现

在上述3个界面中,核心的是最后一个:
用户使用界面
在这个界面中,涉及到聊天平台的多数主要功能,

接下来逐次分析各类界面的实现:

登录界面

先展示效果图:
这是登录前的引导界面
在这里插入图片描述
这是登录界面的本体
在这里插入图片描述
刷新系统 用于新用户注册后重新读取本地信息
其余按钮的意义如其字面意义

用户账户和登录密码中输入的内容会和user.txt进行匹配
(暂时还没有学过数据库)
如下是匹配代码:

void cusLoginJC::on_pushButton_clicked()
{
    if(isEmpty == false) {
        QString id = this->ui->le_account->text();
        QString password = this->ui->le_password->text();
        int count = 0;
        for(Map::iterator it = inform.begin();it != inform.end();it++) {
            if(id == it.key() && password == it.value()) {
                QFile file("./inform/currentuser.txt");
                file.open(QIODevice::WriteOnly);
                QTextStream twrite(&file);
                twrite << id << endl;
                file.close();
                ccj = new cusChatJC(this);
                void (cusChatJC::*findSignal) () = &cusChatJC::mySignal;
                void (cusLoginJC::*findSlot) () = &cusLoginJC::showCusChatJC;
                connect(ccj, findSignal, this, findSlot);
                this->close();
                ccj->show();
                break;
            }
            else count++;
            if(count == inform.size()) {
                this->ui->le_notice->setText("登录信息错误");
                delayTime(1500);
                this->ui->le_notice->setText("");
                break;
            }
        }
    }
}

这是这个界面所涉及的最核心的操作;
其余的按钮基本是打开子窗口,利用Qt已有的机制非常容易实现;

用户注册界面

同样,也先展示效果图:
在这里插入图片描述
由于实验课时间不长,整个编写过程稍显匆忙,有些细节我没有给予重视
比如窗口的名称:dialog我没有修改,不过修改起来是一件非常简单的事

注册信息所输入的内容会写进userinform.txt
同时,为了确保用户只能通过点击确定或是取消按钮退出该界面
我通过在界面构造函数中添加

Qt::WindowTitleHint | Qt::CustomizeWindowHint

取消了原有的关闭键(右上的红叉)

同时,为了避免在设置昵称和密码出现的不规范表达
我利用函数:

void cusLoginJC::setTextLabel()
{
    QRegExp regx("[A-Za-z0-9]+$");                                                  //正则表达式(在A-Z和0-9之间有效输入)
    QValidator *validator = new QRegExpValidator(regx,this->ui->le_account);        //QRegExpValidator用于检测对应的line edit中文字是否合乎正则表达式
    this->ui->le_account->setValidator(validator);                                  //通过QValidator类指针validator将判断条件传入相应的控件
}

限制了对应文本框内输入的格式

以下是注册时所涉及的整个函数体
同时包括了查重密码确认等过程,但总体实现的难度不大
函数体的实现可能不是最优,欢迎大佬指正交流

void cusRegisterJC::on_pushButton_clicked()
{
    QString nickName = this->ui->le_name->text();
    QString password1 = this->ui->le_setting->text();
    QString password2 = this->ui->le_ensure->text();
    QString birthDay = this->ui->le_birthday->text();
    QString homePlace = this->ui->le_homePlace->text();
    QFile file0("./inform/user.txt");
    file0.open(QIODevice::ReadOnly);
    QTextStream tread(&file0);
    while(!tread.atEnd()) {
        QString linestr = tread.readLine();
        cusId.append(linestr);
        tread.readLine();
    }
    file0.close();
loop:
    int num = rand()%90000000 + 10000000;
    name = QString::number(num,10);
    for(int i = 0; i < cusId.size(); i++)
        if(name == cusId[i]) goto loop;
    if(password1.length() < 6 || password1.length() > 12) {
        this->ui->le_warning->setText("密码长度不合要求");
        delayTime(1500);
        this->ui->le_warning->setText("");
        return;
    }
    if(password1 == password2 && password1 != "") {
        QFile file("./inform/user.txt");
        QFile filex("./inform/userinform.txt");
        if(file.open(QIODevice::WriteOnly|QIODevice::Append) == true) {
            QTextStream twrite(&file);
            twrite << name << endl << password1 << endl;
        }
        file.close();
        if(filex.open(QIODevice::WriteOnly|QIODevice::Append) == true) {
            QTextStream twrite(&filex);
            QDateTime *time = new QDateTime(QDateTime::currentDateTime());
            QString strtime = time->toString("yyyy-MM-dd hh:mm:ss");
            twrite << name << endl <<nickName << endl << password1 << endl << homePlace << endl << birthDay << endl << strtime << endl;
        }
        filex.close();
        srj = new sucRegsiterJC;
        srj->show();
        this->close();
    }
    else if(password1 != password2 || name == "" || password1 == "" || password2 == "") {
        this->ui->le_warning->setText("两次密码输入不一致");
        this->ui->le_setting->setText("");
        this->ui->le_ensure->setText("");
        delayTime(1500);
        this->ui->le_warning->setText("");
        return;
    }
}

用户使用界面

终于到了最关键的界面了!
这个界面的设计较为复杂,按钮较多
话不多说,先来看看:
在这里插入图片描述
该界面对大量的按钮进行了复用
例如:
好友列表;群组列表;“管理群组”中的所有按钮;创建群组等等

这些按钮均作为函数体,封装于对好友和群组管理的基类
针对服务(QQ微信等)的差异,派生出略有差异的派生类
具体的实现不多赘述,主要是对本地txt文件的反复操作

有兴趣具体了解的读者可以在文章末尾下载完整文件自行操作

而界面的左下是一个内嵌窗口,可以查询不同服务所拥有的的共同好友:

#include "cusfrirecommjc.h"
#include "ui_cusfrirecommjc.h"

#include <QFile>

cusFriRecommJC::cusFriRecommJC(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::cusFriRecommJC)
{
    ui->setupUi(this);
    judge = this->checkBounding();
    if(!judge) {
        ui->textFri->append("QQ与微X尚未绑定");
        ui->textFri->append("无法跨平台推荐好友");
    }
    else
        ui->textFri->append("点击刷新查看推荐好友");
}

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

void cusFriRecommJC::on_pushButton_clicked()
{
    judge = this->checkBounding();
    if(judge)
        this->recommendFriend();
    else {
        ui->textFri->setText("");
        ui->textFri->append("QQ与微X尚未绑定");
        ui->textFri->append("无法跨平台推荐好友");
    }
}

这是点击好友推荐共享时所弹出的内嵌窗口的完整代码;
当点击关于开发者时所弹出的内嵌窗口的代码略去
主要是因为其中只包含一些TextLabel一个小小的彩蛋

其次,为了使用户能退回登录界面实现账户切换,
我添加了退出登录的按钮
关闭和打开窗口本身并不难,但是由于涉及子窗口调用父窗口,故需要特殊的处理,笔者在此处仅附上操作代码,具体的原理解释可以查看笔者的其他博客
用户使用界面中:

void cusChatJC::on_back_clicked()
{
    emit mySignal();
}

登录界面中:

void cusLoginJC::showCusChatJC()
{
    ccj->close();
    this->show();
    this->ui->le_account->setText("");
    this->ui->le_password->setText("");
}

额外的connect信号:

ccj = new cusChatJC(this);
void (cusChatJC::*findSignal) () = &cusChatJC::mySignal;
void (cusLoginJC::*findSlot) () = &cusLoginJC::showCusChatJC;
connect(ccj, findSignal, this, findSlot);

至此,用户使用界面的左右模块均已介绍完毕,
由于代码过长,受限篇幅,就不全部呈现了;
有兴趣具体了解的读者可以在文章末尾下载完整文件自行操作

接下来将着重分析界面的中间模块:
这也算是一个聊天软件的灵魂——信息传输

信息传输的实现

客户端

在如上的用户使用界面中所见的聊天框用作客户端(client)
在设置了服务器(server)地址后便向server发动申请,请求连接:

void cusChatJC::on_pus_connect_clicked()
{
    if(ui->pus_connect->text() == "连接服务") {
        tcpClient->connectToHost(ui->ip_address->text(), ui->ip_port->text().toInt());
        if (tcpClient->waitForConnected(1000)) {
            ui->pus_connect->setText("断开服务");
            ui->pus_send_mess->setEnabled(true);
        }
    }
    else {
        tcpClient->disconnectFromHost();
        if (tcpClient->state() == QAbstractSocket::UnconnectedState \
            || tcpClient->waitForDisconnected(1000)) {
            ui->pus_connect->setText("连接服务");
            ui->pus_send_mess->setEnabled(false);
        }
    }
}

如上代码也包含了连接成功后的断开操作
当server开始监听client后
两者之间便可以开始传送信息:

void cusChatJC::on_pus_send_mess_clicked()
{
    QString data = ui->text_send->toPlainText();
    if(data != "") {
        ui->text_send->setText("");
        QDateTime *datatime=new QDateTime(QDateTime::currentDateTime());
        QString str = datatime->toString("yyyy-MM-dd hh:mm:ss ddd");    //设置时间格式
        QString strSend = str + "  " + username + ":" + "\n" + data;
        ui->text_recv->append(strSend);
        tcpClient->write(strSend.toUtf8());
    }
}

同时client亦能够接收到server所传来的消息:

void cusChatJC::ReadData()
{
    QByteArray buffer = tcpClient->readAll();
    if(!buffer.isEmpty())
        ui->text_recv->append(buffer);
}

由上述3个函数综合对button和槽信号的应用
便可以实现客户端的完整功能

服务器

有client则必有对应的server
为此,我单独编写了一个简易的server:
在这里插入图片描述
其中:
10.151.232.189是服务器的地址
8000则是其端口号
作为sever,首先其能获取IP地址:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    tcpServer = new QTcpServer(this);
    ui->ip_address->setText(QNetworkInterface().allAddresses().at(1).toString());
    //获取本机IP地址
    ui->tcp_listen->setEnabled(true);
    ui->tcp_send->setEnabled(false);

    connect(tcpServer, SIGNAL(newConnection()), this, SLOT(NewConnectionSlot()));
}

这一功能我直接添加在了界面的构造函数里
其次sever能接收来自client的请求:

void MainWindow::NewConnectionSlot()
{
    currentClinet = tcpServer->nextPendingConnection();
    qDebug() << currentClinet << endl;
    tcpClient.append(currentClinet);
    ui->tcp_connect->addItem(tr("%1:%2").arg(currentClinet->peerAddress().toString().split("::ffff:")[1])\
                                        .arg(currentClinet->peerPort()));
    if(tcpClient.length() == 2) {
        connect(currentClientPre, SIGNAL(readyRead()), this, SLOT(autoSend()));
        connect(currentClinet, SIGNAL(readyRead()), this, SLOT(autoSend()));
    }
    else connect(currentClinet, SIGNAL(readyRead()), this, SLOT(ReadData()));
    currentClientPre = currentClinet;
    connect(currentClinet, SIGNAL(disconnected()), this, SLOT(disconnectedSlot()));
}

并且sever能区分所接收的消息由哪一客户端所发送:

void MainWindow::ReadData()
{
    //首先需遍历所有客户端
    for(int i = 0; i < tcpClient.length(); i++) {
        buffer = tcpClient[i]->readAll();
        if(buffer.isEmpty()) continue;

        qDebug() << buffer << endl;

        static QString Ip_port;
        Ip_port = tr("[%1:%2]:").arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])\
                                .arg(tcpClient[i]->peerPort());

        ui->text_receive->append(buffer);
    }
}

且server能指定断开与某一客户端的连接:

void MainWindow::disconnectedSlot()
{
    for(int i=0; i<tcpClient.length(); i++)
        {
            if(tcpClient[i]->state() == QAbstractSocket::UnconnectedState)
            {
                // 删除存储在combox中的客户端信息
                ui->tcp_connect->removeItem(ui->tcp_connect->findText(tr("%1:%2")\
                                      .arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])\
                                      .arg(tcpClient[i]->peerPort())));
                // 删除存储在tcpClient列表中的客户端信息
                 tcpClient[i]->destroyed();
                 tcpClient.removeAt(i);
            }
        }
}

sever对client的监听则是对上述函数的综合利用
sever的信息发送则类似于client的信息发送,只需指明发送至哪一客户端即可
有兴趣具体了解的读者可以在文章末尾下载完整文件自行操作

总结

历时一个半月的简易聊天软件的编写时间上稍显仓促
不过在从架构到编写到最后的基本实现的过程中,自己也是收获良多
不过,仍然有很多细节没有实现
而在点对点的信息的传输中
我目前只实现了客户端——服务器的传输
客户端——客户端的传输仍然没有尝试成功

本人非常欢迎大佬的交流指正,如果能给出一种在现有基础上实现客户端——客户端信息传输的方案,那真是再好不过了

最后,如约附上完整程序实现的下载链接
(包括聊天软件和服务器)
聊天软件:
点击这里
提取码:ob4b
服务器:
点击这里
提取码:q7f2

  • 26
    点赞
  • 136
    收藏
  • 打赏
    打赏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论 13

打赏作者

JOKECHEN66

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值