Files
MyChat_Client/messageshowarea.cpp
2025-09-09 15:37:57 +08:00

513 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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);
}
}