Qt基础项目篇——聊天软件

一、核心界面演示

1. 主界面

程序只实现模拟群聊功能。

群成员已事先确定,并以一组带图片的按钮的形式在“抽屉盒”中列出。单击“抽屉盒”里的某个按钮,弹出相应成员用户的聊天窗口。

2. 聊天窗口界面

分为4个部分

①显示聊天记录

②发送聊天信息

③工具控件

④显示登陆的用户列表

3. 开发步骤

①界面设计开发

抽屉盒、聊天界面、传输文件界面

②实现基本聊天会话功能

主要使用UDP广播方式在群里进行消息会话,聊天信息实时地显示在左上方的 Text Browser 控件中。

③实现文件传输功能

该功能使用 TCP 实现,需要分别实现服务器和客户端。

④增添附加功能

信息文本字体格式设置、聊天记录的保存和清除。

二、界面设计与开发

新建 Qt Widgets Application,项目名称命名 MySelfQQ,基类选 QWidget,类名为 Widget。

1. 创建“抽屉盒”

抽屉盒用 QToolBox 类实现,其带上图片的按钮则采用 QToolButton 类实现。

1)定义9个 QToolButton 控件,对于群里的9个成员用户。

#ifndef DRAWER_H
#define DRAWER_H

#include <QToolBox>
#include <QToolButton>

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

private:
    QToolButton *toolBtn1;
    QToolButton *toolBtn2;
    QToolButton *toolBtn3;
    QToolButton *toolBtn4;
    QToolButton *toolBtn5;
    QToolButton *toolBtn6;
    QToolButton *toolBtn7;
    QToolButton *toolBtn8;
    QToolButton *toolBtn9;
};

#endif // DRAWER_H

2)向项目导入图片资源

对应9个按钮需要9张图片作为头像图标使用,搜集9张图片。

3)创建 .qrc 文件

<RCC>
    <qresource prefix="/">
        <file>images/spqy.png</file>
        <file>images/ymrl.png</file>
        <file>images/qq.png</file>
        <file>images/Cherry.png</file>
        <file>images/dr.png</file>
        <file>images/jj.png</file>
        <file>images/lswh.png</file>
        <file>images/qmnn.png</file>
        <file>images/wy.png</file>
    </qresource>
</RCC>

4)drawer.cpp

#include "drawer.h"

