一、核心界面演示
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 Brower | msgBrower |
---|---|---|
① | Text Brower | msgBower |
② | Text Edit | msgTxtEdit |
③ | Table Widget | usrTblWidget |
④ | Font Combo Box | fontCbx |
⑤ | comboBox | sizeCbx |
⑥ | pushButton | sendBtn |
⑦ | label | usrNumLbl |
⑧ | pushButton | exitBtn |
![]() | Tool Button | boldTBtn |
![]() | Tool Button | italicTBtn |
![]() | Tool Button | underlineTBtn |
![]() | Tool Button | colorTBtn |
![]() | Tool Button | sendTBtn |
![]() | Tool Button | saveTBtn |
![]() | Tool Button | clearTBtn |
<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广播
消息类型 | 用途 |
---|---|
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。
序号 | 类型 | objectName 属性 |
---|---|---|
① | Label | label_2 |
② | Push Button | sOpenBtn |
③ | Push Button | sSendBtn |
④ | Progress Bar | progressBar |
⑤ | Label | sStatusLbl |
⑥ | Push Button | sCloseBtn |
①的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 属性 |
---|---|---|
① | Label | label |
② | Progress Bar | progressBar |
③ | Label | cStatusLbl |
④ | Push Button | cCancelBtn |
⑤ | Push Button | cCloseBtn |
将界面的 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);
}