PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛”
前沿
新的一年又是一个崭新的开始,在这里要对大家说新年快乐~
相信有很多女性程序员在工作之余还会巴拉巴拉的讨论些属于两个人甚至是几个人的小秘密了,又不想让人知道,但是在工作期间按捺不住激动的心情,所以,所以,我想拥有属于我们几个人特有的聊天室,这里可以畅所欲言,没事拉拉家常,咬咬耳朵,好不自在呀!
模仿微信聊天来建立属于我们的小秘密吧~
功能实现
登录
为了更好的体验秘密性以及安全性,这是同样需要登录窗口,只有匹配上了暗号才可以加入我们的秘密聊天哟~
因为是秘密聊天,参与的人员肯定不会多,这里对用户名以及登录密码的存储采用了xml的方式。后续为了保密起见,可以对文件进行加密处理哦
该窗口的父类是QDialog,只有登录成功之后才会跳转到聊天页面,否则会提示用户输入不匹配错误。
xml的文件格式:
<?xml version='1.0' encoding='UTF-8'?>
<UserAccount>
<user id="1">
<name>小明</name>
<password>123</password>
</user>
<user id="2">
<name>王二</name>
<password>123</password>
</user>
<user id="3">
<name>小丽</name>
<password>123</password>
</user>
</UserAccount>
依据上述格式,Qt读取xml文件内容方法如下:
//获取当前运行exe的路径
QString qExePath = QDir::currentPath();
//对获取的路径进行转换,将"/"转成"\\"
qExePath = QDir::toNativeSeparators(qExePath)
QString qsXMLFile = qExePath + "\\userlog.xml";
QFile file(qsXMLFile); //创建XML文件文件
QDomDocument mydoc; //获取XML中的DOM对象
//将XML对象赋给QdomDucument类型的Qt文档句柄
bool bOk = mydoc.setContent(&file, true);
if (bOk == false)
{
return;
}
//获取xml文档的DOM根元素
QDomElement root = mydoc.documentElement();
if (root.hasChildNodes())
{
QDomNodeList userList = root.childNodes();
for (int i = 0; i < userList.count(); i++)
{
//根据当前索引 i 获取用户节点元素
QDomNode user = userList.at(i);
QDomNodeList record = user.childNodes();
//解析:用户名
QString qsUserName = record.at(0).toElement().text();
//解析:密码
QString qsPassword = record.at(1).toElement().text();
//数据存储
st_LoginData stInfo;
stInfo.qsUserName = qsUserName;
stInfo.qsPassword = qsPassword;
m_vetLoginData.push_back(stInfo);
}
}
对于获取的用户名密码信息,此时,全部存放到一个叫做m_vetLoginData的容器中。当我们输入用户名和密码想要登录时,就会从该容器中逐一匹配,直到匹配到合适的数据,才会跳出页面,进入聊天页面。
那么,该如何实现呢?
QString qsUserName = ui.editUserName->text();
QString qsPassword = ui.editPassword->text();
//对比vector数据中是否匹配内容
int nIndex = -1;
for (int i = 0; i < m_vetLoginData.size(); i++)
{
st_LoginData stInfo = m_vetLoginData[i];
if ((stInfo.qsUserName == qsUserName) && (stInfo.qsPassword == qsPassword))
{
nIndex = i;
m_stInfo = stInfo;
break;
}
}
if (nIndex == -1)
{
//未匹配成功
QMessageBox::warning(0, QStringLiteral("提示"), QStringLiteral("此用户不存在,请重新输入!"), QMessageBox::Yes);
return;
}
done(Accepted); //确定
采用了最简单的对比方式,相信大家一目了然了,让我需要说明的是:done(Accepted);这个方法
这是QDialog特有的方法,因为QDialog是阻塞窗口哦,dlg.exec()的方式,只有获取到是true或者是false时,才会进行下一步处理,在该窗口中,使用Accepted代表了确定,Rejected代表了取消
实现了上述功能之后,那么登录窗口要在哪里显示比较合适呢?
只有登录成功之后才会展示聊天窗口,关闭时直接退出程序。
对于这个功能,我刚开始写的时候是放到了主窗口的show函数中,对show函数进行了重写,如果要是顺利登陆没有问题,但是当我关闭登录页面时,发现了很严重的问题,程序居然无法正常退出!
我的天,尝试了各种方式也不行,我怀疑是主窗口没有show出来就关闭,有些功能是无法进行的,让程序无法走到正常流程中,即使我在show()函数中,exit()程序也是不可以的。
好吧,好吧,我们换一个新思路~
在main.cpp中首先展示登录窗口,只有登录成功之后再弹出聊天窗口,退出时,直接关闭程序。尝试以后发现,不错,确实好用~
在这里,我要分享给大家,操作简单,但是方便实用。
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QString qExePath = QCoreApplication::applicationDirPath();
QDir::setCurrent(qExePath);
QtLogin dlg;
int nResult = dlg.exec();
st_LoginData stLogin = dlg.GetLoginSuccessfulData();
if (nResult == QDialog::Accepted) //点击了确定按钮
{
QtChat w;
w.SetLoginUserData(stLogin);
w.show();
return a.exec();
}
return 1;
}
聊天
到了我们最备受瞩目的地方啦!
上班使用微信聊天,总是巴拉巴拉的一直说话,属实不太好,随着过年的脚步逐渐到来,工作的浮躁心态也越来越明显了,这时候体现了我们程序员的好处了,美其名曰的测试,其实可以偷偷地说一些悄悄话,很是美滋滋~
这里采用了Udp方式进行消息收发以及处理。
UDP主框架搭建
在这个聊天室中,每个用户的地位都是“平等”的,每个用户的聊天窗口进程可以叫做一个端点(Peer),既可以作为服务器,也可以作为客户端,因此,可以将它看成端到端的系统:P2P
图形粗糙,凑合着看吧,咱也没有这绘图天分,能看就行~
根据应用的需求,在我设计的聊天室中分成了三种广播消息
| 消息类型 | 功能 |
|---|---|
| UdpMsgType_Chat | 聊天内容 |
| UdpMsgType_Online | 用户上线 |
| UdpMsgType_Offline | 用户下线 |
这里采用了Qt中特有的UDP消息处理类:QUdpSocket
接收UDP消息处理
初始化UDP套接字
m_UdpSocket = new QUdpSocket();
m_nUdpPort = 11336;
m_UdpSocket->bind(m_nUdpPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
定义接收UDP槽函数
connect(m_UdpSocket, &QUdpSocket::readyRead, this, &QtChat::MsgReceivedRecvUdpChartMsg);
那么,对于其他UDP消息来之后,该如何处理呢?
思路
第一步:触发了MsgReceivedRecvUdpChartMsg响应函数后,需要判断是否有可供读取的数据,调用QUdpSocket::hasPendingDatagrams()只有存在有效数据后,才需要进行处理。
第二步:获取当前可供读取的UDP数据报的大小
第三步:读取传入的UDP消息类型,是聊天?上线?离线?根据不同的消息类型,做出不同的处理。
接下来,看具体是如何实现的吧~
while (m_UdpSocket->hasPendingDatagrams()) //这是第一步
{
QByteArray qba;
qba.resize(m_UdpSocket->pendingDatagramSize()); //这是第二步
m_UdpSocket->readDatagram(qba.data(), qba.size());
QDataStream read(&qba, QIODevice::ReadOnly);
int nMsgType;
read >> nMsgType; //这是第三步
QString qsName, qsHostIP, qsChatMsg, qsRName, qsFName;
QString qsCurtime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
switch (nMsgType)
{
case UdpMsgType_Chat:
{
//当前是普通的聊天,仅展示用户名、主机IP、和聊天内容信息,并进行展示
read >> qsName >> qsHostIP >> qsChatMsg;
ui.widgetContent->SetChatContent(qsName, qsCurtime, qsChatMsg);
}
break;
case UdpMsgType_Online:
{
//当前是新用户上线,获取用户名和IP地址信息,使用OnLine对新用户登录进行处理
read >> qsName >> qsHostIP;
this->OnLine(qsName, qsCurtime);
}
break;
case UdpMsgType_Offline:
{
read >> qsName;
this->OffLine(qsName, qsCurtime);
}
break;
default:break;
}
}
讲到了接收UDP消息,那么接下来说一说是如何发送UDP消息的吧~
发送UDP消息
程序中定义了一个叫做SendChartMsg函数用来做不同类型的UDP消息发送
枚举类型:UdpMsgType_Chat
只是发送普通的聊天文本,向要发送的数据中写入本机端的IP地址,以及用户输入的聊天内容
枚举类型:UdpMsgType_Online
对于新上线的用户,只是简单地写入IP地址,通知接收UDP的地方,该用户只是上线了。
枚举类型:UdpMsgType_Offline
用户离线,因为用户已经离线,所以,是不需要做任何特殊处理的
根据不同类型需要发送的数据已经组装好了,该如何发送给各个UDP接收者呢?
QUdpSocket::writeDatagram()的方式,完成对消息的处理,从而将消息广播出去
具体实现:
QByteArray qba;
QDataStream write(&qba, QIODevice::WriteOnly);
QString qsLocHostIp = GetLocalHostIp();
QString qsChartMsg = GetLocalChatMsg();
//向要发送的数据中写入消息类型以及用户信息
write << enumMsg << m_stUser.qsUserName;
switch (enumMsg)
{
case UdpMsgType_Chat:
{
//向要发送的数据中写入本机端的IP地址和用户输入的聊天信息文本
write << qsLocHostIp << qsChartMsg;
}
break;
case UdpMsgType_Offline:
//只要用户不离线,不需要进行任何操作
break;
case UdpMsgType_Online:
{
//对于新用户上线,只是简单地向数据中写入IP地址
write << qsLocHostIp;
}
break;
}
//完成对消息的处理后,使用套接口的writeDatagram()函数广播出去
m_UdpSocket->writeDatagram(qba, qba.length(), QHostAddress::Broadcast, m_nUdpPort);
主要的发送接收数据核心已经完成了,接下来到了我们的装修环节了,赶紧给我们的程序刷上白漆,铺上瓷砖,迫不及待的想要使用了。
聊天通讯过程函数处理
首当其冲的就是页面了,先把页面布置完成后,我们才能安排内容展示了
| 控件 | 描述 |
|---|---|
| 上线用户 | 使用QTableWidget来显示已经上线的所有用户名信息 |
| 聊天内容 | 采用QScrollArea控件展示所有的聊天内容 |
| 发送的聊天内容 | 使用QTextEdit输入聊天内容 |
第一个需要装修的就是上线处理了,UDP用户来了,该如何在界面上展示有人来了呢?
上线:UdpMsgType_Online
思路:当有一个新用户名上线后,判断左侧列表中是否已经存储过,如果未存储就记录,并在窗口中提示,该用户上线了~
bool bNotExist = ui.tableWidget->findItems(qsName, Qt::MatchExactly).isEmpty();
if (bNotExist == true)
{
QTableWidgetItem *newUser = new QTableWidgetItem(qsName);
ui.tableWidget->insertRow(0);
ui.tableWidget->setItem(0, 0, newUser);
ui.tableWidget->setRowHeight(0, 40);
ui.widgetContent->SetOnLine(qsName, qsTime);
this->SendChartMsg(UdpMsgType_Online);
}
看到这里,大家会发现,在存储数据之后,又调用了一次SendChatMsg函数,为什么会再次调用呢?
解答:这是因为已经在线的各个用户的也需要被告知新上线的用户端他们自己的信息,如果不设置的话,当新用户上线后,在新用户的QTableWidget控件中是看不到其他已经在线的用户的。
离线:UdpMsgType_Offline
这个功能更好操作了,查询到需要被离线的用户时,直接从QTableWidget剔除掉就可以了
获取本机IP
该功能是在发送UDP消息时用到的
//获取本机IP地址
QList<QHostAddress> addrlist = QNetworkInterface::allAddresses();
foreach(QHostAddress addr, addrlist)
{
if (addr.protocol() == QAbstractSocket::IPv4Protocol)
return addr.toString();
}
return "";
获取输入的发送文本
该功能是在发送UDP消息时用到的
//获取本地用户输入的聊天消息
QString qsChatMsg = ui.editSendContent->toHtml();
ui.editSendContent->clear();
ui.editSendContent->setFocus();
return qsChatMsg;
一旦获取了发送内容之后,需要将获取的内容从QTextEdit控件上清除,为了方便下一次输入文本。
回车发送聊天内容
界面上设置了一个"发送"按钮,但是大多数情况下我们已经习惯使用回车键发送数据了,为了方便使用,我们也做了这样的操作
第一步:监视QTextEdit控件
ui.editSendContent->installEventFilter(this);
第二步:筛选出回车消息,并处理
virtual bool eventFilter(QObject * watched, QEvent * event);//重载过滤器--响应标签点击消息
bool QtChat::eventFilter(QObject * watched, QEvent * event)
{
if ((watched == ui.editSendContent) && (event->type() == QEvent::KeyPress))
{
//QTextEdit回车发送消息
QKeyEvent *e = static_cast <QKeyEvent *> (event);
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return)
{
this->SendChartMsg(UdpMsgType_Chat);
return true;
}
}
return QWidget::eventFilter(watched, event);
}
装修
到了我们的装修环节了,邻近过年了,为了喜庆,采用了红红火火的背景图,我这个人还是喜欢热闹的哈~
1:设置窗口背景
QPixmap m_backPixmap; //背景图
m_backPixmap.load(":/QtChat/image/back2.jpg");
void QtChat::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
if (!m_backPixmap.isNull())
{
painter.drawPixmap(0, 0, this->width(), this->height(), m_backPixmap);
}
}
2:对QTableWidget风格设置
采用默认的控件背景难免有些丑陋,实在是不符合我这个粉粉嫩嫩的少女心,我的程序我做主,必须设置成粉色的,带有70%的透明度,既能看到窗口背景,又不影响文本显示,完美~
QString qsStyleTableWidget = "QTableWidget{font-size:18px; font-family:Microsoft YaHei UI;text-align:left;background-color:rgba(255, 182, 193, 70);border:none;}""QTableWidget::item:selected{ color:#FFFFFF; background:#108ee9;}";
ui.tableWidget->setStyleSheet(qsStyleTableWidget);
3:QTextEdit风格设置
ui.editSendContent->installEventFilter(this);
QString qsStyleEditSendContent = "QTextEdit{color:#333333; font-family:Microsoft YaHei UI; font-size:16px;border:2px solid #CCCCCC ;border-radius: 6px;} QLineEdit{background-color:transparent}";
ui.editSendContent->setStyleSheet(qsStyleEditSendContent);
QPalette plEdit = ui.editSendContent->palette();
plEdit.setBrush(QPalette::Base, QBrush(QColor(255, 182, 193, 70)));
ui.editSendContent->setPalette(plEdit);
4:QScrollArea展示所有聊天内容
在QScrollArea中自带了一个QWidget控件,这里提升了QWidget控件,用于展示所有的聊天内容。为了简单起见,所有文件都采用了QLabel进行展示
分成了三个功能:上线、离线以及聊天内容展示
4.1:上线、离线
某一个UDP用户上线或者离线后,需要在页面上提示用户名以及相应时间
//获取上一个存储的控件的位置
int nTop = 0;
if (m_vetLabel.size() != 0)
{
QLabel *labLast = m_vetLabel[m_vetLabel.size() - 1];
nTop = labLast->geometry().bottom() + 5;
}
//插入新的内容
QString qsContent = qsTime + QStringLiteral(" 【") + qsName + QStringLiteral("】 上线!");
QString qsStyleTitle = "QLabel{color:#0000FF; font-family:Microsoft YaHei UI; font-size:14px;} QLabel{background-color: transparent}";
QLabel *labTitle = new QLabel(this);
labTitle->setGeometry(10, nTop, 300, 30);
labTitle->setText(qsContent);
labTitle->setStyleSheet(qsStyleTitle);
labTitle->show();
m_vetLabel.push_back(labTitle);
int nTotalHeight = nTop + 30 + 5;
setFixedSize(QSize(610, nTotalHeight));
在每次添加新的内容时,都需要从容器存储的最后一个数据位置上拼接,保证了所有的数据都是追加展示的。
4.2:聊天内容显示
基本上与上线、离线功能显示方式大同小异。
需要展示:发送人、接收时间以及详细的聊天内容
最需要说明的是:如何显示文本过多的聊天内容,并且自动换行展示。
核心来喽~
labContent->setFixedWidth(300);
labContent->setText(qsChatContent);
labContent->setWordWrap(true);
labContent->adjustSize();
int nHeight = labContent->height();
labContent->setGeometry(10, nTop, 300, nHeight);
好了姐妹们,我们的秘密聊天室功能已经全部实现了~
总结
赶紧尝试下吧,也做一个属于我们自己的聊天工具,偷偷的跟小伙伴聊天~
实现技术很简答,让我们在工作之余也要丰富自己的技术知识吧。
我是中国好公民,专注C++开发程序员!