QT项目:11---模仿腾讯QQ聊天软件

主界面:

  • 实现一个QQ好友列表,列表中人员已确定
  • 点击一位好友,弹出相应的聊天窗口

聊天窗口界面设计:

  • 左上方是一个Text Browser部件,显示用户聊天信息
  • 下方底部是一个Text Edit部件,用来输入聊天信息
  • 上两者之间一行工具控件用于设置聊天信息文本的字体、字号、格式,并配有“更改字体颜色”、“传输文件”、“保存聊天记录”、“清空聊天记录”等功能按钮
  • 右边是一个Table Widget部件,用来显示登陆的用户列表

开发步骤:

  • ①界面设计:就是上面两个界面的设计
  • ②实现基本聊天会话功能:使用UDP广播在群里进行消息聊天
  • ③文件传输:使用TCP实现,分别需要服务器和客户端
  • ④附加功能:设置聊天字体格式、聊天记录的保存与清除等

一、项目的创建

  • 新建Qt Gui应用,项目名称为“QQChat”,基类为QWidget,类名采取默认值

QT项目:11---模仿腾讯QQ聊天软件_第1张图片

  • 在使用到中文的.h和.cpp文件中加入以下几行代码,从而避免中文导致的乱码
#if _MSC_VER >=1600
#pragma execution_character_set("utf-8")
#endif

二、好友列表界面的设计

QT项目:11---模仿腾讯QQ聊天软件_第2张图片

  • 好友列表界面用QToolBox类实现,界面中带图片的按钮使用QToolButton类来实现

开发步骤:

  • 第一步:右击项目,添加“C++类”,类名取为“Drawer”,基类名为“QToolBox”

QT项目:11---模仿腾讯QQ聊天软件_第3张图片QT项目:11---模仿腾讯QQ聊天软件_第4张图片

  • 第二步:更改头文件和和构造函数(因为我们需要将此窗口继承于QToolBox),所以需要将其继承的类名改变
#ifndef DRAWER_H
#define DRAWER_H

#include 
#include 
#include "widget.h"

class Drawer : public QToolBox
{
    Q_OBJECT
public:
    Drawer(QWidget *parent=0,Qt::WindowFlags f=0);
};

#endif // DRAWER_H
Drawer::Drawer(QWidget *parent,Qt::WindowFlags f)
    :QToolBox(parent,f)
{
}
  • 第三步:书写头文件,定义九个QToolButton控件,作为用户
#include 
#include 

class Drawer : public QToolBox
{
/*
...
*/

private:
    QToolButton *toolBtn1;
    QToolButton *toolBtn2;
    QToolButton *toolBtn3;
    QToolButton *toolBtn4;
    QToolButton *toolBtn5;
    QToolButton *toolBtn6;
    QToolButton *toolBtn7;
    QToolButton *toolBtn8;
    QToolButton *toolBtn9;
/*
...
*/
};
  • 第四步:在项目的目录下建立一个“images”目录,在里面放入图片,作为用户的头像和一些列的工具图片使用。然后右击项目,将这些图片作为资源文件导入

QT项目:11---模仿腾讯QQ聊天软件_第5张图片QT项目:11---模仿腾讯QQ聊天软件_第6张图片

QT项目:11---模仿腾讯QQ聊天软件_第7张图片

  • 第五步:书写构造函数,创建九个用户
#include "drawer.h"
#include 
#include 

Drawer::Drawer(QWidget *parent,Qt::WindowFlags f)
    :QToolBox(parent,f)
{
    setWindowTitle(tr("Myself QQ 2017"));            //设置主窗体的标题
    setWindowIcon(QPixmap(":/pic/images/qq.png"));      //设置主窗体标题栏图标

    toolBtn1 =new QToolButton;
    toolBtn1->setText(tr("水漂奇鼋"));  //设置按钮的文字
    toolBtn1->setIcon(QPixmap(":/pic/images/spqy.png"));  //设置按钮的图标
    toolBtn1->setIconSize(QPixmap(":/pic/images/spqy.png").size()); //设置按钮的大小与图标的大小相同
    toolBtn1->setAutoRaise(true); //当鼠标离开时,按钮自动恢复成弹起状态
    toolBtn1->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); //设置按钮的文字显示在图标旁边。每一个按钮的设置方法都是雷同的

    toolBtn2 =new QToolButton;
    toolBtn2->setText(tr("忆梦如澜"));
    toolBtn2->setIcon(QPixmap(":/pic/images/ymrl.png"));
    toolBtn2->setIconSize(QPixmap(":/pic/images/ymrl.png").size());
    toolBtn2->setAutoRaise(true);
    toolBtn2->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn3 =new QToolButton;
    toolBtn3->setText(tr("北京出版人"));
    toolBtn3->setIcon(QPixmap(":/pic/images/qq.png"));
    toolBtn3->setIconSize(QPixmap(":/pic/images/qq.png").size());
    toolBtn3->setAutoRaise(true);
    toolBtn3->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn4 =new QToolButton;
    toolBtn4->setText(tr("Cherry"));
    toolBtn4->setIcon(QPixmap(":/pic/images/Cherry.png"));
    toolBtn4->setIconSize(QPixmap(":/pic/images/Cherry.png").size());
    toolBtn4->setAutoRaise(true);
    toolBtn4->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn5 =new QToolButton;
    toolBtn5->setText(tr("淡然"));
    toolBtn5->setIcon(QPixmap(":/pic/images/dr.png"));
    toolBtn5->setIconSize(QPixmap(":/pic/images/dr.png").size());
    toolBtn5->setAutoRaise(true);
    toolBtn5->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn6 =new QToolButton;
    toolBtn6->setText(tr("娇娇girl"));
    toolBtn6->setIcon(QPixmap(":/pic/images/jj.png"));
    toolBtn6->setIconSize(QPixmap(":/pic/images/jj.png").size());
    toolBtn6->setAutoRaise(true);
    toolBtn6->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn7 =new QToolButton;
    toolBtn7->setText(tr("落水无痕"));
    toolBtn7->setIcon(QPixmap(":/pic/images/lswh.png"));
    toolBtn7->setIconSize(QPixmap(":/pic/images/lswh.png").size());
    toolBtn7->setAutoRaise(true);
    toolBtn7->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn8 =new QToolButton;
    toolBtn8->setText(tr("青墨暖暖"));
    toolBtn8->setIcon(QPixmap(":/pic/images/qmnn.png"));
    toolBtn8->setIconSize(QPixmap(":/pic/images/qmnn.png").size());
    toolBtn8->setAutoRaise(true);
    toolBtn8->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    toolBtn9 =new QToolButton;
    toolBtn9->setText(tr("无语"));
    toolBtn9->setIcon(QPixmap(":/pic/images/wy.png"));
    toolBtn9->setIconSize(QPixmap(":/pic/images/wy.png").size());
    toolBtn9->setAutoRaise(true);
    toolBtn9->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

    QGroupBox *groupBox=new QGroupBox; //创建一个QGroupBpx类实例
    QVBoxLayout *layout=new QVBoxLayout(groupBox); //创建一个QVBoxLayout类实例,用来设置各按钮的布局
    layout->setMargin(20);//布局中各窗体的显示间距
    layout->setAlignment(Qt::AlignLeft);//布局中各窗体的显示位置
    layout->addWidget(toolBtn1); //将按钮加入到布局中
    layout->addWidget(toolBtn2);
    layout->addWidget(toolBtn3);
    layout->addWidget(toolBtn4);
    layout->addWidget(toolBtn5);
    layout->addWidget(toolBtn6);
    layout->addWidget(toolBtn7);
    layout->addWidget(toolBtn8);
    layout->addWidget(toolBtn9);
    layout->addStretch();//插入一个占位符

    this->addItem((QWidget*)groupBox,tr("群成员")); 
}