Drawer::Drawer(QWidget *parent, Qt::WindowFlags f)
    :QToolBox(parent,f)
{
    setWindowTitle(tr("Myself QQ 2025"));                   //设置主窗体的标题
    setWindowIcon(QPixmap(":/images/qq.png"));              //设置主窗体标题栏图标
    
    toolBtn1 =new QToolButton;
    toolBtn1->setText(tr("水漂奇鼋"));
    toolBtn1->setIcon(QPixmap(":/images/spqy.png"));
    toolBtn1->setIconSize(QPixmap(":/images/spqy.png").size());
    toolBtn1->setAutoRaise(true);
    toolBtn1->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn1,SIGNAL(clicked()),this,SLOT(showChatWidget1()));
    
    toolBtn2 =new QToolButton;
    toolBtn2->setText(tr("忆梦如澜"));
    toolBtn2->setIcon(QPixmap(":/images/ymrl.png"));
    toolBtn2->setIconSize(QPixmap(":/images/ymrl.png").size());
    toolBtn2->setAutoRaise(true);
    toolBtn2->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn2,SIGNAL(clicked()),this,SLOT(showChatWidget2()));
    
    toolBtn3 =new QToolButton;
    toolBtn3->setText(tr("北京出版人"));
    toolBtn3->setIcon(QPixmap(":/images/qq.png"));
    toolBtn3->setIconSize(QPixmap(":/images/qq.png").size());
    toolBtn3->setAutoRaise(true);
    toolBtn3->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn3,SIGNAL(clicked()),this,SLOT(showChatWidget3()));
    
    toolBtn4 =new QToolButton;
    toolBtn4->setText(tr("Cherry"));
    toolBtn4->setIcon(QPixmap(":/images/Cherry.png"));
    toolBtn4->setIconSize(QPixmap(":/images/Cherry.png").size());
    toolBtn4->setAutoRaise(true);
    toolBtn4->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn4,SIGNAL(clicked()),this,SLOT(showChatWidget4()));
    
    toolBtn5 =new QToolButton;
    toolBtn5->setText(tr("淡然"));
    toolBtn5->setIcon(QPixmap(":/images/dr.png"));
    toolBtn5->setIconSize(QPixmap(":/images/dr.png").size());
    toolBtn5->setAutoRaise(true);
    toolBtn5->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn5,SIGNAL(clicked()),this,SLOT(showChatWidget5()));
    
    toolBtn6 =new QToolButton;
    toolBtn6->setText(tr("娇娇girl"));
    toolBtn6->setIcon(QPixmap(":/images/jj.png"));
    toolBtn6->setIconSize(QPixmap(":/images/jj.png").size());
    toolBtn6->setAutoRaise(true);
    toolBtn6->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn6,SIGNAL(clicked()),this,SLOT(showChatWidget6()));
    
    toolBtn7 =new QToolButton;
    toolBtn7->setText(tr("落水无痕"));
    toolBtn7->setIcon(QPixmap(":/images/lswh.png"));
    toolBtn7->setIconSize(QPixmap(":/images/lswh.png").size());
    toolBtn7->setAutoRaise(true);
    toolBtn7->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn7,SIGNAL(clicked()),this,SLOT(showChatWidget7()));
    
    toolBtn8 =new QToolButton;
    toolBtn8->setText(tr("青墨暖暖"));
    toolBtn8->setIcon(QPixmap(":/images/qmnn.png"));
    toolBtn8->setIconSize(QPixmap(":/images/qmnn.png").size());
    toolBtn8->setAutoRaise(true);
    toolBtn8->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn8,SIGNAL(clicked()),this,SLOT(showChatWidget8()));
    
    toolBtn9 =new QToolButton;
    toolBtn9->setText(tr("无语"));
    toolBtn9->setIcon(QPixmap(":/images/wy.png"));
    toolBtn9->setIconSize(QPixmap(":/images/wy.png").size());
    toolBtn9->setAutoRaise(true);
    toolBtn9->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    //connect(toolBtn9,SIGNAL(clicked()),this,SLOT(showChatWidget9()));
    
    QGroupBox *groupBox=new QGroupBox;
    QVBoxLayout *layout=new QVBoxLayout(groupBox);
    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("群成员"));    
}

Qt6 中 setContentsMargins方法来替代setMargin方法。

    layout->setContentsMargins(20,20,20,20);//布局中各窗体的显示间距

2. 设计聊天窗口

2.1 基本界面布局

界面宽度、高度属性分别设置为730和450

序号或图标Test BrowermsgBrower
Text BrowermsgBower
Text EditmsgTxtEdit
Table WidgetusrTblWidget
Font Combo BoxfontCbx
comboBoxsizeCbx
pushButtonsendBtn
labelusrNumLbl
pushButtonexitBtn
Tool ButtonboldTBtn
Tool ButtonitalicTBtn
Tool ButtonunderlineTBtn
Tool ButtoncolorTBtn
Tool ButtonsendTBtn
Tool ButtonsaveTBtn
Tool ButtonclearTBtn
<RCC>
    <qresource prefix="/">
        ......
        <file>images/bold.png</file>
        <file>images/clear.png</file>
        <file>images/color.png</file>
        <file>images/italic.png</file>
        <file>images/save.png</file>
        <file>images/send.png</file>
        <file>images/under.png</file>
    </qresource>
</RCC>

Tool Button 设置为 宽33,高32;icon 选择特定的图像。

iconSize 宽和高均为 26,选中 autoRaise,前三个按钮选中 checkable 属性。

将它们的 toolTip 属性依次更改为 “加粗”、“倾斜”、“下划线”、“更改字体颜色”、“传输文件”、“保存聊天记录”和“清空聊天记录”。

2.2 特定控件属性设置

(1)字体大小下拉列表框

双击 combox,添加8到22,currentIndex 属性设置为4,默认字体为12号字。

(2)显示用户列表的 Table Widget 控件。

控件③

3. 将图片按钮与聊天窗口关联

3.1 声明 聊天窗 对象

