1. 获取文件内容
主要目标是实现获取内容二进制数据的接口,主要是为后面的消息功能提供服务
具体实现
客户端发送请求
服务端处理请求,同时支持三种数据类型
客户端处理服务端的响应
2. 发送图片消息
客户端与服务端的通信约定
客户端从服务器中获取图片消息的时候的,仅返回消息中的文件ID,而不是直接包含文件内容。如果需要文件的实际内容则需要客户端进行二次请求来获取相应的数据
设计目的为了减少初始消息传输体积,从而提高传输效率。客户端和服务端通信,直接传输问价的时候,可能会影响性能,通过文件ID二次获取内容,确保消息的基本信息与文件数据分开传输,从而避免占用过多带宽
服务器和服务器之间,直接附带文件内容,而不会单独的通过文件ID请求。因为服务器的网络环境是相对稳定的,传输文件不会造成较大的性能问题
客户端界面发送图片消息实现
整体流程首先是初始化显示图片的控件,然后配置其样式。然后异步加载图片,如果图片数据没有加载那么就异步获取图片内容;如果获取了图片数据,那么就出发updateUI进行页面更新。最后根据父组件的大小在页面上进行绘制
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; }");
if(content.isEmpty()){
DataCenter* dataCenter = DataCenter::genInstance();
connect(dataCenter, &DataCenter::getSingleFileDone, this, &MessageImageLabel::updateUI);
dataCenter->getSingleFileAsync(fileId);
}
}
void MessageImageLabel::updateUI(const QString &fileId, const QByteArray &content)
{
//不是当前FileID
if(this->fileId != fileId){
return;
}
this->content = content;
this->update();
}
void MessageImageLabel::paintEvent(QPaintEvent *event)
{
(void)event;
//1.根据父控件计算图片最大宽度
QObject* object = this->parent();
if(!object->isWidgetType()){
return;
}
QWidget* parent = dynamic_cast<QWidget*>(object);
int width = parent->width()*0.6;//最大宽度设置为父控件的0.6倍
//2.加载图片数据
QImage image;
if(content.isEmpty()){
//图片数据为空的时候,加载默认图片
QByteArray tmpContent = loadFileToByteArray(":/resource/image/image.png");
image.loadFromData(tmpContent);
}else{
image.loadFromData(content);
}
//3.根据父控件宽度缩放照片
int height = 0;
if(image.width() > width){
height = static_cast<int>(((double)image.height() / image.width()) * width);
}else{
width = image.width();
height = image.height();
}
//4.将QImage转换为QPixmap
QPixmap pixmap = QPixmap::fromImage(image);
imageBtn->setIconSize(QSize(width,height));
imageBtn->setIcon(QIcon(pixmap));
//5.动态调整父组件高度
parent->setFixedHeight(height + 50);
//6.根据消息类型调整按钮位置
if(isLeft){
//左侧消息靠左显示
imageBtn->setGeometry(10,0,width,height);
}else{
//右侧消息靠右显示
int leftPos = this->width() - width -10;
imageBtn->setGeometry(leftPos,0,width,height);
}
}
websocket推送图片消息实现
总体逻辑仍然通过按钮触发信号,然后通过服务器槽函数进行处理,向客户端推送图片消息,最后客户端对接收到的响应进行处理即可(处理响应已经在获取文件内容进行了统一处理)
3. 发送文件消息
具体实现
点击图片按钮触发该处点击逻辑
通过客户端发送请求到服务端
在消息显示区中,将文件信息显示上去
4. 语音消息
4.1 录制音频
具体实现
实现鼠标按下录制,松开完成录制的功能
发送语音逻辑
4.2 播放音频
具体实现
点击语音消息的时候触发该处逻辑
更新UI
4.3 语音转文字
具体实现
补充:音频代码
#ifndef SOUNDRECORDER_H
#define SOUNDRECORDER_H
#include <QObject>
#include <QStandardPaths>
#include <QFile>
#include <QAudioSource>
#include <QAudioSink>
#include <QMediaDevices>
class SoundRecorder : public QObject
{
Q_OBJECT
public:
const QString RECORD_PATH = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/sound/tmpRecord.pcm";
const QString PLAY_PATH = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/sound/tmpPlay.pcm";
public:
static SoundRecorder* getInstance();
/
/// 录制语音语音
/
// 开始录制
void startRecord();
// 停止录制
void stopRecord();
private:
static SoundRecorder* instance;
explicit SoundRecorder(QObject *parent = nullptr);
QFile soundFile;
QAudioSource* audioSource;
/
/// 播放语音
/
public:
// 开始播放
void startPlay(const QByteArray& content);
// 停止播放
void stopPlay();
private:
QAudioSink *audioSink;
QMediaDevices *outputDevices;
QAudioDevice outputDevice;
QFile inputFile;
signals:
// 录制完毕后发送这个信号
void soundRecordDone(const QString& path);
// 播放完毕发送这个信号
void soundPlayDone();
};
#endif // SOUNDRECORDER_H
#include "soundrecorder.h"
#include <QDir>
#include <QMediaDevices>
#include "model/data.h"
#include "toast.h"
/
/// 单例模式
/
SoundRecorder* SoundRecorder::instance = nullptr;
SoundRecorder *SoundRecorder::getInstance()
{
if (instance == nullptr) {
instance = new SoundRecorder();
}
return instance;
}
// 播放参考 https://www.cnblogs.com/tony-yang-flutter/p/16477212.html
// 录制参考 https://doc.qt.io/qt-6/qaudiosource.html
SoundRecorder::SoundRecorder(QObject *parent)
: QObject{parent} {
// 1. 创建目录
QDir soundRootPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
soundRootPath.mkdir("sound");
// 2. 初始化录制模块
soundFile.setFileName(RECORD_PATH);
QAudioFormat inputFormat;
inputFormat.setSampleRate(16000);
inputFormat.setChannelCount(1);
inputFormat.setSampleFormat(QAudioFormat::Int16);
QAudioDevice info = QMediaDevices::defaultAudioInput();
if (!info.isFormatSupported(inputFormat)) {
LOG() << "录制设备, 格式不支持!";
return;
}
audioSource = new QAudioSource(inputFormat, this);
connect(audioSource, &QAudioSource::stateChanged, this, [=](QtAudio::State state) {
if (state == QtAudio::StoppedState) {
// 录制完毕
if (audioSource->error() != QAudio::NoError) {
LOG() << audioSource->error();
}
}
});
// 3. 初始化播放模块
outputDevices = new QMediaDevices(this);
outputDevice = outputDevices->defaultAudioOutput();
QAudioFormat outputFormat;
outputFormat.setSampleRate(16000);
outputFormat.setChannelCount(1);
outputFormat.setSampleFormat(QAudioFormat::Int16);
if (!outputDevice.isFormatSupported(outputFormat)) {
LOG() << "播放设备, 格式不支持";
return;
}
audioSink = new QAudioSink(outputDevice, outputFormat);
connect(audioSink, &QAudioSink::stateChanged, this, [=](QtAudio::State state) {
if (state == QtAudio::IdleState) {
LOG() << "IdleState";
this->stopPlay();
emit this->soundPlayDone();
} else if (state == QAudio::ActiveState) {
LOG() << "ActiveState";
} else if (state == QAudio::StoppedState) {
LOG() << "StoppedState";
if (audioSink->error() != QtAudio::NoError) {
LOG() << audioSink->error();
}
}
});
}
void SoundRecorder::startRecord() {
soundFile.open( QIODevice::WriteOnly | QIODevice::Truncate );
audioSource->start(&soundFile);
}
void SoundRecorder::stopRecord() {
audioSource->stop();
soundFile.close();
emit this->soundRecordDone(RECORD_PATH);
}
void SoundRecorder::startPlay(const QByteArray& content) {
if (content.isEmpty()) {
Toast::showMessage("数据加载中, 请稍后播放");
return;
}
// 1. 把数据写入到临时文件
model::writeByteArrayToFile(PLAY_PATH, content);
// 2. 播放语音
inputFile.setFileName(PLAY_PATH);
inputFile.open(QIODevice::ReadOnly);
audioSink->start(&inputFile);
}
void SoundRecorder::stopPlay() {
audioSink->stop();
inputFile.close();
}
5. 历史消息调整
补充之前历史消息的遗漏问题,历史消息可以显示文本消息和语音消息,其中点击文件消息可以出触发保存操作;点击语音消息可以触发播放操作
具体实现
调用地方在历史消息显示窗口,其中通过判断不同消息类型进行创建
图片历史消息
- 初始化的时候,如果图片内容存在就直接显示;如果图片为空,那么就通过DataCenter请求图片数据
- 图片数据从网络中加载完成后,通过更新界面的方法显示到界面上,同时根据窗口大小进行调整
文件历史消息
基本逻辑与图片消息相同,只是多了一个重写鼠标点击操作,点击触发另存为操作
语音历史消息
逻辑和文件消息相同,点击语音消息可以触发播放操作
6. 发布程序
借助Qt下的windeployqt.ext实现程序自动获得依赖文件
release版本中添加外部依赖库
创建好的文件结构