三、聊天窗口的设计

第一步:

  • 进入“widget.ui”设计模式,将界面的宽度和高度分别设置为730、450

QT项目:11---模仿腾讯QQ聊天软件_第8张图片

第二步:

  • 设计界面

QT项目:11---模仿腾讯QQ聊天软件_第9张图片

  • 相关序号的控件类型以及名称如下:

QT项目:11---模仿腾讯QQ聊天软件_第10张图片

第三步:

设置7个Tool Button组件的属性:

  • ①每个组件的autoRaise属性选中,并且iconSize设置为26*26

QT项目:11---模仿腾讯QQ聊天软件_第11张图片QT项目:11---模仿腾讯QQ聊天软件_第12张图片

  • ②设置图标(上面导入的资源图片)

  • ③前3个Tool Button的checkable属性选中

QT项目:11---模仿腾讯QQ聊天软件_第13张图片

  • ④7个Tool Button的toolTip分别设置为“加粗”、“倾斜”、“下划线”、“更改字体颜色”、“传输文件”、“保存聊天记录”、“清空聊天记录”

QT项目:11---模仿腾讯QQ聊天软件_第14张图片

第四步:

  • 设置字体大小下拉列表,设置范围为“8~22”(与腾讯QQ相同),双击组件设置即可

QT项目:11---模仿腾讯QQ聊天软件_第15张图片

  • 将其currentIndex属性设置为4,即12号字体大小

QT项目:11---模仿腾讯QQ聊天软件_第16张图片

第五步:

  • 设置用户列表Table Widget控件,将其selectionMode属性设置为SingleSelection,将selectBehavior设置为SelectRows

  • 取消showGrid

  • 双击组件,添加“用户名”、“IP地址两个列”

QT项目:11---模仿腾讯QQ聊天软件_第17张图片

四、将好友列表与聊天窗口关联

  • 当点击好友列表中的一个图片按钮时,能够弹出一个用户的聊天窗口

第一步:

  • 在“drawer.h”头文件中加入以下内容
//省略了前面的代码,为了简洁
#include "widget.h"

class Drawer : public QToolBox
{
private:
    ////9个用户分别对应的聊天窗口
    Widget *chatWidget1;
    Widget *chatWidget2;
    Widget *chatWidget3;
    Widget *chatWidget4;
    Widget *chatWidget5;
    Widget *chatWidget6;
    Widget *chatWidget7;
    Widget *chatWidget8;
    Widget *chatWidget9;
private slots:
    //显示9个用户的聊天窗口函数
    void showChatWidget1();
    void showChatWidget2();
    void showChatWidget3();
    void showChatWidget4();
    void showChatWidget5();
    void showChatWidget6();
    void showChatWidget7();
    void showChatWidget8();
    void showChatWidget9();
};

第二步:

  • 在“drawer.cpp”中实现9个showChatWidget()函数,代码都是相似的
void Drawer::showChatWidget1()
{
    chatWidget1 = new Widget(0,toolBtn1->text()); //以toolBtn1的文本为用户名创建一个Widget类的实例,对应于一个聊天窗口
    chatWidget1->setWindowTitle(toolBtn1->text()); //设置窗口标题
    chatWidget1->setWindowIcon(toolBtn1->icon()); //设置窗口图标
    chatWidget1->show(); //显示窗口
}

void Drawer::showChatWidget2()
{
    chatWidget2 = new Widget(0,toolBtn2->text());
    chatWidget2->setWindowTitle(toolBtn2->text());
    chatWidget2->setWindowIcon(toolBtn2->icon());
    chatWidget2->show();
}

void Drawer::showChatWidget3()
{
    chatWidget3 = new Widget(0,toolBtn3->text());
    chatWidget3->setWindowTitle(toolBtn3->text());
    chatWidget3->setWindowIcon(toolBtn3->icon());
    chatWidget3->show();
}