//draw.h
class Drawer : public QToolBox
{
    Q_OBJECT
public:
    Drawer(QWidget *parent=0,Qt::WindowFlags f=0);
private slots:
    void showChatWidget1();
    void showChatWidget2();
    void showChatWidget3();
    void showChatWidget4();
    void showChatWidget5();
    void showChatWidget6();
    void showChatWidget7();
    void showChatWidget8();
    void showChatWidget9();
private:
    QToolButton *toolBtn1;
    QToolButton *toolBtn2;
    QToolButton *toolBtn3;
    QToolButton *toolBtn4;
    QToolButton *toolBtn5;
    QToolButton *toolBtn6;
    QToolButton *toolBtn7;
    QToolButton *toolBtn8;
    QToolButton *toolBtn9;    
    Widget *chatWidget1;
    Widget *chatWidget2;
    Widget *chatWidget3;
    Widget *chatWidget4;
    Widget *chatWidget5;
    Widget *chatWidget6;
    Widget *chatWidget7;
    Widget *chatWidget8;
    Widget *chatWidget9;
};

3.2 定义 聊天窗 显示函数

void Drawer::showChatWidget1()
{
    chatWidget1 = new Widget(0,toolBtn1->text());
    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();
}

稍微调试一下看看 ,那么需要先做以下调整:

//widget.h
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Widget : public QWidget
{
    Q_OBJECT

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


};
#endif // WIDGET_H
//widget.cpp
#include "widget.h"

Widget::Widget(QWidget *parent,QString usrname)
    : QWidget(parent)
{}

Widget::~Widget() {}
//main.cpp
#include "widget.h"
#include "drawer.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Drawer drawer;
    drawer.resize(250,700);
    drawer.show();
    return a.exec();
}

 最终标题栏如图:

3.3 绑定按钮与显示函数

现在,在 “drawer.cpp” 中的 Drawer 类构造函数中的 connect 语句需要绑定了。

三、基本聊天会话功能实现

1. 消息类型与UDP广播

UDP 广播消息类型
消息类型用途

Msg

聊天信息
UsrEnter新用户加入
UsrLeft用户退出
FileName文件名
Refuse拒绝接收文件

在 widget.h 中定义枚举变量 MsgType,用于区分不同的广播消息类型:

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

1.2 声明变量、函数和头文件

//widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
enum MsgType{Msg, UsrEnter, UsrLeft, FileName, Refuse};
class QUdpSocket;

namespace Ui {
class Widget;
}
class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent,QString usrname);
    ~Widget();
    
protected:
    void usrEnter(QString usrname,QString ipaddr);  //处理新用户加入
    void usrLeft(QString usrname,QString time);     //处理用户离开
    void sndMsg(MsgType type, QString srvaddr="");  //广播UDP消息
    
    QString getIP();                                //获取IP地址
    QString getUsr();                               //获取用户名
    QString getMsg();                               //获取聊天信息
    
private:
    Ui::Widget *ui;
    QUdpSocket *udpSocket;
    qint16 port;
    QString uName;
    
private slots:
    void processPendingDatagrams(); //接收UDP消息

};
#endif // WIDGET_H
//widget.cpp

#include <QUdpSocket>
#include <QHostInfo>
#include <QMessageBox>
#include <QScrollBar>
#include <QDateTime>
#include <QNetworkInterface>
#include <QProcess>

1.3 发送 UDP 广播

构造函数:

//widget.cpp

Widget::Widget(QWidget *parent,QString usrname)
    : QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    uName = usrname;
    udpSocket = new QUdpSocket(this);
    port = 23232;
    udpSocket->bind(port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams()));
    sndMsg(UsrEnter);
}

Widget::~Widget() {}

 这里创建了 UDP 套接字并进行了初始化,端口默认为23232,槽函数 processPendingDatagrams()用来接收来自其他用户的 UDP 广播消息。

发送 UDP 广播消息:

void Widget::sndMsg(MsgType type, QString srvaddr)
{
    QByteArray data;
    QDataStream out(&data, QIODevice::WriteOnly);
    QString address = getIP();
    out << type << getUsr();

    switch(type)
    {
    case Msg :
        if (ui->msgTxtEdit->toPlainText() == "") {
            QMessageBox::warning(0,tr("警告"),tr("发送内容不能为空"),QMessageBox::Ok);
            return;
        }
        out << address << getMsg();
        ui->msgBrowser->verticalScrollBar()->setValue(ui->msgBrowser->verticalScrollBar()->maximum());
        break;

    case UsrEnter :
        out << address;
        break;

    case UsrLeft :
        break;

    case FileName : {
        break;
    }

    case Refuse :
        break;
    }
    udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast, port);
}

