1. 群成员界面设计:
?
在 signin.cpp ?中的构造函数中 创建按钮
// 1. 设置窗口名字、图标
this->setWindowTitle("QQ群聊器");
this->setWindowIcon(QIcon(":/img/qq_副本.png"));
// 创建两个容器 分别存放网名 图片名
QList<QString> nameList; // 网名列表
nameList << "星光大道" << "朝天阙" << "大路朝天" << "沙生欲夺" << "云上飞宵" << "塞尚不屈" << "罗黄泉"
<< "升星夺命" << "九曲殒命";
QStringList iconNameList; // 图标列表
iconNameList << "1" << "2" << "3" << "4"
<< "5" << "6" << "7" << "8" << "9";
// 2. 设置九个按钮 九个用户
QVector<QToolButton*> vector; // 按钮的容器
// 循环创建9个按钮 贴图、网名、透明化
for(int i = 0; i < 9; i++)
{
// 创建按钮对象
QToolButton* btn = new QToolButton(this);
// 设置图片路径
QString str = QString(":/img/%1.jpeg").arg(iconNameList.at(i));
btn->setIcon(QPixmap(str)); // 给按钮设置图片
btn->setText(nameList[i]);
// 设置图片大小
btn->setIconSize(QSize(80, 70));
// 透明化按钮
btn->setAutoRaise(true);
// 设置图片和按钮网名显示的方式
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
// 将按钮放到 vertricalLayout 布局中去
ui->verticalLayout->addWidget(btn);
// 将按钮存放到容器中去
vector.push_back(btn);
}
?创建一个聊天界面类 ChatWidget
2. ? ? 点击按钮弹出聊天对话框
// 3. 点击按钮弹出聊天窗口
for(int i = 0; i < vector.size(); i++)
{
// 将按钮与聊天窗口关联起来
connect(vector[i], &QToolButton::clicked, [=]()
{
// 判断是否已经打开聊天窗口
if(isShow[i])
{
QMessageBox::warning(this, "警告", "该聊天窗口已打开,无法再次打开");
return; // 退出函数
}
// 将标记是否已经打开聊天窗口 置成true
isShow[i] = true;
// 实例化聊天窗口对象
ChatWidget* widget = new ChatWidget(nullptr, vector[i]->text());
widget->setWindowTitle(vector[i]->text());
widget->setWindowIcon(QIcon(vector.at(i)->icon()));
widget->show(); // 显示窗口
// 解决关闭聊天窗口之后无法再次打开的Bug
// 接收关闭窗口信号
connect(widget, &ChatWidget::closeWidget, [=]()
{
isShow[i] = false; // 将标记置成false
});
});
}
?3. 聊天界面窗口布局
4. 用UDP 收发消息?
-
chatwidget.h 头文件中: -
public:
// 3. 枚举 分别代表 普通信息,用户进入信息,用户离开信息
enum MsgType{MSG, USERENTER, USERLEFT};
// 4. 广播udp 信息 将发送的信息显示到控件 TextBrowser
void sendMsg(MsgType type); // 发送普通信息,用户进入信息,用户下线信息
// 5. 获取名字
// 6. 接收UDP 消息
void ReceivMessage();
// 7. 获取聊天信息 获取编辑控件中的要发送的消息
QString getMsg();
private:
// 用户名 网名
QString myName;
// 端口号
quint16 port;
// 套接字
QUdpSocket * udpSocket;
-
在 chatwidget.cpp 中的构造函数 实例化套接字; 绑定端口; 连接接收信息 -
// 获取用户名(网名)通过构造函数获取
this->myName = name;
// 实例化套接字对象
this->udpSocket = new QUdpSocket(this);
// 设置端口号
this->port = 8989;
// 2.绑定地址与端口号
// 采用 ShareAddress 模式 (允许其他的服务连接到相同的地址和端口)
// 特别是用在多客户端监听同一个服务器端口号等时,特别有效, 和ReuseAddressHint 模式(重新连接服务器)
this->udpSocket->bind(this->port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
// 3. 监听信号
connect(this->udpSocket, &QUdpSocket::readyRead, this, &ChatWidget::ReceivMessage);
// 4. 点击发送按钮 将信息发送出去
connect(ui->sendBtn, &QPushButton::clicked, [=]()
{
sendMsg(MSG);
});
-
广播信息 -
// 4. 广播udp 信息 将发送的信息显示到控件 TextBrowser
void ChatWidget::sendMsg(MsgType type)
{
// QDataStream 是数据流,相当于数据管道,屏蔽了数据转换过程
QByteArray array; // 创建一个数组 装数据
// 创建流 好处是可以分段 参数1:流入地址, 参数2:以只写的方式
QDataStream stream(&array, QIODevice::WriteOnly);
// 流入类型和用户名
stream << type << this->myName;
// 分类 类型消息处理
switch (type)
{
case MSG:
// 判断编辑框输入的消息是否为空
if(ui->msgTextEdit->toPlainText() == "")
{
QMessageBox::warning(this, "警告", "发送的信息不能为空");
return; // 退出函数
}
stream << this->getMsg(); // 流入信息 普通的聊天信息
break;
case USERENTER:
break;
case USERLEFT:
break;
default:
break;
}
// 发送数据报 书写报文 QHostAddress::Broadcast --> 发送给所有人
udpSocket->writeDatagram(array.data(), array.size(), QHostAddress::Broadcast, this->port);
}
-
接收消息 -
// 6. 接收UDP 消息
void ChatWidget::ReceivMessage()
{
// 获取 第一个挂起的UDP 数据报的大小
qint64 size = udpSocket->pendingDatagramSize();
// 将 qint64 转换成 int
int mysize = static_cast<int>(size);
// 定义一个数组用于接收数据流的数据 将元素全部置成0
QByteArray array = QByteArray(mysize, 0); // 大小为mysize 第一个挂起的数据报的大小
/* QUdpSocket::readDatagram 接收数据报
* 接收不大与 maxSize 字节的数据并存储在数据中,发送方的主机地址和端口号存储在 *地址和*端口号中(除非指针为0)
* 成功返回数据报的大小; 否则返回-1
* 如果 maxsize 太小,则数据报的其余部分将丢失。为了避免数据丢失,请在尝试读取挂起的数据报之前
* 调用 penddingDatagramSize() 确定关起的数据报的大小,如果 maxSize 为0, 则数据报将被丢弃
*/
// 接收数据报
udpSocket->readDatagram(array.data(), size);
// 以只读的方式将信息流入到数组中去
QDataStream stream(&array, QIODevice::ReadOnly);
// 声明一个变量 通过 数据流的方式获取 消息类型
int msgtype;
// 流入类型 读取类型
stream >> msgtype;
// 声明 用户名 聊天信息
QString name, msg;
// 格式化时间类型
QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
// 分类处理
switch(msgtype)
{
case MSG: // 普通消息
// 流入 网名和聊天信息
stream >> name >> msg;
// 将信息追加到控件 TextBorwser 中去(显示聊天信息的控件)
ui->msgBrowser->setTextColor(Qt::blue); // 设置显示信息的颜色
ui->msgBrowser->setCurrentFont(QFont("方正粗黑宋简体", 11));
// 追加网名 和 时间
ui->msgBrowser->append("[" + name + "]" + time);
// 追加聊天内容
ui->msgBrowser->append(msg);
break;
case USERENTER: // 用户进入
// 流入用户名
stream >> name;
break;
case USERLEFT: // 用户离开 下线
// 流入用户名
stream >> name;
break;
}
}
-
从编辑框控件中获取输入的信息 -
// 7. 获取聊天信息 获取编辑控件中的要发送的消息
QString ChatWidget::getMsg()
{
// 从编辑控件中获取输入的信息
// toPlainText()【简单文本】 和 toHtml() 的区别是,toHtml()可以设置特殊标点,分段等(富文本)
QString msg = ui->msgTextEdit->toHtml();
// 清空编辑框控件
ui->msgTextEdit->clear();
// 设置光标
ui->msgTextEdit->setFocus(); // 重置编辑框的鼠标光标
// 返回获取到的信息
return msg;
}
-
将用户进入的信息显示到控件中去 -
用户下线消息 -
控件功能实现
?5. 完整代码:
signin.h
#ifndef SIGNIN_H
#define SIGNIN_H
#include <QWidget>
namespace Ui {
class SignIn;
}
class SignIn : public QWidget
{
Q_OBJECT
public:
explicit SignIn(QWidget *parent = 0);
~SignIn();
QVector<bool> isShow; // 标记是否已经打开了聊天窗口
private:
Ui::SignIn *ui;
};
#endif // SIGNIN_H
signin.cpp
#include "signin.h"
#include "ui_signin.h"
#include <QMessageBox>
#include <QToolButton>
#include "chatwidget.h"
SignIn::SignIn(QWidget *parent) :
QWidget(parent),
ui(new Ui::SignIn)
{
ui->setupUi(this);
// 1. 设置窗口名字、图标
this->setWindowTitle("QQ群聊器");
this->setWindowIcon(QIcon(":/img/qq_副本.png"));
// 创建两个容器 分别存放网名 图片名
QList<QString> nameList; // 网名列表
nameList << "星光大道" << "朝天阙" << "大路朝天" << "沙生欲夺" << "云上飞宵" << "塞尚不屈" << "罗黄泉"
<< "升星夺命" << "九曲殒命";
QStringList iconNameList; // 图标列表
iconNameList << "1" << "2" << "3" << "4"
<< "5" << "6" << "7" << "8" << "9";
// 2. 设置九个按钮 九个用户
QVector<QToolButton*> vector; // 按钮的容器
// 循环创建9个按钮 贴图、网名、透明化
for(int i = 0; i < 9; i++)
{
// 创建按钮对象
QToolButton* btn = new QToolButton(this);
// 设置图片路径
QString str = QString(":/img/%1.jpeg").arg(iconNameList.at(i));
btn->setIcon(QPixmap(str)); // 给按钮设置图片
btn->setText(nameList[i]);
// 设置图片大小
btn->setIconSize(QSize(80, 70));
// 透明化按钮
btn->setAutoRaise(true);
// 设置图片和按钮网名显示的方式
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
// 将按钮放到 vertricalLayout 布局中去
ui->verticalLayout->addWidget(btn);
// 将按钮存放到容器中去
vector.push_back(btn);
// 将标记是否打开聊天窗口 置成 false
isShow.push_back(false);
}
// 3. 点击按钮弹出聊天窗口
for(int i = 0; i < vector.size(); i++)
{
// 将按钮与聊天窗口关联起来
connect(vector[i], &QToolButton::clicked, [=]()
{
// 判断是否已经打开聊天窗口
if(isShow[i])
{
QMessageBox::warning(this, "警告", "该聊天窗口已打开,无法再次打开");
return; // 退出函数
}
// 将标记是否已经打开聊天窗口 置成true
isShow[i] = true;
// 实例化聊天窗口对象
ChatWidget* widget = new ChatWidget(nullptr, vector[i]->text());
widget->setWindowTitle(vector[i]->text());
widget->setWindowIcon(QIcon(vector.at(i)->icon()));
widget->show(); // 显示窗口
// 解决关闭聊天窗口之后无法再次打开的Bug
// 接收关闭窗口信号
connect(widget, &ChatWidget::closeWidget, [=]()
{
isShow[i] = false; // 将标记置成false
});
});
}
}
SignIn::~SignIn()
{
delete ui;
}
chatwidget.h
#ifndef CHATWIDGET_H
#define CHATWIDGET_H
#include <QWidget>
#include <QUdpSocket>
namespace Ui {
class ChatWidget;
}
class ChatWidget : public QWidget
{
Q_OBJECT
public:
ChatWidget(QWidget *parent, QString name);
~ChatWidget();
// 1.重写关闭窗口事件
void closeEvent(QCloseEvent *event);
signals:
// 2.自定义关闭窗口
void closeWidget();
public:
// 3. 枚举 分别代表 普通信息,用户进入信息,用户离开信息
enum MsgType{MSG, USERENTER, USERLEFT};
// 4. 广播udp 信息 将发送的信息显示到控件 TextBrowser
void sendMsg(MsgType type); // 发送普通信息,用户进入信息,用户下线信息
// 5. 获取名字
// 6. 接收UDP 消息
void ReceivMessage();
// 7. 获取聊天信息 获取编辑控件中的要发送的消息
QString getMsg();
// 8. 用户进入 参数:用户名(网名)
void userEnter(QString username);
// 9. 用户下线 参数1:用户名,参数2:用户下线的时间
void userLeft(QString username, QString time);
private:
// 用户名 网名
QString myName;
// 端口号
quint16 port;
// 套接字
QUdpSocket * udpSocket;
private:
Ui::ChatWidget *ui;
};
#endif // CHATWIDGET_H
chatwidget.cpp
#include "chatwidget.h"
#include "ui_chatwidget.h"
#include <QColorDialog>
#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QFileDialog>
#include <QMessageBox>
ChatWidget::ChatWidget(QWidget *parent, QString name) :
QWidget(parent),
ui(new Ui::ChatWidget)
{
ui->setupUi(this);
// 获取用户名(网名)通过构造函数获取
this->myName = name;
// 实例化套接字对象
this->udpSocket = new QUdpSocket(this);
// 设置端口号
this->port = 8989;
// 2.绑定地址与端口号
// 采用 ShareAddress 模式 (允许其他的服务连接到相同的地址和端口)
// 特别是用在多客户端监听同一个服务器端口号等时,特别有效, 和ReuseAddressHint 模式(重新连接服务器)
this->udpSocket->bind(this->port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
// 3. 监听信号
connect(this->udpSocket, &QUdpSocket::readyRead, this, &ChatWidget::ReceivMessage);
// 4. 点击发送按钮 将信息发送出去
connect(ui->sendBtn, &QPushButton::clicked, [=]()
{
// 广播普通聊天消息
sendMsg(MSG);
});
// 5. 广播用户上线消息
sendMsg(USERENTER);
// 6. 点击退出按钮关闭窗口
connect(ui->quitBtn, &QPushButton::clicked, this, &ChatWidget::close);
// 按键功能设置
// 1.字体设置
connect(ui->fontCbx, &QFontComboBox::currentFontChanged, [=](const QFont& font)
{
ui->msgTextEdit->setCurrentFont(font);
/* void QWidget::setFocus()
* 如果此小部件或其父窗口之一是活动窗口,
* 则将键盘输入焦点提供给此小部件(或其焦点代理)
*/
ui->msgTextEdit->setFocus();
});
// 2.设置字体大小
// 指针函数
void (QComboBox::*cbxsingal)(const QString &text)=&QComboBox::currentTextChanged;
connect(ui->sizeCbx, cbxsingal, [=](const QString &text)
{
ui->msgTextEdit->setFontPointSize(text.toDouble());
ui->msgTextEdit->setFocus();
});
// 3.字体加粗
connect(ui->boldBtn, &QPushButton::clicked, this, [=](bool checked)
{
if(checked) // 点击加粗按钮 加粗字体
{
ui->msgTextEdit->setFontWeight(QFont::Bold);
}
else // 没有点击加粗按钮 字体不加粗
{
ui->msgTextEdit->setFontWeight(QFont::Normal);
}
});
// 4.字体倾斜
connect(ui->italicBtn, &QPushButton::clicked, this, [=](bool checked)
{
ui->msgTextEdit->setFontItalic(checked);
ui->msgTextEdit->setFocus();
});
// 5.下划线
connect(ui->undetlineBtn, &QPushButton::clicked, this, [=](bool checked)
{
ui->msgTextEdit->setFontUnderline(checked);
ui->msgTextEdit->setFocus();
});
// 6.设置文字颜色
connect(ui->colorBtn, &QPushButton::clicked, this, [=]()
{
QColor color = QColorDialog::getColor(color, this);
ui->msgTextEdit->setTextColor(color);
});
// 7.清空聊天记录
connect(ui->clearBtn, &QPushButton::clicked, [=]()
{
ui->msgBrowser->clear();
});
// 8.保存聊天信息 打开文件 写文件
connect(ui->savdBtn, &QPushButton::clicked, [=]()
{
// 判断要保存的聊天信息为空
/* document : QTextDocument *
* 此属性保存文本编辑器的基础文档。
注意:编辑器不拥有文档的所有权,除非它是文档的父对象。
所提供文档的父对象仍然是该对象的所有者。如果先前指定的文档是编辑器的子文档,
则将删除该文档
*/
if(ui->msgBrowser->document()->isEmpty())
{
QMessageBox::warning(this, "警告", "要保存的信息不能为空!");
}
else
{
QString fileName = QFileDialog::getSaveFileName(this, "保存聊天记录", "聊天记录", "(*.txt)");
if(!fileName.isEmpty())
{
// 保存名称不能为空
QFile file(fileName);
file.open(QIODevice::WriteOnly | QFile::Text);
QTextStream stream(&file);
stream << ui->msgBrowser->toPlainText();
file.close();
}
}
});
}
ChatWidget::~ChatWidget()
{
delete ui;
}
// 重写关闭窗口事件 将关闭窗口信号发生出去
void ChatWidget::closeEvent(QCloseEvent *event)
{
emit this->closeWidget();
// 广播下线消息
sendMsg(USERLEFT);
// 关闭套接字
udpSocket->close();
// 销毁套接字
udpSocket->destroyed();
}
// 4. 广播udp 信息 将发送的信息显示到控件 TextBrowser
void ChatWidget::sendMsg(MsgType type)
{
// QDataStream 是数据流,相当于数据管道,屏蔽了数据转换过程
QByteArray array; // 创建一个数组 装数据
// 创建流 好处是可以分段 参数1:流入地址, 参数2:以只写的方式
QDataStream stream(&array, QIODevice::WriteOnly);
// 流入类型和用户名
stream << type << this->myName; // 将网名流入到数据流中去
//qDebug() << "测试 << this->myName : " << this->myName;
// 分类 类型消息处理
switch (type)
{
case MSG:
// 判断编辑框输入的消息是否为空
if(ui->msgTextEdit->toPlainText() == "")
{
QMessageBox::warning(this, "警告", "发送的信息不能为空");
return; // 退出函数
}
stream << this->getMsg(); // 流入信息 普通的聊天信息
//qDebug() << "测试 << this->getMsg() : " << this->getMsg();
break;
case USERENTER:
break;
case USERLEFT:
break;
default:
break;
}
// 发送数据报 书写报文 QHostAddress::Broadcast --> 发送给所有人
udpSocket->writeDatagram(array.data(), array.size(), QHostAddress::Broadcast, this->port);
}
// 6. 接收UDP 消息
void ChatWidget::ReceivMessage()
{
// 获取 第一个挂起的UDP 数据报的大小
qint64 size = udpSocket->pendingDatagramSize();
// 将 qint64 转换成 int
int mysize = static_cast<int>(size);
// 定义一个数组用于接收数据流的数据 将元素全部置成0
QByteArray array = QByteArray(mysize, 0); // 大小为mysize 第一个挂起的数据报的大小
/* QUdpSocket::readDatagram 接收数据报
* 接收不大与 maxSize 字节的数据并存储在数据中,发送方的主机地址和端口号存储在 *地址和*端口号中(除非指针为0)
* 成功返回数据报的大小; 否则返回-1
* 如果 maxsize 太小,则数据报的其余部分将丢失。为了避免数据丢失,请在尝试读取挂起的数据报之前
* 调用 penddingDatagramSize() 确定关起的数据报的大小,如果 maxSize 为0, 则数据报将被丢弃
*/
// 接收数据报
udpSocket->readDatagram(array.data(), size);
// 以只读的方式将信息流入到数组中去
QDataStream stream(&array, QIODevice::ReadOnly);
// 声明一个变量 通过 数据流的方式获取 消息类型
int msgtype;
//从数据流中流出类型 读取类型
stream >> msgtype; // 获取消息类型
//qDebug() << "测试 QDataStream ----> " << msgtype;
// 声明 用户名 聊天信息
QString name, msg;
// 格式化时间类型
QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
// 分类处理
switch(msgtype)
{
case MSG: // 普通消息
// 从数据流中流出 网名和聊天信息
stream >> name >> msg; // 获取网名 消息
// qDebug() << "测试name -->" << name;
// qDebug() << "测试msg -->" << msg;
// 将信息追加到控件 TextBorwser 中去(显示聊天信息的控件)
ui->msgBrowser->setTextColor(Qt::blue); // 设置显示信息的颜色
ui->msgBrowser->setCurrentFont(QFont("方正粗黑宋简体", 11));
// 追加网名 和 时间
ui->msgBrowser->append("[" + name + "]" + time);
// 追加聊天内容
ui->msgBrowser->append(msg);
break;
case USERENTER: // 用户进入
// 从数据流中流出 用户名
stream >> name;
userEnter(name);
break;
case USERLEFT: // 用户离开 下线
// 从数据流中流出 用户名
stream >> name;
userLeft(name, time);
break;
}
}
// 7. 获取聊天信息 获取编辑控件中的要发送的消息
QString ChatWidget::getMsg()
{
// 从编辑控件中获取输入的信息
// toPlainText()【简单文本】 和 toHtml() 的区别是,toHtml()可以设置特殊标点,分段等(富文本)
QString msg = ui->msgTextEdit->toHtml();
// 清空编辑框控件
ui->msgTextEdit->clear();
// 设置光标
ui->msgTextEdit->setFocus(); // 重置编辑框的鼠标光标
// 返回获取到的信息
return msg;
}
// 8. 用户进入 参数:用户名(网名)
void ChatWidget::userEnter(QString username)
{
// 查询 item 项中有没有匹配的
bool isEmpty = ui->tableWidget->findItems(username, Qt::MatchExactly).isEmpty();
if(isEmpty) // 为空时,显示新进入的用户(网名)
{
QTableWidgetItem * user = new QTableWidgetItem(username);
// 插入行
ui->tableWidget->insertRow(0);
// 设置 item 用户名
ui->tableWidget->setItem(0, 0, user);
// 将上线的用户名显示到聊天控件中
ui->msgBrowser->setTextColor(Qt::gray);
ui->msgBrowser->append(username + " 用户以上线");
// 显示在线人数到便签中去
ui->useNumlabel->setText(QString("在线人数:%1人").arg(ui->tableWidget->rowCount()));
// 发送消息 广播用户上线消息
sendMsg(USERENTER);
}
}
// 9. 用户下线 参数1:用户名,参数2:用户下线的时间
void ChatWidget::userLeft(QString username, QString time)
{
// 1.显示用户控件QTaleWidget 中的删除下线用户所在的行
// 2.显示用户下线消息,用户名+时间
// 查询控件 QTableWidget 中项有没有匹配到
bool isEmpty = ui->tableWidget->findItems(username, Qt::MatchExactly).isEmpty();
if(!isEmpty)
{
// 寻找行
int row = ui->tableWidget->findItems(username, Qt::MatchExactly).first()->row();
// 移除该行
ui->tableWidget->removeRow(row);
// 将下线的用户 显示聊天信息的控件中去
ui->msgBrowser->setTextColor(Qt::green);
ui->msgBrowser->setFont(QFont("叶根友毛笔行书2.0"));
ui->msgBrowser->append(username + "用户名于" + time + "下线");
// 显示在线人数的便签
ui->useNumlabel->setText(QString("在线人数:%1人").arg(ui->tableWidget->rowCount()));
}
}
程序运行结果:
?
|