一、前言
微信作为通信交流工具已经成为生活中不可或缺的一环,通过微信可以完成网络中随时随地的聊天,文件传输,朋友圈等功能,本文将通过Qt实现仿真微信程序,包含登录,好友列表,聊天,文件传输等功能。
二、界面设计
程序由登录,聊天,文件传输,文件接收四个界面构成,通过调用不同界面的不同模块完成相应的功能
1.登录界面
通过设计label空间并填入背景图片,通过LineEdit接收输入的账户密码,PushButton调用登录功能
2.聊天界面
聊天界面左侧是好友列表框,记录已上线的用户,右侧是聊天记录框,记录发送的消息,输入框喝发送按钮如下
3.文件传输界面
文件传输界面由文件选择,发送,进度条,大小和停止组成,分别实现不同的功能
4.文件接收模块
文件接收模块由大小显示,进度条和停止按钮组成
三、模块功能设计和具体代码
1.登录功能
运行程序首先出现的是登录界面,通过读取数据库存储的用户信息和密码进行读取判定,当数据库中不存在对应用户时,提示无用户,用户名和密码不匹配时提示密码错误。
首先进行数据库初始化,通过设置数据库名和密码进行数据库打开,并通过按钮信号调用登录验证功能。
void LoginDialog::Init()
{
db = QSqlDatabase::addDatabase("QMYSQL");
db.setDatabaseName("test");
db.setHostName("localhost");
db.setUserName("root");
db.setPassword("root");
QSqlQuery query(db);
db.open();
}
登录验证功能首先接收LineEdit中的用户名进行SQL筛选查询对应用户名的密码,同时通过exec(),next()导航到有效记录,使用next()的原因是因为exec()执行SQL查询后,查询将放置在无效记录上,并且必须先导航到有效记录上才能检索数据值,因此调用next()将检索结果中的下一条记录,并将查询定位在检索到的记录上。
void LoginDialog::showWeChatWindow()
{
QSqlQuery query;
QString name = ui->usrLineEdit->text();
QString sel1 = QString("select pwd from user where name = '%1'").arg(name);
query.exec(sel1);//查询数据库对应用用户名的密码
if(query.next())
{
QString pwd1 = query.value(0).toString();
QString pwd2 = ui->pwdLineEdit->text();
if(pwd2 == pwd1)
{
//密码正确
weChatWindow = new MainWindow(this);
weChatWindow->setWindowTitle(ui->usrLineEdit->text());
// this->close();
weChatWindow->show();//显示聊天窗口
}
else
{
//密码错误
QMessageBox::warning(this,"提示","密码错误,请重新输入");
ui->pwdLineEdit->clear();
ui->pwdLineEdit->setFocus();
}
}
else
{
//未查询到用户
QMessageBox::warning(this,"提示","此用户不存在!请重新输入");
ui->usrLineEdit->clear();
ui->pwdLineEdit->clear();
ui->pwdLineEdit->setFocus();
}
}
2.聊天界面
聊天界面主要功能是用户上线和消息发送,以及跳转文件传输界面的按钮。聊天功能主要是由发送UDP广播实现的,
(1)上线功能
由于登录验证时设置用户名为窗体标题,因此在用户点击search按钮时候,将聊天头设置为用户名,同时调用信息接收模块sendChatMsg(),此模块优先通过设置的getLocHost()方法获取IP地址,getLocChatMsg()方法则是获取聊天框输入的数据,通过switch调用不同的模块方法,上线功能则是将获取的IP地址写入文件流,并发送UDP信号,激活对应的信号槽函数,函数执行后,调用上线函数onLine(),进行新用户上线显示
case OnLine:
write<<locHostIp;
break;
case OnLine: //新用户上线
read>>name>>hostip; //获取用户名和IP地址信息
onLine(name,curtime);
break;
void MainWindow::onLine(QString name, QString time)
{
bool notExist = ui->userListTableWidget->findItems(name,Qt::MatchExactly).isEmpty();
// 通过判断用户名是否在用户列表中确定是否上线
if(notExist)
{
//没有就在左边用户列表添加用户网名
QTableWidgetItem *newuser = new QTableWidgetItem(name);
ui->userListTableWidget->insertRow(0);
ui->userListTableWidget->setItem(0,0,newuser);
//在信息浏览器中显示该用户上线的提示信息
ui->chatTextBrowser->setTextColor(Qt::gray);
ui->chatTextBrowser->setCurrentFont(QFont("Times New Roman",12));
ui->chatTextBrowser->append(tr(u8"%1 %2 上线!").arg(time).arg(name));
sendChatMsg(OnLine);
}
}
(2)下线功能
下线功能重写closeEvent()函数,此函数用于窗体的关闭,在closeEvent()中调用sendChatMsg()传输OffLine参数,通过参数判断调用offLine()方法,完成用户列表的清除和离线信息的显示
void MainWindow::offLine(QString name, QString time)
{
int row = ui->userListTableWidget->findItems(name,Qt::MatchExactly).first()->row();
ui->userListTableWidget->removeRow(row);
ui->chatTextBrowser->setTextColor(Qt::gray);
ui->chatTextBrowser->setCurrentFont(QFont("Times New roman",12));
QString msg = QString("%1 %2 离线!").arg(time).arg(name);
ui->chatTextBrowser->append(msg);
}
void MainWindow::closeEvent(QCloseEvent *event)
{
sendChatMsg(OffLine);
}
case OffLine: //用户离线
read>>name; //获取其中的用户名
offLine(name,curtime);
break;
(3)信息发送功能
信息发送功能由信息发送输入框和发送按钮组成,输入框用来接收发送的信息,消息的接收通过getLocChatMsg()方法读取,并传输ChatMsg参数进行聊天信息和时间的显示
QString MainWindow::getLocChatMsg()
{
QString chatmsg = ui->chatTextEdit->toPlainText();
ui->chatTextEdit->clear();
ui->chatTextEdit->setFocus();
return chatmsg;
}
QByteArray qba;
qba.resize(myUdpSocket->pendingDatagramSize()); //获取当前可供读取的UDP数据报大小
myUdpSocket->readDatagram(qba.data(),qba.size());
QDataStream read(&qba,QIODevice::ReadOnly);
int msgType;
read>>msgType; //获取消息类型
QString name,hostip,chatmsg,rname,fname;
QString curtime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
switch (msgType)
{
case ChatMsg: //普通聊天消息
{
read>>name>>hostip>>chatmsg; //获取用户名,主机IP和聊天内容信息
ui->chatTextBrowser->setTextColor(Qt::darkGreen);
ui->chatTextBrowser->setCurrentFont(QFont("Times New Roman",14));
ui->chatTextBrowser->append("【"+ name +"】"+curtime);
ui->chatTextBrowser->append(chatmsg);
break;
}
3.文件发送界面
文件的传输采用TCP传输来实现,以C/S方式运行,当传输文件时,不同进程的客户端分别扮演Server和Client,当发送文件时首先通过UDP报文传送即将发出的文件名,当客户端拒收时,通过UDP返回拒收消息,当同意时,建立一个TCP连接向客户端传输文件,传送文件需要在聊天界面选择要发送的用户,并单击文件传输图标(如未选中好友会有对应提示)此时会弹出文件发送界面,由打开文件按钮,发送按钮,停止按钮,进度条组成。
(1)初始化界面
建立信号槽连接,设置端口号,重置信号条等工作
void FileSrvDlg::Init()
{
myTcpSrv = new QTcpServer(this);
mySrvPort = 1096;
connect(myTcpSrv,SIGNAL(newConnection()),this,SLOT(sndChatMsg()));
myTcpSrv->close();
myTotalBytes = 0;
mySendBytes = 0;
myBytesTobeSend = 0;
myPayloadSize = 64*1024;
ui->sendProgressBar->reset();
ui->openFilePushButton->setEnabled(true);
ui->sendFilePushButton->setEnabled(false);
}
(2)文件选择
界面出现后点击文件选择,通过QFileDialog()类中的getOpenFileName()方法操作文件,同时获取文件名(此时有多种方法,例如split()切片文件路径或者),本文介绍另一种方法,组合lastIndexOf()方法和QString类中right()方法,lastIndexOf()方法从后往前筛选参数的值并返回索引值,通过此方法可以找到最后一个“/”的索引位置,此时用总长度减去索引值再减一(索引值从零开始),就可以获取文件名的长度,right()方法可以获取字符串右边的n个字符,组合这三种方法可以成功获取到文件名。
void FileSrvDlg::on_openFilePushButton_clicked()
{
myPathFile = QFileDialog::getOpenFileName(this);
if(!myPathFile.isEmpty())
{
myFileName = myPathFile.right(myPathFile.size() - myPathFile.lastIndexOf('/') -1);
ui->sfileNameLineEdit->setText(tr("%1").arg(myFileName));
ui->sendFilePushButton->setEnabled(true);
ui->openFilePushButton->setEnabled(false);
}
}
(3)文件发送
当点击发送按钮时,通过listen侦听所有TCP端口,当返回失败的时候发送警告,当有客户端和它进行连接时候,服务器会触发newConnection信号,同时客户端如果连接成功,客户端会触发connected信号,表示成功和服务器连接,同时将文件名通过信号进行传送。
void FileSrvDlg::on_sendFilePushButton_clicked()
{
if(!myTcpSrv->listen(QHostAddress::Any,mySrvPort)) // 开始监听
{
QMessageBox::warning(0,QObject::tr("异常"),"打开TCP端口出错,请检查网络连接!");
close();
return;
}
emit sendFileName(myFileName);
}
触发信号槽跳转到sndChatMsg()函数执行
此函数将绑定一个新的连接,并将下一个挂起的连接作为连接的 QTcpSocket 对象返回,此时通过发送bytesWritten()信号,此信号的发送条件是每次将数据有效载荷写入设备的当前写入通道时,都会发出此信号。bytes 参数设置为此负载中写入的字节数,并绑定进度条刷新槽函数,以进行进度条的刷新,并将数据通过文件流形式进行发送
void FileSrvDlg::sndChatMsg()
{
ui->sendFilePushButton->setEnabled(false);
mySrvSocket = myTcpSrv->nextPendingConnection();
connect(mySrvSocket,SIGNAL(bytesWritten(qint64)),this,SLOT(refreshProgress(qint64)));
myLocPatFile = new QFile(myPathFile);
//以只读方式打开选中的文件
myLocPatFile->open(QFile::ReadOnly);
//通过QFile类的size()函数获取待发送文件的大小,
myTotalBytes = myLocPatFile->size();
//将发送缓存放装在QDataStream类型变量中,可以通过"<<"操作符填写文件头
QDataStream sendOut(&myOutputBlock,QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_5_11);
//启动计时
mytime.start();
//通过right()函数去掉文件名的路径部分
QString curFile = myPathFile.right(myPathFile.size()-myPathFile.lastIndexOf('/')-1);
//构造临时文件头,将值追加到myTotalBytes字段,完成实际需发送字节数记录
sendOut<<qint64(0)<<qint64(0)<<curFile;
myTotalBytes +=myOutputBlock.size();
//读写操作定位到从文件头开始
sendOut.device()->seek(0);
//填写实际的总长度和文件长度
sendOut<<myTotalBytes<<qint64((myOutputBlock.size()-sizeof(qint64)*2));
//将该文件头发出,同时修改待发送字节数myBytesTobeSend
myBytesTobeSend = myTotalBytes - mySrvSocket->write(myOutputBlock);
//清空发送缓存以备下次使用
myOutputBlock.resize(0);
}
(4)刷新进度条
processEvents()保证界面不会被冻结,作用是根据指定的标志处理调用线程的所有挂起事件,直到没有更多事件要处理。使用qApp全局指针,并填写已发送和进度条,同时在发送成功时提示文件传输完成。
void FileSrvDlg::refreshProgress(qint64 bynum)
{
//用于传输大文件时界面不会冻结
qApp->processEvents();
mySendBytes +=(int)bynum;
if(myBytesTobeSend > 0)
{
myOutputBlock = myLocPatFile->read(qMin(myBytesTobeSend,myPayloadSize));
myBytesTobeSend -= (int)mySrvSocket->write(myOutputBlock);
myOutputBlock.resize(0);
}
else
{
myLocPatFile->close();
}
ui->sendProgressBar->setMaximum(myTotalBytes);
ui->sendProgressBar->setValue(mySendBytes);
//填写文件总大小栏
ui->sfileSizeLineEdit->setText(tr("%1").arg(myTotalBytes/(1024*1024))+"MB");
//填写已发送栏
ui->sendSizeLineEdit->setText(tr("%1").arg(mySendBytes/(1024*1024))+"MB");
if(mySendBytes == myTotalBytes)
{
myLocPatFile->close();
myTcpSrv->close();
QMessageBox::information(0,QObject::tr("完毕"),"文件传输完成!");
}
}
4.文件接收界面
(1)初始化
端口号,并通过readyRead()函数绑定信号槽
void FileCntDlg::Init()
{
myCntSocket = new QTcpSocket(this);
mySrvPort = 1096;
connect(myCntSocket,SIGNAL(readyRead()),this,SLOT(readChatMsg()));
myFileNameSize= 0;
myTotalBytes = 0;
myRcvedBytes = 0;
}
(2)数据写入
判断等待读取的传入字节数是否小于等于数据在内存中占用的存储空间,并执行对应方法
当文件名不存在并且等待读取的传入字节数大小大于等于qint64*2时,获取其中的文件总大小和文件名大小,并将已接收数据字节数置为qint64*2
当文件名存在并且传输的数据大于等于文件名大小时,将文件名取出数据流并将已传输数据大小增加文件名大小,通过open()方法设定保存文件的路径
将数据写入设备,同时计算传输速率并进行进度条显示
void FileCntDlg::readChatMsg()
{
QDataStream in(myCntSocket);
in.setVersion(QDataStream::Qt_5_11);
float usedTime = mytime.elapsed();
if(myRcvedBytes <= sizeof(qint64)*2)
{
if((myCntSocket->bytesAvailable()>=sizeof(qint64)*2)&&(myFileNameSize ==0))
{
in>>myTotalBytes>>myFileNameSize;
myRcvedBytes += sizeof(qint64)*2;
}
if((myCntSocket->bytesAvailable()>=myFileNameSize)&&(myFileNameSize !=0))
{
in >>myFileName;
myRcvedBytes +=myFileNameSize;
myLocPathFile->open(QFile::WriteOnly);
ui->rfileNameLineEdit->setText(myFileName);
}
else
{
return;
}
}
if(myRcvedBytes < myTotalBytes)
{
//从数据中向设备写入
myRcvedBytes += myCntSocket->bytesAvailable();
myInputBlock = myCntSocket->readAll();
myLocPathFile->write(myInputBlock);
myInputBlock.resize(0);
}
ui->recvProgressBar->setMaximum(myTotalBytes);
ui->recvProgressBar->setValue(myRcvedBytes);
double transpeed = myRcvedBytes / usedTime;
//填写文件大小栏
ui->rfileSizeLineEdit->setText(tr("%1").arg(myTotalBytes/(1024*1024))+"MB");
//填写已接收栏
ui->recvSizeLineEdit->setText(tr("%1").arg(myRcvedBytes/(1024*1024))+"MB");
//计算并显示传输速率
ui->rateLabel->setText(tr("%1").arg(transpeed*1000/(1024*1024),0,'f',2)+"MB/秒");
if(myRcvedBytes == myTotalBytes)
{
myLocPathFile->close();
myCntSocket->close();
ui->rateLabel->setText("接收完毕!");
}
}
(3)拒绝接收功能
点击拒绝接收时,调用close()方法并提示对方拒绝接收
void FileSrvDlg::cntRefused()
{
myTcpSrv->close();
QMessageBox::warning(0,QObject::tr("提示"),"对方拒绝接收!");
}
四、总结
微信仿真通过UDP和TCP两种网络传输方式,分别使用聊天信息传输和文件信息的传输,是两种协议的不同运用,后续的改善可以通过MVC架构,通过建设Server进行信息的接收,通过接收的信息调用不同的方法,并通过Server转发处理后的数据,做到客户端只进行信号发送和数据接收,而在服务器端进行信息的处理。