out << type << getUsr() 向发送数据写入信息类型、用户名。

case Msg 对于普通的聊天消息 Msg,如果为空则警告;然后向发送的数据写入本机的IP和用户输入的文本信息。

case UsrEnter 对于新用户加入 UsrEnter,只是简单的向数据中加入IP地址。

case UsrLeft 对于用户离开,不需要进行任何操作。

case FileName,case Refuse 对于发送文件名和拒绝接收文件 这一部分留在第四节。

1.4 接收 UDP 消息

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:
            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:
            in >>usrName >>ipAddr;
            usrEnter(usrName,ipAddr);
            break;

        case UsrLeft:
            in >>usrName;
            usrLeft(usrName,time);
            break;

        case FileName: {
            break;
        }

        case Refuse: {

            break;
        }
        }
    }
}

case Msg:in >> usrName >> ipAddr >> msg;

如果是普通聊天消息Msg,就获取用户名、IP和内容信息等数据,然后将用户名和聊天内容显示在左上角的 msgBrower 中,在显示聊天消息的同时还需显示系统当前日期时间。

2. 会话过程的处理

2.1 usrEnter()函数

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);
    }
}
  •  usrname 判断用户是否以及加入用户列表
    • 若没有,加入后,向右侧的用户列表 usrTblWidget 中添加用户的消息
    • 左上方的 msgBrower 中显示用户加入提示信息

2.2 usrLeft()函数

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()));
}

 用户列表中,将离开用户的信息删除,然后进行提示。

2.3 getIP() 和 getUsr() 函数

获取 IP 和用户名

QString Widget::getIP()
{
    QList<QHostAddress> list = QNetworkInterface::allAddresses();
    foreach (QHostAddress addr, list) {
        if(addr.protocol() == QAbstractSocket::IPv4Protocol)
            return addr.toString();
    }
    return 0;
}
QString Widget::getUsr()
{
    return uName;
}

2.4 getMsg()函数

获取用户输入的聊天消息

QString Widget::getMsg()
{
    QString msg = ui->msgTxtEdit->toHtml();

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

从消息文本编辑器获取用户输入消息,然后清空文本编辑器。

发送 按钮的单击信号 clicked 对于槽:

void Widget::on_sendBtn_clicked()
{
    sndMsg(Msg);
}

3. 聊天程序试程序

记得在 .pro 文件中添加:

QT       += network

ui的对象名记得改成Widget

四、文件传输功能实现

1. 需求方案

之前的聊天会话使用的是 UDP,文件的传输采用 TCP 来实现,采用 C/S 方式。

创建两个新的类来分别实现 TCP 服务器和 TCP 客户服的功能。

对于文件传输的流程,简单描述如下:

(1)在主界面用户列表中首先选中要为其发送文件的用户。然后单击“传输文件”按钮打开“发送”文件对话框。

(2)“发送”文件对话框中,用户要首先选择传输的文件,然后单击“发送”按钮。程序会使用 UDP 广播将文件名发送给接收端,接收端在收到发送文件的 UDP 消息时,就会弹出一个提示框,询问是否要接收指定的文件。

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

(3)当打开文件并单击“发送”按钮后,服务器进入监听状态并使用 UDP 广播将要传输的文件名发送给接受端。如果接收端拒绝接收该文件,则关闭服务器,否则进行正常的 TCP 数据传输。

2. 服务器开发

2.1 界面设计

创建新的 Qt 设计师界面类,类名更为 Sever。

服务器界面控件 objetctName 属性
序号类型objectName 属性
Labellabel_2
Push ButtonsOpenBtn
Push ButtonsSendBtn
Progress BarprogressBar
LabelsStatusLbl
Push ButtonsCloseBtn

 ①的font改为12的大小,⑤的font改为10,将④的value属性设为0。

界面的 windowTitle 属性设置为 “发送”。

2.2 声明变量和函数

//server.h

#include <QDialog>
#include <QTime>

class QFile;
class QTcpServer;
class QTcpSocket;

namespace Ui {
class Server;
}

class Server : public QDialog
{
    Q_OBJECT
    
public:
    explicit Server(QWidget *parent = 0);
    ~Server();
    
    void initSrv();
    void refused();
protected:
    void closeEvent(QCloseEvent *);
private:
    Ui::Server *ui;
    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);        
};