void Drawer::showChatWidget4()
{
    chatWidget4 = new Widget(0,toolBtn4->text());
    chatWidget4->setWindowTitle(toolBtn4->text());
    chatWidget4->setWindowIcon(toolBtn4->icon());
    chatWidget4->show();
}

void Drawer::showChatWidget5()
{
    chatWidget5 = new Widget(0,toolBtn5->text());
    chatWidget5->setWindowTitle(toolBtn5->text());
    chatWidget5->setWindowIcon(toolBtn5->icon());
    chatWidget5->show();
}

void Drawer::showChatWidget6()
{
    chatWidget6 = new Widget(0,toolBtn6->text());
    chatWidget6->setWindowTitle(toolBtn6->text());
    chatWidget6->setWindowIcon(toolBtn6->icon());
    chatWidget6->show();
}

void Drawer::showChatWidget7()
{
    chatWidget7 = new Widget(0,toolBtn7->text());
    chatWidget7->setWindowTitle(toolBtn7->text());
    chatWidget7->setWindowIcon(toolBtn7->icon());
    chatWidget7->show();
}

void Drawer::showChatWidget8()
{
    chatWidget8 = new Widget(0,toolBtn8->text());
    chatWidget8->setWindowTitle(toolBtn8->text());
    chatWidget8->setWindowIcon(toolBtn8->icon());
    chatWidget8->show();
}

void Drawer::showChatWidget9()
{
    chatWidget9 = new Widget(0,toolBtn9->text());
    chatWidget9->setWindowTitle(toolBtn9->text());
    chatWidget9->setWindowIcon(toolBtn9->icon());
    chatWidget9->show();
}

第三步:

  • 为每个用户头像按钮建立信号与槽,当点击时,执行showChatWidget()函数,从而弹出聊天窗口
connect(toolBtn1,SIGNAL(clicked()),this,SLOT(showChatWidget1()));
connect(toolBtn2,SIGNAL(clicked()),this,SLOT(showChatWidget2()));
connect(toolBtn3,SIGNAL(clicked()),this,SLOT(showChatWidget3()));
connect(toolBtn4,SIGNAL(clicked()),this,SLOT(showChatWidget4()));
connect(toolBtn5,SIGNAL(clicked()),this,SLOT(showChatWidget5()));
connect(toolBtn6,SIGNAL(clicked()),this,SLOT(showChatWidget6()));
connect(toolBtn7,SIGNAL(clicked()),this,SLOT(showChatWidget7()));
connect(toolBtn8,SIGNAL(clicked()),this,SLOT(showChatWidget8()));
connect(toolBtn9,SIGNAL(clicked()),this,SLOT(showChatWidget9()));

第四步:

  • 因为Widget类的默认构造函数只有一个参数,但是上面我们构造Widget时,构造函数用到了两个参数,因此需要更改Widget类的构造函数
class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent,QString username);
    ~Widget();

private:
    Ui::Widget *ui;
};
Widget::Widget(QWidget *parent,QString username) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
}

 

第五步:

  • 编写“main.cpp”的内容,如下
  • 当运行程序时,会弹出好友列表界面,然后点击一个好友头像就可以弹出聊天窗口了
#include "drawer.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    /*Widget w;
    w.show();*/

    Drawer drawer;
    drawer.resize(250,700);
    drawer.show();

    return a.exec();
}

 QT项目:11---模仿腾讯QQ聊天软件_第18张图片

五、聊天会话功能实现

  • 在进行聊天时,每个用户是对等的,都是一个端点。用户登录时要进行广播,而且用户退出、发送消息时都是用UDP广播来告知所有用户

QT项目:11---模仿腾讯QQ聊天软件_第19张图片

  • 在传输文件时,文件发送方为服务端,接收方为客户端
  • 服务端发送文件前首先利用UDP发送其文件名,若客户端用户拒绝接收文件,就回送一个UDP应答;如果客户端同一接收文件,则服务器会利用TCP连接向客户端发送文件

QT项目:11---模仿腾讯QQ聊天软件_第20张图片

第一步:

  • 在“widget.h”头文件中定义一个枚举变量,用于区分不同的UDP广播消息类型

QT项目:11---模仿腾讯QQ聊天软件_第21张图片

enum MsgType{Msg,UsrEnter,UsrLeft,FileName,Refuse};

第二步:

  • 定义Widget类的相关成员和函数
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class Widget : public QWidget
{
protected:
    void usrEnter(QString usrname,QString usrname); //处理新用户加入
    void usrLeft(QString usrname,QString time); //处理用户离开
    void sndMsg(MsgType type,QString srvaddr=""); //发送UDP广播消息

    QString getIP(); //获取IP地址
    QString getUsr(); //获取用户名
    QString getMsg(); //获取聊天信息
private:
    QUdpSocket *udpSocket;
    qint16 port;
    QString uName;
private slots:
    void processPendingDatagrams();  //接受UDP消息
};

第三步:

  • 实现构造函数
Widget::Widget(QWidget *parent,QString username) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    uName=username;
    udpSocket=new QUdpSocket(this);
    port=23232;
    
    udpSocket->bind(port,QUdpSocket::ShareAddress|QUdpSocket::ReuseAddressHint); 
    connect(udpSocket,SIGNAL(readyRead()),this,SLOT(processPendingDatagrams()));
    
    sndMsg(UsrEnter); /向广播中发送一个UsrEnter消息,表示自己加入到广播组中了
}

第四步:

  • 实现发送信息的sndMsg函数
