mirror of
https://gitee.com/Zhaoxin59/my-chat_-client.git
synced 2026-02-13 16:41:48 +08:00
513 lines
18 KiB
C++
513 lines
18 KiB
C++
#include "messageshowarea.h"
|
||
|
||
#include <QMenu>
|
||
#include <qtimer.h>
|
||
#include "mainwidget.h"
|
||
#include "QFileDialog"
|
||
#include "toast.h"
|
||
#include "userinfowidget.h"
|
||
#include "model/datacenter.h"
|
||
#include "soundrecorder.h"
|
||
|
||
MessageShowArea::MessageShowArea() {
|
||
//初始化基本属性
|
||
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
this->setWidgetResizable(true);
|
||
|
||
//设置滚动条样式
|
||
this->verticalScrollBar()->setStyleSheet("QScrollBar:vertical { width: 2px; background-color: rgb(240, 240, 240); }");
|
||
this->horizontalScrollBar()->setStyleSheet("QScrollBar:horizontal { height: 0; }");
|
||
this->setStyleSheet("QScrollArea { border: none; }");
|
||
|
||
//创建container这样的widget,作为包含内部元素的容器
|
||
container = new QWidget();
|
||
this->setWidget(container);
|
||
|
||
//给container添加界面布局管理器
|
||
QVBoxLayout* layout = new QVBoxLayout();
|
||
layout->setSpacing(0);
|
||
layout->setContentsMargins(0, 0, 0, 0);
|
||
container->setLayout(layout);
|
||
|
||
// 添加测试数据
|
||
#if TEST_UI
|
||
|
||
//测试长消息
|
||
model::UserInfo userInfo;
|
||
userInfo.nickname = "???";
|
||
userInfo.userId = "4862";
|
||
userInfo.phone = "00000000000";
|
||
userInfo.description = "A test description.";
|
||
const QString s = R"(The starry sky is just a few years ago. And the past may no longer exist,The only thing that remains is light years away, it's just an ethereal phantom.)";
|
||
|
||
userInfo.avatar = QIcon(":/resource/image/defaultAvatar.png");
|
||
Message message = Message::makeMessage(model::TEXT_TYPE, "", userInfo, s.toUtf8(), "");
|
||
this->addMessage(false, message);
|
||
|
||
bool k = true;
|
||
for(int i = 0; i < 8; i++) {
|
||
model::UserInfo userInfo;
|
||
userInfo.userId = QString::number(i);
|
||
userInfo.phone = "12345678900";
|
||
userInfo.description = "A test description.";
|
||
userInfo.nickname = "xyz" + QString::number(i);
|
||
userInfo.avatar = QIcon(":/resource/image/defaultAvatar.png");
|
||
Message message = Message::makeMessage(model::TEXT_TYPE, "", userInfo, (QString("this is a test message...") + QString::number(i)).toUtf8(), "");
|
||
k = !k;
|
||
this->addMessage(k, message);
|
||
}
|
||
#endif
|
||
|
||
}
|
||
|
||
void MessageShowArea::addFrontMessage(bool isLeft, const Message &message)
|
||
{
|
||
MessageItem* messageItem = MessageItem::makeMessageItem(isLeft, message);
|
||
QVBoxLayout* layout = dynamic_cast<QVBoxLayout*>(container->layout());
|
||
layout->insertWidget(0, messageItem);
|
||
}
|
||
|
||
void MessageShowArea::addMessage(bool isLeft, const Message &message)
|
||
{
|
||
MessageItem* messageItem = MessageItem::makeMessageItem(isLeft, message);
|
||
container->layout()->addWidget(messageItem);
|
||
}
|
||
|
||
void MessageShowArea::clear()
|
||
{
|
||
QLayout* layout = container->layout();
|
||
//要遍历布局管理器,删除里面的元素
|
||
for(int i = layout->count() - 1; i >= 0; i--) {
|
||
QLayoutItem* item = layout->takeAt(i);
|
||
if(item != nullptr && item->widget() != nullptr) {
|
||
delete item->widget();
|
||
}
|
||
}
|
||
}
|
||
|
||
void MessageShowArea::scrollToEnd()
|
||
{
|
||
//拿到滚动区域的滚动条
|
||
//获取到滚动条的最大值并设置
|
||
//为了使滚动到正确的位置,即能够在界面绘制好后进行滚动设置
|
||
//这里选择给滚动条加个延时
|
||
QTimer* timer = new QTimer();
|
||
connect(timer, &QTimer::timeout, this, [=]() {
|
||
//获取到垂直滚动条的最大值
|
||
int maxValue = this->verticalScrollBar()->maximum();
|
||
//设置滚动条滚动的位置
|
||
this->verticalScrollBar()->setValue(maxValue);
|
||
|
||
timer->stop();
|
||
timer->deleteLater();
|
||
});
|
||
timer->start(500);
|
||
}
|
||
|
||
////////////////////////////////////////////
|
||
/// 表示一个消息元素
|
||
////////////////////////////////////////////
|
||
MessageItem::MessageItem(bool isLeft)
|
||
:isLeft(isLeft)
|
||
{
|
||
|
||
}
|
||
|
||
MessageItem *MessageItem::makeMessageItem(bool isLeft, const Message &message)
|
||
{
|
||
//创建对象和布局管理器
|
||
MessageItem* messageItem = new MessageItem(isLeft);
|
||
QGridLayout* layout = new QGridLayout();
|
||
layout->setSpacing(10);
|
||
layout->setContentsMargins(30, 10, 40, 0);
|
||
|
||
//这个message最低不能低于100
|
||
messageItem->setMinimumHeight(100);
|
||
messageItem->setLayout(layout);
|
||
|
||
//创建头像
|
||
QPushButton* avatarBtn = new QPushButton();
|
||
avatarBtn->setFixedSize(40, 40);
|
||
avatarBtn->setIconSize(QSize(40, 40));
|
||
avatarBtn->setIcon(message.sender.avatar);
|
||
avatarBtn->setStyleSheet("QPushButton { border: none; }");
|
||
if(isLeft) {
|
||
layout->addWidget(avatarBtn, 0, 0, 2, 1, Qt::AlignCenter | Qt::AlignLeft);
|
||
} else {
|
||
layout->addWidget(avatarBtn, 0, 1, 2, 1, Qt::AlignCenter | Qt::AlignRight);
|
||
}
|
||
|
||
//创建名字和时间
|
||
QLabel* nameLabel = new QLabel();
|
||
nameLabel->setText(message.sender.nickname + " " + message.time);
|
||
nameLabel->setAlignment(Qt::AlignBottom);
|
||
nameLabel->setStyleSheet("QLabel { font-size: 12px; color: rgb(178, 178, 178); }");
|
||
if(isLeft) {
|
||
layout->addWidget(nameLabel, 0, 1);
|
||
} else {
|
||
layout->addWidget(nameLabel, 0, 0, Qt::AlignRight);
|
||
}
|
||
|
||
//创建消息体
|
||
QWidget* contentWidget = nullptr;
|
||
switch(message.messageType) {
|
||
case model::TEXT_TYPE:
|
||
contentWidget = makeTextMessageItem(isLeft, message.content);
|
||
break;
|
||
case model::IMAGE_TYPE:
|
||
contentWidget = makeImageMessageItem(isLeft, message.fileId, message.content);
|
||
break;
|
||
case model::FILE_TYPE:
|
||
contentWidget = makeFileMessageItem(isLeft, message);
|
||
break;
|
||
case model::SPEECH_TYPE:
|
||
contentWidget = makeSpeechMessageItem(isLeft, message);
|
||
break;
|
||
default:
|
||
LOG() << "error messageType: " << message.messageType;
|
||
}
|
||
if(isLeft) {
|
||
layout->addWidget(contentWidget, 1, 1);
|
||
} else {
|
||
layout->addWidget(contentWidget, 1, 0);
|
||
}
|
||
|
||
//连接信号槽,处理用户点击头像的操作
|
||
connect(avatarBtn, &QPushButton::clicked, messageItem, [=]() {
|
||
MainWidget* mainWidget = MainWidget::getInstance();
|
||
UserInfoWidget* userInfoWidget = new UserInfoWidget(message.sender, mainWidget);
|
||
userInfoWidget->setStyleSheet("QDialog { background-color: rgb(245, 245, 245); }");
|
||
userInfoWidget->exec();
|
||
});
|
||
|
||
//当用户修改了昵称的时候,更新名字的显示
|
||
if (!isLeft) {
|
||
DataCenter* dataCenter = DataCenter::getInstance();
|
||
connect(dataCenter, &DataCenter::changeNicknameDone, messageItem, [=]() {
|
||
nameLabel->setText(dataCenter->getMyselfsync()->nickname + " " + message.time);
|
||
});
|
||
|
||
connect(dataCenter, &DataCenter::changeAvatarDone, messageItem, [=]() {
|
||
UserInfo* myself = dataCenter->getMyselfsync();
|
||
avatarBtn->setIcon(myself->avatar);
|
||
});
|
||
}
|
||
return messageItem;
|
||
}
|
||
|
||
QWidget *MessageItem::makeTextMessageItem(bool isLeft, const QString &text)
|
||
{
|
||
MessageContentLabel* messageContentLabel = new MessageContentLabel(text, isLeft, TEXT_TYPE, "", QByteArray());
|
||
return messageContentLabel;
|
||
}
|
||
|
||
QWidget *MessageItem::makeImageMessageItem(bool isLeft, const QString& fileId, const QByteArray& content)
|
||
{
|
||
MessageImageLabel* messageImageLabel = new MessageImageLabel(fileId, content, isLeft);
|
||
return messageImageLabel;
|
||
}
|
||
|
||
QWidget *MessageItem::makeFileMessageItem(bool isLeft, const Message& message)
|
||
{
|
||
MessageContentLabel* messageContentLabel = new MessageContentLabel("[文件] " + message.fileName, isLeft, message.messageType,
|
||
message.fileId, message.content);
|
||
return messageContentLabel;
|
||
}
|
||
|
||
QWidget *MessageItem::makeSpeechMessageItem(bool isLeft, const Message& message)
|
||
{
|
||
MessageContentLabel* messageContentLabel = new MessageContentLabel("[语音]", isLeft, message.messageType, message.fileId, message.content);
|
||
return messageContentLabel;
|
||
}
|
||
|
||
|
||
|
||
////////////////////////////////////////////
|
||
/// 创建类表示“文本消息”正文部分
|
||
//这个类也表示文件消息
|
||
////////////////////////////////////////////
|
||
MessageContentLabel::MessageContentLabel(const QString& text, bool isLeft, model::MessageType messageType, const QString& fileId,
|
||
const QByteArray& content)
|
||
:isLeft(isLeft), messageType(messageType), fileId(fileId), content(content)
|
||
{
|
||
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
|
||
QFont font;
|
||
font.setFamily("微软雅黑");
|
||
font.setPixelSize(16);
|
||
|
||
this->label = new QLabel(this);
|
||
this->label->setText(text);
|
||
this->label->setFont(font);
|
||
this->label->setAlignment(Qt::AlignCenter | Qt::AlignLeft);
|
||
this->label->setWordWrap(true);
|
||
this->label->setStyleSheet("QLabel { padding: 0 10px; line-height: 1.2; background-color: transparent; }");
|
||
|
||
//针对文件消息,且content为空,通过网络加载数据
|
||
if (messageType == model::TEXT_TYPE) {
|
||
return;
|
||
}
|
||
|
||
if (this->content.isEmpty()) {
|
||
DataCenter* dataCenter = DataCenter::getInstance();
|
||
connect(dataCenter, &DataCenter::getSingleFileDone, this, &MessageContentLabel::updateUI);
|
||
dataCenter->getSingleFileAsync(this->fileId);
|
||
}
|
||
else {
|
||
//content 不为空,说明这个数据已经是现成的,加载状态直接改为true即可
|
||
this->loadContentDone = true;
|
||
}
|
||
}
|
||
|
||
//这个函数会在该控件被显示时,自动的调用到
|
||
void MessageContentLabel::paintEvent(QPaintEvent *event)
|
||
{
|
||
(void) event;
|
||
|
||
//获取到父元素的宽度
|
||
QObject* object = this->parent();
|
||
if(!object->isWidgetType()) {
|
||
//说明当前的对象不是QWidget,则不需要进行任何后续的绘制操作
|
||
return;
|
||
}
|
||
QWidget* parent = dynamic_cast<QWidget*>(object);
|
||
int width = parent->width() * 0.6;
|
||
|
||
//计算当前文本,如果单行防止放置需要多宽
|
||
QFontMetrics metrics(this->label->font());
|
||
int totalWidth = metrics.horizontalAdvance(this->label->text());
|
||
|
||
//计算出此处的行数是多少
|
||
int rows = (totalWidth / (width - 40)) + 1;
|
||
if(rows == 1) {
|
||
//若此时得到的行数只有一行
|
||
width = totalWidth + 40;
|
||
}
|
||
|
||
//根据行数来确定高度
|
||
//行数 × 行高(字体高度的 1.2 倍) + 上下内边距(各 10px)
|
||
int height = rows * (this->label->font().pixelSize() * 1.2 ) + 20;
|
||
|
||
//绘制圆角矩形和箭头
|
||
QPainter painter(this);
|
||
QPainterPath path;
|
||
//设置抗锯齿
|
||
painter.setRenderHint(QPainter::Antialiasing);
|
||
if(isLeft) {
|
||
painter.setPen(QPen(QColor(255, 255, 255)));
|
||
painter.setBrush(QColor(255, 255, 255)); //白色填充
|
||
|
||
//绘制圆角矩形
|
||
painter.drawRoundedRect(10, 0, width, height, 10 ,10);
|
||
//绘制箭头
|
||
path.moveTo(10, 15);
|
||
path.lineTo(0, 20);
|
||
path.lineTo(10, 25);
|
||
path.closeSubpath(); //绘制的线形成闭合的多边形,才能进行使用Brush填充颜色
|
||
painter.drawPath(path);//设置好,调用画笔进行绘制操作
|
||
|
||
this->label->setGeometry(10, 0, width, height);
|
||
} else {
|
||
painter.setPen(QPen(QColor(137, 217, 97)));
|
||
painter.setBrush(QColor(137, 217, 97));
|
||
|
||
//圆角矩形左侧边的横坐标位置
|
||
int leftPos = this->width() - width - 10; //10 用来容纳箭头的宽度
|
||
//圆角矩形右侧边的横坐标位置
|
||
int rightPos = this->width() - 10;
|
||
|
||
//绘制圆角矩形
|
||
painter.drawRoundedRect(leftPos, 0, width, height, 10, 10);
|
||
//绘制箭头
|
||
path.moveTo(rightPos, 15);
|
||
path.lineTo(rightPos + 10, 20);
|
||
path.lineTo(rightPos, 25);
|
||
path.closeSubpath(); //绘制的线形成闭合的多边形,才能进行使用Brush填充颜色
|
||
painter.drawPath(path);//设置好,调用画笔进行绘制操作
|
||
|
||
this->label->setGeometry(leftPos, 0, width, height);
|
||
}
|
||
|
||
//重新设置父元素的高度,保证父元素足够的高,能够容纳下上述绘制消息的显示区域
|
||
//注意高度要涵盖之前的名字和时间的label的高度,以及留一点的冗余的空间
|
||
parent->setFixedHeight(height + 50);
|
||
}
|
||
|
||
void MessageContentLabel::mousePressEvent(QMouseEvent* event)
|
||
{
|
||
//鼠标点击之后,触发文件另存为
|
||
if (event->button() == Qt::LeftButton) {
|
||
//左键按下
|
||
if (this->messageType == FILE_TYPE) {
|
||
//真正触发另存为
|
||
if (!this->loadContentDone) {
|
||
Toast::showMessage("数据尚未加载成功,请稍后重试");
|
||
return;
|
||
}
|
||
saveAsFile(this->content);
|
||
}
|
||
else if (this->messageType == SPEECH_TYPE) {
|
||
if (!this->loadContentDone) {
|
||
Toast::showMessage("数据尚未加载成功,请稍后重试");
|
||
return;
|
||
}
|
||
SoundRecorder* soundRecorder = SoundRecorder::getInstance();
|
||
this->label->setText("播放中...");
|
||
connect(soundRecorder, &SoundRecorder::soundPlayDone, this, &MessageContentLabel::playDone, Qt::UniqueConnection);
|
||
soundRecorder->startPlay(this->content);
|
||
}
|
||
else {
|
||
//啥也不做
|
||
}
|
||
}
|
||
}
|
||
|
||
void MessageContentLabel::updateUI(const QString& fileId, const QByteArray& fileContent)
|
||
{
|
||
if (fileId != this->fileId) {
|
||
return;
|
||
}
|
||
this->content = fileContent;
|
||
this->loadContentDone = true;
|
||
//从服务器拿到文件正文之前,界面内容就应该已经绘制好了,拿到正文之后,也不需要做出调整
|
||
//所以,👇没有也行
|
||
this->update();
|
||
}
|
||
|
||
void MessageContentLabel::saveAsFile(const QByteArray& content)
|
||
{
|
||
//弹出对话框,让让用户选择路径
|
||
QString filePath = QFileDialog::getOpenFileName(this, "另存为", QDir::homePath(), "*");
|
||
if (filePath.isEmpty()) {
|
||
LOG() << "用户取消了文件另存为";
|
||
return;
|
||
}
|
||
writeByteArrayToFile(filePath, content);
|
||
}
|
||
|
||
void MessageContentLabel::playDone()
|
||
{
|
||
if (this->label->text() == "播放中...") {
|
||
this->label->setText("[语音]");
|
||
}
|
||
}
|
||
|
||
void MessageContentLabel::contextMenuEvent(QContextMenuEvent* event)
|
||
{
|
||
//LOG() << "触发上下文菜单";
|
||
(void)event;
|
||
if (messageType != SPEECH_TYPE) {
|
||
LOG() << "非语音消息暂不支持右键菜单";
|
||
return;
|
||
}
|
||
|
||
QMenu* menu = new QMenu(this);
|
||
menu->setStyleSheet("QMenu { color: rgb(0, 0, 0); }");
|
||
QAction* action = menu->addAction("语音转文字");
|
||
connect(action, &QAction::triggered, this, [=]() {
|
||
DataCenter* dataCenter = DataCenter::getInstance();
|
||
connect(dataCenter, &DataCenter::speechConvertTextDone, this, &MessageContentLabel::speechConverTextDone, Qt::UniqueConnection);
|
||
dataCenter->speechConvertTextAsync(this->fileId, this->content);
|
||
});
|
||
//类似于模态对话框
|
||
menu->exec(event->globalPos());
|
||
delete menu;
|
||
}
|
||
|
||
void MessageContentLabel::speechConverTextDone(const QString& fileId, const QString& text)
|
||
{
|
||
if (this->fileId != fileId) {
|
||
return;
|
||
}
|
||
//修改界面内容
|
||
this->label->setText("[语音转文字] " + text);
|
||
this->update();
|
||
}
|
||
|
||
////////////////////////////////////////////
|
||
/// 创建类表示“图片消息”部分
|
||
////////////////////////////////////////////
|
||
MessageImageLabel::MessageImageLabel(const QString& fileId, const QByteArray& content, bool isLeft)
|
||
:fileId(fileId), content(content), isLeft(isLeft)
|
||
{
|
||
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
|
||
imageBtn = new QPushButton(this);
|
||
imageBtn->setStyleSheet("QPushButton { border: none; border-radius: 10px; }");
|
||
|
||
if (content.isEmpty()) {
|
||
//此处,从服务器拿到图片消息
|
||
//拿着fileId,去服务器获取图片内容
|
||
DataCenter* dataCenter = DataCenter::getInstance();
|
||
connect(dataCenter, &DataCenter::getSingleFileDone, this, &MessageImageLabel::updateUI);
|
||
dataCenter->getSingleFileAsync(fileId);
|
||
}
|
||
}
|
||
|
||
void MessageImageLabel::updateUI(const QString& fileId, const QByteArray& content)
|
||
{
|
||
//由于是一呼百应,要判断fileId是否是当前的fileId
|
||
if (this->fileId != fileId) {
|
||
return;
|
||
}
|
||
//对上了,就真正显示图片内容
|
||
this->content = content;
|
||
|
||
//进行绘制图片到界面上
|
||
this->update();
|
||
}
|
||
|
||
//真正进行绘制图片到界面上
|
||
void MessageImageLabel::paintEvent(QPaintEvent* event)
|
||
{
|
||
(void)event;
|
||
QObject* object = this->parent();
|
||
if (!object->isWidgetType()) {
|
||
return;
|
||
}
|
||
QWidget* parent = dynamic_cast<QWidget*>(object);
|
||
int width = parent->width() * 0.4;
|
||
|
||
//加载二进制数据为图片对象
|
||
QImage image;
|
||
if (content.isEmpty()) {
|
||
//说明此时响应的数据还没有回来,
|
||
//那么先拿默认图片临时代替
|
||
QByteArray tmpContent = loadFileToByteArray(":resource/image/image.png");
|
||
image.loadFromData(tmpContent);
|
||
}
|
||
else {
|
||
image.loadFromData(content);
|
||
}
|
||
|
||
//针对图片进行缩放
|
||
int height = 0;
|
||
if (image.width() > width) {
|
||
//说明图片过宽,把图片放缩(等比)
|
||
height = ((double)image.height() / image.width()) * width;
|
||
}
|
||
else {
|
||
//没有过阈值,不用管
|
||
width = image.width();
|
||
height = image.height();
|
||
}
|
||
|
||
//QImage不能直接转换为QIcon,需要QPixmap中转一下
|
||
QPixmap pixmap = QPixmap::fromImage(image);
|
||
imageBtn->setFixedSize(width, height);
|
||
imageBtn->setIconSize(QSize(width, height));
|
||
imageBtn->setIcon(QIcon(pixmap));
|
||
|
||
//为了容纳下上方名字部分,同时留下一点冗余
|
||
parent->setFixedHeight(height + 50);
|
||
|
||
//确定是左侧还是右侧消息
|
||
if (isLeft) {
|
||
imageBtn->setGeometry(10, 0, width, height);
|
||
}
|
||
else {
|
||
int leftPos = this->width() - width - 10;
|
||
imageBtn->setGeometry(leftPos, 0, width, height);
|
||
}
|
||
}
|