class QTcpServer:在TCP服务器类中,要创建一个发送对话框以供用户选择文件发送,这里是通过新创建的 QTcpServer 对象实现。

2.3 服务器初始化

//server.cpp

#include "server.h"
#include "ui_server.h"

#include <QFile>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMessageBox>
#include <QFileDialog>
#include <QDebug>

Server::Server(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Server)
{
    ui->setupUi(this);
    setFixedSize(400,207);

    tPort = 5555;
    tSrv = new QTcpServer(this);
    connect(tSrv, SIGNAL(newConnection()), this, SLOT(sndMsg()));

    initSrv();
}

服务器初始化

//server.cpp

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();
}

2.4 发送数据

//server.cpp

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);
    sendOut.setVersion(QDataStream::Qt_5_8);
    time.start();  // 开始计时
    QString curFile = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1);
    sendOut << qint64(0) << qint64(0) << curFile;
    totalBytes += outBlock.size();
    sendOut.device()->seek(0);
    sendOut << totalBytes << qint64((outBlock.size() - sizeof(qint64)*2));
    bytesTobeWrite = totalBytes - clntConn->write(outBlock);
    outBlock.resize(0);
}

2.5 更新进度条

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));
    }
}

2.6 服务器界面按钮的槽函数

(1)打开

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);
    }
}

(2)发送

void Server::on_sSendBtn_clicked()
{
    if(!tSrv->listen(QHostAddress::Any,tPort))//开始监听
    {
        qDebug() << tSrv->errorString();
        close();
        return;
    }

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

(3)关闭

void Server::on_sCloseBtn_clicked()
{
    if(tSrv->isListening())
    {
        tSrv->close();
        if (locFile->isOpen())
            locFile->close();
        clntConn->abort();
    }
    close();
}
void Server::closeEvent(QCloseEvent *)
{
    on_sCloseBtn_clicked();
}
void Server::refused()
{
    tSrv->close();
    ui->sStatusLbl->setText(tr("对方拒绝接收!"));
}

3. 客户端开发

3.1 界面设计

一样的,添加新的 Qt 设计师界面类,类名更为 Client,设计client.ui。

序号类型objectName 属性
Labellabel
Progress BarprogressBar
LabelcStatusLbl
Push ButtoncCancelBtn
Push ButtoncCloseBtn

将界面的 windowTitle 属性设置为“接收”;将①的显示文本更改为“已完成”,在 font 中将点大小设置为 12;将③的显示文本更改为“等待接收文件···”,在 font 中将点大小设置为 10;将②的 value 属性设为0。

3.2 声明变量和函数

//client.h

#ifndef CLIENT_H
#define CLIENT_H

#include <QDialog>
#include <QHostAddress>
#include <QFile>
#include <QTime>
class QTcpSocket;

namespace Ui {
class Client;
}

class Client : public QDialog
{
    Q_OBJECT
    
public:
    explicit Client(QWidget *parent = 0);
    ~Client();
    
    void setHostAddr(QHostAddress addr);        //获取发送端 IP 地址
    void setFileName(QString name);             //获取文件保存路径
    
protected:
    void closeEvent(QCloseEvent *);
private:
    Ui::Client *ui;                             
    
    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);  //显示错误信息
};

#endif // CLIENT_H

3.3 客户端初始化

//client.cpp

#include "client.h"
#include "ui_client.h"

#include <QTcpSocket>
#include <QDebug>
#include <QMessageBox>
//client.cpp
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);
    tPort = 5555;
    connect(tClnt, SIGNAL(readyRead()), this, SLOT(readMsg()));
    connect(tClnt, SIGNAL(error(QAbstractSocket::SocketError)), this,SLOT(displayErr(QAbstractSocket::SocketError)));
}
void Client::displayErr(QAbstractSocket::SocketError sockErr)
{
    switch(sockErr)
    {
    case QAbstractSocket::RemoteHostClosedError : break;
    default : qDebug() << tClnt->errorString();
    }
}