void Widget::sndMsg(MsgType type, QString srvaddr)
{
    QByteArray data;
    QDataStream out(&data, QIODevice::WriteOnly);
    QString address = getIP();
    /*向要发送的数据中写入信息类型type,用户名。
     * 其中,type用于接收端分区信息类型,从而对不同类型的信息进行不同的处理*/
    out << type << getUsr();

    switch(type)
    {
    case Msg : //如果是普通的聊天信息Msg
        if (ui->msgTxtEdit->toPlainText() == "") {//首先判断发送的消息是否为空
            QMessageBox::warning(0,tr("警告"),tr("发送内容不能为空"),QMessageBox::Ok);
            return;
        }
        out << address << getMsg();//向发送的数据中写入本机的IP和用户输入的聊天信息文本
        ui->msgBrowser->verticalScrollBar()->setValue(ui->msgBrowser->verticalScrollBar()->maximum());
        break;

    case UsrEnter : //如果是新用户加入
        out << address; //简单的写入IP地址
        break;

    case UsrLeft : //用户离开,不进行任何操作
        break;

    case FileName : { //如果是发送文件,这里先不进行处理,后面再添加代码
        break;
    }

    case Refuse : //拒绝接受文件,这里先不进行处理,后面再添加代码
        break;
    }
    
    //完成信息的处理后,使用writeDatagram函数进行UDP广播
    udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast, port);
}

第五步:

  • 实现接收消息的processPendingDatagrams函数
void Widget::processPendingDatagrams()
{
    while(udpSocket->hasPendingDatagrams()) //如果有可读取的数据
    {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(datagram.data(), datagram.size());//读取数据
        QDataStream in(&datagram, QIODevice::ReadOnly);
        int msgType;
        in >> msgType; //获取消息的类型
        
        QString usrName,ipAddr,msg;
        //获取当前系统的时间
        QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");

        switch(msgType) //判断消息的类型
        {
        case Msg: //如果是聊天消息,获取其用户名、IP和消息数据
            in >> usrName >> ipAddr >> msg;
            ui->msgBrowser->setTextColor(Qt::blue);
            ui->msgBrowser->setCurrentFont(QFont("Times New Roman",12));
            ui->msgBrowser->append("[ " +usrName+" ] "+ time);
            ui->msgBrowser->append(msg);
            break;

        case UsrEnter: //如果是新用户加入,获取其用户名和IP地址
            in >>usrName >>ipAddr;
            usrEnter(usrName,ipAddr); //调用usrEnter函数进行处理
            break;

        case UsrLeft: //如果是用户退出
            in >>usrName;
            usrLeft(usrName,time); //调用usrLeft函数处理用户推出
            break;

        case FileName: {//先不做处理,后面添加代码
            break;
        }

        case Refuse: {//先不做处理,后面添加代码
            break;
        }
        }
    }
}

第六步:

  • 实现处理用户加入和退出的函数
//用户加入
void Widget::usrEnter(QString usrname, QString ipaddr)
{
    bool isEmpty = ui->usrTblWidget->findItems(usrname, Qt::MatchExactly).isEmpty();
    //判断该用户是否先前已经加入到用户列表中了,如果没有
    if (isEmpty) {
        //将相关信息加入到相关的组件中
        QTableWidgetItem *usr = new QTableWidgetItem(usrname);
        QTableWidgetItem *ip = new QTableWidgetItem(ipaddr);

        ui->usrTblWidget->insertRow(0);
        ui->usrTblWidget->setItem(0,0,usr);
        ui->usrTblWidget->setItem(0,1,ip);
        ui->msgBrowser->setTextColor(Qt::gray);
        ui->msgBrowser->setCurrentFont(QFont("Times New Roman",10));
        ui->msgBrowser->append(tr("%1 在线!").arg(usrname));
        ui->usrNumLbl->setText(tr("在线人数:%1").arg(ui->usrTblWidget->rowCount()));

        //再次调用此函数是为了:将先前已经在线的各个端点的用户信息告诉这个新用户,否则新用户无法知晓先前已在线的用户
        sndMsg(UsrEnter);
    }
}
//用户退出
void Widget::usrLeft(QString usrname, QString time)
{
    //将用户从用户列表中删除,然后设置相关显示信息
    int rowNum = ui->usrTblWidget->findItems(usrname, Qt::MatchExactly).first()->row();
    ui->usrTblWidget->removeRow(rowNum);
    ui->msgBrowser->setTextColor(Qt::gray);
    ui->msgBrowser->setCurrentFont(QFont("Times New Roman", 10));
    ui->msgBrowser->append(tr("%1 于 %2 离开!").arg(usrname).arg(time));
    ui->usrNumLbl->setText(tr("在线人数:%1").arg(ui->usrTblWidget->rowCount()));
}

第七步:

  • 实现一系列的获取信息函数
//获取IP地址
QString Widget::getIP()
{
    QList list = QNetworkInterface::allAddresses();
    foreach (QHostAddress addr, list) {
        if(addr.protocol() == QAbstractSocket::IPv4Protocol)
            return addr.toString();
    }
    return 0;
}
//获取当前的用户名
QString Widget::getUsr()
{
    return uName;
}
//获取用户输入的消息,并进行一些设置
QString Widget::getMsg()
{
    QString msg = ui->msgTxtEdit->toHtml();

    ui->msgTxtEdit->clear();
    ui->msgTxtEdit->setFocus();
    return msg;
}

第八步:

  • 发送按钮的触发函数
void Widget::on_sendBtn_clicked()
{
    sndMsg(Msg);
}

六、文件传输的过程与原理

  • 文件传输采用TCP来实现,但是在文件传输之前我们使用UDP广播来告诉对方是否需要接收文件

QT项目:11---模仿腾讯QQ聊天软件_第22张图片

第一步:

  • 在用户列表选择一个用户,然后选择“发送文件”按钮,会打开一个发送文件对话框

QT项目:11---模仿腾讯QQ聊天软件_第23张图片

第二步:

  • 在对话框中选择要发送的文件,然后点击“发送按钮”,这时程序会建立一个TCP服务端并进行监听,然后使用UDP广播将文件名发送给接收端,接收端弹出一个提示框,询问是否要接收指定的文件。如果客户端拒绝接收文件,就取消文件传输并关闭TCP服务端;如果同意就进行正常的TCP数据传输

第三步:

  • 如果同意接收,则在接收端创建一个TCP客户端,然后双方建立一个TCP连接进行文件传输;如果拒绝接收,则客户端会用UDP广播将拒绝消息返回发送端,一旦发送端收到该消息就取消文件的传输

七、文件传输服务端的建立

第一步:

  • 添加Qt设计师界面类,命名为“server”

QT项目:11---模仿腾讯QQ聊天软件_第24张图片QT项目:11---模仿腾讯QQ聊天软件_第25张图片QT项目:11---模仿腾讯QQ聊天软件_第26张图片

第二步:

  • 设计“server.ui”界面

QT项目:11---模仿腾讯QQ聊天软件_第27张图片QT项目:11---模仿腾讯QQ聊天软件_第28张图片

  • 将①的font属性改为12;将⑤的font属性改为10;将④的value默认值改为0

第三步:

  • 在server.h头文件中添加变量和函数声明(只给出了自定义的部分)
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class QFile;
class QTcpServer;
class QTcpSocket;

class server : public QDialog
{
public:
    void initSrv(); //初始化类中的一些成员变量
    //这个函数在主界面Widget类中,当接收到客户端拒绝接收文件的UDP消息时被调用
    void refused(); //关闭服务器
    
protected:
    void closeEvent(QCloseEvent *);
    
private:
    qint16 tPort;
    QTcpServer *tSrv;  //服务端对象
    QString fileName;   //文件名(包括路径)
    QString theFileName;//文件名(不包括路径)
    QFile *locFile;      //待发送的文件

    qint64 totalBytes;     //总共需发送的字节数
    qint64 bytesWritten;   //已发送字节数
    qint64 bytesTobeWrite; //待发送字节数
    qint64 payloadSize;    //被初始化为一个常量
    QByteArray outBlock;   //缓存一次发送的数据

    QTcpSocket *clntConn;  //客户端连接的套接字

    QTime time; //计时器,用来统计传输所用的时间
    
private slots:
    void sndMsg();   //发送数据
    void updClntProgress(qint64 numBytes); //更新进度条
    
signals:
    void sndFileName(QString fileName);
};

第四步:

  • 构造函数与initSrv()函数
server::server(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::server)
{
    setFixedSize(400,207); //将对话框大小固定位400*207

    tPort = 5555;
    tSrv = new QTcpServer(this); //创建TCP服务端对象
    //如果有客户端连接,执行sndMsg函数
    connect(tSrv, SIGNAL(newConnection()), this, SLOT(sndMsg()));

    initSrv(); //初始化TCP服务端
}
void server::initSrv()
{
    payloadSize = 64*1024;
    totalBytes = 0;
    bytesWritten = 0;
    bytesTobeWrite = 0;

    ui->sStatusLbl->setText(tr("请选择要传送的文件"));
    ui->progressBar->reset();
    ui->sOpenBtn->setEnabled(true);
    ui->sSendBtn->setEnabled(false);

    tSrv->close();
}

第五步:

  • 初始化数据发送sndMsg()函数
void server::sndMsg()
{
    ui->sSendBtn->setEnabled(false);
    clntConn = tSrv->nextPendingConnection(); //等待客户端连接(阻塞在此,等待客户端连接)
    connect(clntConn,SIGNAL(bytesWritten(qint64)),this,SLOT(updClntProgress(qint64)));

    ui->sStatusLbl->setText(tr("开始传送文件 %1 !").arg(theFileName));

    locFile = new QFile(fileName);
    if(!locFile->open((QFile::ReadOnly))){ //以只读方式打开文件
        QMessageBox::warning(this, tr("应用程序"), tr("无法读取文件 %1:\n%2").arg(fileName).arg(locFile->errorString()));
        return;
    }
    totalBytes = locFile->size(); //获取待发送文件的大小
    QDataStream sendOut(&outBlock, QIODevice::WriteOnly);//将outBlock封装在一个QDataStream类型的变量中进行读写
    sendOut.setVersion(QDataStream::Qt_5_8);
    time.start();  //开始计时
    //通过right()函数去掉文件的路径部分,仅保留文件名
    QString curFile = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1);
    //构造一个临时的文件头,将值追加到totalBytes字段,从而完成实际需发送字节数的记录
    sendOut << qint64(0) << qint64(0) << curFile;
    totalBytes += outBlock.size();
    //将读写操作指向头
    sendOut.device()->seek(0);
    //填写实际的总长度和文件长度
    sendOut << totalBytes << qint64((outBlock.size() - sizeof(qint64)*2));
    //将该头文件发出,同时修改待发送字节数bytesTobeWrite
    bytesTobeWrite = totalBytes - clntConn->write(outBlock);
    outBlock.resize(0);//清空缓冲区以备下次使用
}

第六步:

  • 初始化更新进度条函数updClntProgress