3.4 与服务器的连接

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));
    }
}

3.5 客户端界面按钮的槽函数

取消

void Client::on_cCancelBtn_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();
}

4. 主界面的控制

4.1 类、变量和函数声明

//widget.h

class Server;

protected:
    void hasPendingFile(QString usrname, QString sevaddr, QString clntaddr, QString filename);

private:
    QString fileName;
    Server *srv;

private slot:
    void getFileName(QString);

4.2 创建服务器对象

#include "server.h"
#include "client.h"
#include <QFileDialog>

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

    uName = usrname;
    udpSocket = new QUdpSocket(this);
    port = 23232;
    udpSocket->bind(port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams()));
    sndMsg(UsrEnter);

    srv = new Server(this);
    connect(srv, SIGNAL(sndFileName(QString)), this, SLOT(getFileName(QString)));
    
}
void Widget::getFileName(QString name)
{
    fileName = name;
    sndMsg(FileName);
}

4.3 主界面按钮的槽函数

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 处更改:

    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() 

        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();

            if(ipAddr == srvAddr)
            {
                srv->refused();
            }
            break;
        }

hasPendingFile()

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 {
            sndMsg(Refuse, srvaddr);
        }
    }
}
void Client::setFileName(QString name)
{
    locFile = new QFile(name);
}
void Client::setHostAddr(QHostAddress addr)
{
    hostAddr = addr;
    newConn();
}

5. 文件传输测试

五、附件功能

1. 更改字体、字号和颜色

1.1 更改字体

void Widget::on_fontCbx_currentFontChanged(const QFont &f)
{
    ui->msgTxtEdit->setCurrentFont(f);
    ui->msgTxtEdit->setFocus();
}

1.2 更改字体大小

void Widget::on_sizeCbx_currentIndexChanged(const QString &arg1)
{
    ui->msgTxtEdit->setFontPointSize(arg1.toDouble());
    ui->msgTxtEdit->setFocus();
}

1.3 设置字体加粗、倾斜和下划线

void Widget::on_boldTBtn_clicked(bool checked)
{
    if(checked)
        ui->msgTxtEdit->setFontWeight(QFont::Bold);
    else
        ui->msgTxtEdit->setFontWeight(QFont::Normal);
    ui->msgTxtEdit->setFocus();
}

void Widget::on_italicTBtn_clicked(bool checked)
{
    ui->msgTxtEdit->setFontItalic(checked);
    ui->msgTxtEdit->setFocus();
}

void Widget::on_underlineTBtn_clicked(bool checked)
{
    ui->msgTxtEdit->setFontUnderline(checked);
    ui->msgTxtEdit->setFocus();
}

1.4 设置文本颜色

void Widget::on_colorTBtn_clicked()
{
    color = QColorDialog::getColor(color,this);
    if(color.isValid()){
        ui->msgTxtEdit->setTextColor(color);
        ui->msgTxtEdit->setFocus();
    }
}

构造函数的私有变量,以及头文件:

#include <QColorDialog>

private:
    QColor color;

字体大小有点问题,要查看一下什么问题。

2. 字体切换

//widget.h

#include <QTextCharFormat>

private slot:
    void curFmtChanged(const QTextCharFormat &fmt);

构造函数中:

connect(ui->msgTxtEdit, SIGNAL(currentCharFormatChanged(QTextCharFormat)),this, SLOT(curFmtChanged(const QTextCharFormat)));
void Widget::curFmtChanged(const QTextCharFormat &fmt)
{
    ui->fontCbx->setCurrentFont(fmt.font());

    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();
}

3. 保存和清除聊天记录

3.1 保存记录

//widget.h

protected:
    bool saveFile(const QString& filename);
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);
    }
}
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;
}

 

3.2 清除记录

void Widget::on_clearTBtn_clicked()
{
    ui->msgBrowser->clear();
}
void Widget::on_exitBtn_clicked()
{
    close();
}
protected:
    void closeEvent(QCloseEvent *);
void Widget::closeEvent(QCloseEvent *e)
{
    sndMsg(UsrLeft);
    QWidget::closeEvent(e);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值