void server::updClntProgress(qint64 numBytes)
{
    qApp->processEvents(); //用于在传输大文件时使界面不会冻结
    bytesWritten += (int)numBytes;
    if (bytesTobeWrite > 0) {
        outBlock = locFile->read(qMin(bytesTobeWrite, payloadSize));
        bytesTobeWrite -= (int)clntConn->write(outBlock);
        outBlock.resize(0);
    } else {
        locFile->close();
    }
    ui->progressBar->setMaximum(totalBytes);
    ui->progressBar->setValue(bytesWritten);

    float useTime = time.elapsed(); //获取计时器开始计时到现在的耗时的时间
    double speed = bytesWritten / useTime;
    ui->sStatusLbl->setText(tr("已发送 %1MB (%2MB/s) \n共%3MB 已用时:%4秒\n估计剩余时间:%5秒")
                   .arg(bytesWritten / (1024*1024))
                   .arg(speed*1000 / (1024*1024), 0, 'f', 2)
                   .arg(totalBytes / (1024 * 1024))
                   .arg(useTime/1000, 0, 'f', 0)
                   .arg(totalBytes/speed/1000 - useTime/1000, 0, 'f', 0));

    if(bytesWritten == totalBytes) {
        locFile->close();
        tSrv->close();
        ui->sStatusLbl->setText(tr("传送文件 %1 成功").arg(theFileName));
    }
}

第七步:

一系列按钮的响应函数

  • “打开按钮”的响应函数
void server::on_sOpenBtn_clicked()
{   
    //弹出一个文件对话框,选择要发送的文件后更新文本标签和按钮状态
    fileName = QFileDialog::getOpenFileName(this);
    if(!fileName.isEmpty())
    {
        theFileName = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1);
        ui->sStatusLbl->setText(tr("要传送的文件为:%1 ").arg(theFileName));
        ui->sSendBtn->setEnabled(true);
        ui->sOpenBtn->setEnabled(false);
    }
}
  • “关闭按钮”的响应函数
void server::on_sCloseBtn_clicked()
{
    //关闭服务器,然后关闭该对话框
    if(tSrv->isListening())
    {
        tSrv->close();
        if (locFile->isOpen())
            locFile->close();
        clntConn->abort();
    }
    close();
}
  • “发送按钮”的响应函数
void server::on_sSendBtn_clicked()
{
    //将服务器设为监听状态,然后发送sndFileName信号,在主界面中将关联该信号并使用UDP广播将文件名发送给接收端
    if(!tSrv->listen(QHostAddress::Any,tPort))//开始监听
    {
        qDebug() << tSrv->errorString();
        close();
        return;
    }

    ui->sStatusLbl->setText(tr("等待对方接收... ..."));
    emit sndFileName(theFileName);
}

第八步:

  • 窗口关闭事件的处理函数
void server::closeEvent(QCloseEvent *)
{
    on_sCloseBtn_clicked();
}

第九步:

  • refused函数的实现
//这个函数在主界面Widget类中,当接收到客户端拒绝接收文件的UDP消息时被调用
void server::refused()
{
    //如果客户端拒绝接收文件,则关闭服务器
    tSrv->close();
    ui->sStatusLbl->setText(tr("对方拒绝接收!"));
}

八、文件传输客户端的建立

第一步:

  • 添加Qt设计师界面类,命名为“client”

QT项目:11---模仿腾讯QQ聊天软件_第29张图片QT项目:11---模仿腾讯QQ聊天软件_第30张图片QT项目:11---模仿腾讯QQ聊天软件_第31张图片

第二步:

  • 设计“client.ui”界面

QT项目:11---模仿腾讯QQ聊天软件_第32张图片QT项目:11---模仿腾讯QQ聊天软件_第33张图片

第三步:

  • client.h的初始化
#include 
#include 
#include 
#include 
#include 
#include 

class QTcpSocket;

class client : public QDialog
{
public:
    void setHostAddr(QHostAddress addr); //获取发送端IP地址
    void setFileName(QString name); //获取文件保存路径
    
protected:
    void closeEvent(QCloseEvent *);
    
private:
    QTcpSocket *tClnt; //客户端套接字类
    quint16 blockSize;
    QHostAddress hostAddr; //服务端的地址
    qint16 tPort; //服务端的端口

    qint64 totalBytes; //总共需接收的字节数
    qint64 bytesReceived; //已接收字节数
    qint64 fileNameSize;
    QString fileName;
    QFile *locFile;  //待接收的文件
    QByteArray inBlock; //缓存一次接收的数据

    QTime time;
    
private slots:

    void newConn(); //连接到服务器
    void readMsg(); //读取文件数据
    void displayErr(QAbstractSocket::SocketError sockErr); //显示错误信息
};

第四步:

  • 构造函数初始化
client::client(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::client)
{
    ui->setupUi(this);
    setFixedSize(400,190);

    totalBytes = 0;
    bytesReceived = 0;
    fileNameSize = 0;

    tClnt = new QTcpSocket(this); //创建客户端socket对象
    tPort = 5555;
    //关联信号与槽
    connect(tClnt, SIGNAL(readyRead()), this, SLOT(readMsg()));
    //如果有错误,就执行displayErr槽函数
    connect(tClnt, SIGNAL(error(QAbstractSocket::SocketError)), this,SLOT(displayErr(QAbstractSocket::SocketError)));
}
  • 打印错误信息displayErr函数
void client::displayErr(QAbstractSocket::SocketError sockErr)
{
    switch(sockErr)
    {
    case QAbstractSocket::RemoteHostClosedError : break;
    default : qDebug() << tClnt->errorString();
    }
}
  •  获取发送端IP地址(Widget类主界面中获取发送端的IP地址,就是这个函数提供的)
void client::setHostAddr(QHostAddress addr)
{
    hostAddr = addr;
    newConn();
}
  • 获取文件保存路径(Widget类主界面中弹出文件对话框来选择文件的保存路径,要在客户端中提供这个函数来获取路径)
void client::setFileName(QString name)
{
    locFile = new QFile(name);
}

第五步:

  • 连接服务端函数
void client::newConn()
{
    blockSize = 0;
    tClnt->abort();
    tClnt->connectToHost(hostAddr, tPort);
    time.start(); //开始计时
}
  • 接收服务端文件数据函数
void client::readMsg()
{
    QDataStream in(tClnt);
    in.setVersion(QDataStream::Qt_5_8);

    float useTime = time.elapsed();

    if (bytesReceived <= sizeof(qint64)*2) {
        if ((tClnt->bytesAvailable() >= sizeof(qint64)*2) && (fileNameSize == 0))
        {
            in>>totalBytes>>fileNameSize;
            bytesReceived += sizeof(qint64)*2;
        }
        if((tClnt->bytesAvailable() >= fileNameSize) && (fileNameSize != 0)){
            in>>fileName;
            bytesReceived +=fileNameSize;

            if(!locFile->open(QFile::WriteOnly)){
                QMessageBox::warning(this,tr("应用程序"),tr("无法读取文件 %1:\n%2.").arg(fileName).arg(locFile->errorString()));
                return;
            }
        } else {
            return;
        }
    }
    if (bytesReceived < totalBytes) {
        bytesReceived += tClnt->bytesAvailable();
        inBlock = tClnt->readAll();
        locFile->write(inBlock);
        inBlock.resize(0);
    }
    ui->progressBar->setMaximum(totalBytes);
    ui->progressBar->setValue(bytesReceived);

    double speed = bytesReceived / useTime;
    ui->cStatusLbl->setText(tr("已接收 %1MB (%2MB/s) \n共%3MB 已用时:%4秒\n估计剩余时间:%5秒")
                                      .arg(bytesReceived / (1024*1024))
                                      .arg(speed*1000/(1024*1024),0,'f',2)
                                      .arg(totalBytes / (1024 * 1024))
                                      .arg(useTime/1000,0,'f',0)
                                      .arg(totalBytes/speed/1000 - useTime/1000,0,'f',0));

    if(bytesReceived == totalBytes)
    {
        locFile->close();
        tClnt->close();
        ui->cStatusLbl->setText(tr("接收文件 %1 完毕").arg(fileName));
    }
}

第六步:

一系列按钮的响应函数

  • “取消按钮”的响应函数
void client::on_cCancleBtn_clicked()
{
    tClnt->abort();
    if (locFile->isOpen())
        locFile->close();
}
  • “关闭按钮的响应函数”
void client::on_cCloseBtn_clicked()
{
    tClnt->abort();
    if (locFile->isOpen())
        locFile->close();
    close();
}
  • 窗口关闭事件函数重写
void client::closeEvent(QCloseEvent *)
{
    on_cCloseBtn_clicked();
}

九、主界面控制

  • 上面服务端和客户端设计好之后,就可以开始进行文件的发送与接收了

第一步:

  • 在“widget.h”中声明变量和函数
class server;

#include "server.h"
#include "client.h"
#include 

class Widget : public QWidget
{
protected:
    //用于在收到文件名UDP消息时判断是否接收该文件
    void hasPendingFile(QString usrname, QString srvaddr,QString clntaddr, QString filename);

private:
    QString fileName;
    server *srv; //服务器对象

private slots:
    void getFileName(QString name); //获取服务器sndFileName()信号发送过来的文件名
};

第二步:

  • 在“widget.cpp”构造函数中创建服务器对象
Widget::Widget(QWidget *parent,QString username) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    srv=new server(this);
    connect(srv, SIGNAL(sndFileName(QString)), this, SLOT(getFileName(QString)));
}
  • 获取文件名函数
void Widget::getFileName(QString name)
{
    fileName = name;
    sndMsg(FileName);//发送FileName类型的UDP广播
}

第三步:

  • 主界面传输文件按钮的响应函数

void Widget::on_sendTBtn_clicked()
{
    //判断是否选中用户,然后弹出”发送“对话框
    if(ui->usrTblWidget->selectedItems().isEmpty())
    {
        QMessageBox::warning(0, tr("选择用户"),tr("请先选择目标用户!"), QMessageBox::Ok);
        return;
    }
    srv->show();
    srv->initSrv();
}

第四步:

  • 更改原先的sndMsg()函数内FileName核Refuse处的代码
void Widget::sndMsg(MsgType type, QString srvaddr)
{
    switch(type)
    {
    case FileName: {
        int row = ui->usrTblWidget->currentRow();
        QString clntaddr = ui->usrTblWidget->item(row, 1)->text();
        out << address << clntaddr << fileName;
        break;
    }

    case Refuse :
        out << srvaddr;
        break;
    }
}
  • 更改processPendingDatagrams()函数中FileName和Refuse处的代码
void Widget::processPendingDatagrams()
{
    while(udpSocket->hasPendingDatagrams()) //如果有可读取的数据
    {
        switch(msgType) 
        {
        case FileName: {
            in >> usrName >> ipAddr;
            QString clntAddr, fileName;
            in >> clntAddr >> fileName;
            //判断是否要接收该文件
            hasPendingFile(usrName, ipAddr, clntAddr, fileName);
            break;
        }

        case Refuse: {
            in >> usrName;
            QString srvAddr;
            in >> srvAddr;
            QString ipAddr = getIP();

            //判断该程序是否为发送端,如果是,执行服务器的refused函数
            if(ipAddr == srvAddr)
            {
                srv->refused();
            }
            break;
        }
        }
    }
}

第五步:

  • hasPendingFile用于在收到文件名UDP消息时判断是否接收该文件
void Widget::hasPendingFile(QString usrname, QString srvaddr,QString clntaddr, QString filename)
{
    QString ipAddr = getIP();
    if(ipAddr == clntaddr)
    {
        //弹出对话框,使用户判断是否要接收文件
        int btn = QMessageBox::information(this,tr("接受文件"),tr("来自%1(%2)的文件:%3,是否接收?").arg(usrname).arg(srvaddr).arg(filename),QMessageBox::Yes,QMessageBox::No);
        if (btn == QMessageBox::Yes) {//如果接收,则创建客户端对象来传输文件
            QString name = QFileDialog::getSaveFileName(0,tr("保存文件"),filename);
            if(!name.isEmpty())
            {
                client*clnt = new client(this);
                clnt->setFileName(name);
                clnt->setHostAddr(QHostAddress(srvaddr));
                clnt->show();
            }
        } else {//拒绝接收,发送拒绝消息的UDP广播
            sndMsg(Refuse, srvaddr);
        }
    }
}

到此为止,基本的功能都完成了

十、更改字体、字号和颜色

更改字体

  • 添加“widget.ui”中FontComboBox组件的currentFontChanged(QFont)信号函数
void Widget::on_fontCbx_currentFontChanged(const QFont &f)
{
    //首先获取当前选择的字体,然后在消息编辑器中使用改字体
    ui->msgTxtEdit->setCurrentFont(f);
    ui->msgTxtEdit->setFocus();
}

更改字体大小

  • 添加“widget.ui”中Combo Box组件的currentIndexChanged(QString)信号函数
void Widget::on_sizeCbx_currentIndexChanged(const QString &arg1)
{
    ui->msgTxtEdit->setFontPointSize(arg1.toDouble());
    ui->msgTxtEdit->setFocus();
}

设置字体加粗、倾斜、下划线等按钮的响应函数

  • 加粗按钮响应函数(clicked(bool))
void Widget::on_boldTBtn_clicked(bool checked)
{
    if(checked)
        ui->msgTxtEdit->setFontWeight(QFont::Bold);
    else
        ui->msgTxtEdit->setFontWeight(QFont::Normal);
    ui->msgTxtEdit->setFocus();
}
  • 倾斜按钮响应函数(clicked(bool))
void Widget::on_italicTBtn_clicked(bool checked)
{
    ui->msgTxtEdit->setFontItalic(checked);
    ui->msgTxtEdit->setFocus();
}
  • 下划线按钮响应函数(clicked(bool))
void Widget::on_underlineTBtn_clicked(bool checked)
{
    ui->msgTxtEdit->setFontUnderline(checked);
    ui->msgTxtEdit->setFocus();
}

设置文本颜色

  • 添加“widget.ui”中更改颜色组件的响应函数
void Widget::on_colorTBtn_clicked()
{
    color = QColorDialog::getColor(color,this);
    if(color.isValid()){
        ui->msgTxtEdit->setTextColor(color);
        ui->msgTxtEdit->setFocus();
    }
}
  • 但是需要在“widget.h”头文件中加入以下代码
#include 

class Widget : public QWidget
{
private:
    QColor color;
};

字体切换

如果在文本编辑器中几段文本上使用了不同的字体格式,则还需要添加函数使光标在不同的文本上点击时可以使编辑器自动切换为对应的格式

  • 第一步:增加头文件和私有槽函数
#include 

class Widget : public QWidget
{
private slots:
    void curFmtChanged(const QTextCharFormat &fmt);
};
  • 第二步:在构造函数中,将信号与槽函数关联
Widget::Widget(QWidget *parent,QString username) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    connect(ui->msgTxtEdit, SIGNAL(currentCharFormatChanged(QTextCharFormat)),this, SLOT(curFmtChanged(const QTextCharFormat)));
}
  • 第三步:实现槽函数
void Widget::curFmtChanged(const QTextCharFormat &fmt)
{
    ui->fontCbx->setCurrentFont(fmt.font());
    //如果字体大小出错(因为最小的字体为8),则使用12
    if (fmt.fontPointSize() < 8) {
        ui->sizeCbx->setCurrentIndex(4);
    } else {
        ui->sizeCbx->setCurrentIndex(ui->sizeCbx->findText(QString::number(fmt.fontPointSize())));
    }
    ui->boldTBtn->setChecked(fmt.font().bold());
    ui->italicTBtn->setChecked(fmt.font().italic());
    ui->underlineTBtn->setChecked(fmt.font().underline());
    color = fmt.foreground().color();
}

十一、保存和清除聊天记录

保存聊天记录

  • 第一步:在“widget.h”中添加protected函数声明
class Widget : public QWidget
{
    bool saveFile(const QString& filename);
}
  • 第二步:实现UI界面中的saveBtn组件的响应函数
void Widget::on_saveTBtn_clicked()
{
    if (ui->msgBrowser->document()->isEmpty()) {
        QMessageBox::warning(0, tr("警告"), tr("聊天记录为空,无法保存!"), QMessageBox::Ok);
    } else {
        QString fname = QFileDialog::getSaveFileName(this,tr("保存聊天记录"), tr("聊天记录"), tr("文本(*.txt);;所有文件(*.*)"));
        if(!fname.isEmpty())
            saveFile(fname);
    }
}
  • 第三步:实现saveFile()函数
bool Widget::saveFile(const QString &filename)
{
    QFile file(filename);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("保存文件"),tr("无法保存文件 %1:\n %2").arg(filename).arg(file.errorString()));
        return false;
    }
    QTextStream out(&file);
    out << ui->msgBrowser->toPlainText();

    return true;
}

点击保存文件按钮,就会跳出一个文件保存对话框,将聊天记录保存为一个.txt文件

清除聊天记录

  • 第一步:实现UI界面中clearTBtn按钮的响应函数
void Widget::on_clearTBtn_clicked()
{
     ui->msgBrowser->clear();
}
  • 第二步:实现退出按钮的响应函数
void Widget::on_exitBtn_clicked()
{
    close();
}
  • 第三步:编写关闭事件
class Widget : public QWidget
{
protected:
    void closeEvent(QCloseEvent *);
}
void Widget::closeEvent(QCloseEvent *e)
{
    sndMsg(UsrLeft); //发送离开广播消息,使其他端点在其用户列表中删除该用户
    QWidget::closeEvent(e);
}

 

你可能感兴趣的:(QT项目)