重庆官方推广网站泰安飞讯网络有限公司

当前位置: 首页 > news >正文

重庆官方推广网站,泰安飞讯网络有限公司,建设微信网站需要服务器,wordpress 手机首页设置目录 1. 项目整体介绍1.1 项目概况1.2 界面预览和功能介绍1.3 技术重点和服务器架构 2. 项目环境搭建2.1 安装Qt62.3 安装vcpkg2.3 安装protobuf2.4 构建项目2.5 配置CMake属性 3. 项目核心数据结构的实现3.1 创建data.h存放核心的类3.2 工具函数的实现3.3 创建编译开关 4. 界面… 目录 1. 项目整体介绍1.1 项目概况1.2 界面预览和功能介绍1.3 技术重点和服务器架构 2. 项目环境搭建2.1 安装Qt62.3 安装vcpkg2.3 安装protobuf2.4 构建项目2.5 配置CMake属性 3. 项目核心数据结构的实现3.1 创建data.h存放核心的类3.2 工具函数的实现3.3 创建编译开关 4. 界面整体布局的实现4.1 主页面的布局和实现4.2 主界面左侧页面各个功能的实现4.3 主界面中间页面各个功能的实现4.4 主界面右侧页面各个功能的实现4.4.1 实现会话标题栏4.4.2 实现消息展示区域4.4.3 实现消息编辑区域 5. 实现主界面各个按钮的点击功能5.1 实现个人信息详细界面5.2 实现用户详细信息界面5.3 实现单聊消息会话详细信息界面5.4 实现创建群聊会话选择好友界面5.5 实现群聊消息会话详细信息界面5.6 实现添加好友界面5.7 实现历史消息界面 6. 用户名登录/注册界面的实现7. 实现手机号登录/注册界面8. 实现全局通知类9. 构建界面注意事项10. 将项目所需要的图片导入Qt项目中 1. 项目整体介绍 1.1 项目概况 本项目是基于 C 的实现⼀个客户端-服务器结构的聊天程序。 客户端基于 Qt 6的实现。服务器基于 C 的分布式微服务架构 主流后端组件。 服务器微服务个数7 个服务器组件个数17 个业务功能点40前后端交互接口40数据库表个数6 个总代码量1.8w 1.2 界面预览和功能介绍 1下面所展示的界面是在没有接入服务器的情况下展示的效果 2以下是客户端主要功能图 1.3 技术重点和服务器架构 1本项目在设计的时候采用微服务框架设计微服务就是将⼀个大的业务拆分称为多个子业务分别在多台不同的机器节点上提供对应的服务由网关服务统⼀接收多个客户端的各种不同请求然后将请求分发到不同的子服务节点上进行处理获取响应后再转发给客户端。 2模块层次 3服务拆分 入口网关服务器主要用于与客户端直接交互接收客户端的各项请求提供服务。用户管理子服务主要用于管理⽤⼾的数据以及关于⽤户信息的各项操作。好友管理子服务主要用于管理好友与聊天会话管理相关的数据与操作。转发管理子服务主要用于封装消息进行转存然后告诉网关服务器⼀条消息应该发给谁。消息存储子服务主要用于进行消息元信息的存储与搜索功能。文件管理子服务主要用于管理系统中文件类型数据的存储比如用户头像文件消息等。语音转换子服务用于调用语音识别SDK进行语音识别将语音转换为文字。 4技术重点 gflags针对程序运行所需的运行参数解析/配置文件解析框架。gtest针对程序编写到⼀定阶段后进行的单元测试框架。spdlog针对项目中进行日志输出的框架。protobuf针对项目中的网络通信数据所采用的序列化和反序列化框架。brpc项目中的rpc调用使用的框架。redis高性能键值存储系统用于项目中进行用户登录会话信息的存储管理。mysql关系型数据库系统用于项目中的业务数据的存储管理。ODB项目中mysql数据库操作的ORM框架Object-Relational Mapping对象关系映射。Etcd分布式、高可用的⼀致性键值存储系统用于项目中实现服务注册与发现功能的框架。cpp-httplib用于搭建简单轻量HTTP服务器的框架。websocketpp用于搭建Websocket服务器的框架。rabbitMQ用于搭建消息队列服务器用于项⽬中持久化消息的转发消费。elasticsearch用于搭建⽂档存储/搜索服务器用于项⽬中历史消息的存储管理语音云平台采用百度语音识别技术云平台实现语音转文字功能。短信云平台采用阿里云短信云平台实现手机短信验证码通知功能。cmake项目工程的构建工具。docker项目工程的⼀键式部署工具。

  1. 项目环境搭建 2.1 安装Qt6 1Qt6链接https://www.qt.io/download-qt-installer-oss?hsCtaTracking99d9dd4f-5681-48d2-b096-470725510d34%7C074ddad0-fdef-4e53-8aa8-5e8a876d6ab4 2注册登录Qt账户 3进行到选择组件的时候勾选6.7及以上 Qt6项目中一旦代码规模变大了使用MinGW编译速度就会慢很多。MSVCvs自带编译器必须配合VS2019 及其以上版。 4将Additional Libraries全部勾选上 5开发工具勾选 之后的安装只需要一直next即可。 2.3 安装vcpkg 1vcpkg 是一个开源的跨平台 C 包管理工具用于简化库的下载、构建和管理过程。它支持多种操作系统并自动处理依赖关系。参考文档https://learn.microsoft.com/zh-cn/vcpkg/get_started/get-started?pivotsshell-powershell 2打开cmd命令行将vcpkg克隆下来 git clone https://github.com/microsoft/vcpkg.git3进入vcpkg并运行 cd vcpkg bootstrap-vcpkg.bat2.3 安装protobuf 1在cmd命令行当中运行如下代码必须在vcpkg文件当中运行 ./vcpkg.exe install protobuf protobuf:x64-windows2.4 构建项目 1选择Qt widgets 2选择路径和项目名称 3选择构建项目 4选择主窗口 5选择构建套件 将上述操作选择玩项目就算是构建完成了接下来配置CMake。 2.5 配置CMake属性 1配置如下属性 2当运行时找不到protobuf时配置需要点击左侧边栏 “项目”然后修改 cmake的配置项CMAKE_PREFIX_PATH。添加上 protoc 和 grpc 的路径前缀。当切换 debug release 时也要同时修改上述配置。是添加到 current configuration 标签页, 而不是 Initial Configuration 标签页。 3如果未能正确配置会出现形如 这样的错误。
  2. 项目核心数据结构的实现 3.1 创建data.h存放核心的类 1在Header Files当中创建一个model文件夹将data.h存放在此处。 2核心数据类的实现 // /// 用户信息 // class UserInfo { public:QString userId ; // 用户编号QString nickname ; // 用户昵称QString description ; // 用户签名QString phone ; // 手机号码QIcon avatar; // 用户头像 };// /// 消息信息 // enum MessageType {TEXT_TYPE, // 文本消息IMAGE_TYPE, // 图片消息FILE_TYPE, // 文件消息SPEECH_TYPE // 语音消息 };class Message { public:QString messageId ; // 消息的编号QString chatSessionId ; // 消息所属会话的编号QString time ; // 消息的时间. 通过 格式化 时间的方式来表示. 形如 06-07 12:00:00MessageType messageType TEXT_TYPE;// 消息类型UserInfo sender; // 发送者的信息QByteArray content; // 消息的正文内容QString fileId ; // 文件的身份标识. 当消息类型为 文件, 图片, 语音 的时候, 才有效. 当消息类型为 文本, 则为 QString fileName ; // 文件名称. 只是当消息类型为 文件 消息, 才有效. 其他消息均为 // 此处 extraInfo 目前只是在消息类型为文件消息时, 作为 文件名 补充.static Message makeMessage(MessageType messageType, const QString chatSessionId,const UserInfo sender, const QByteArray content,const QString extraInfo){if(messageType TEXT_TYPE){return makeTextMessage(chatSessionId, sender, content);}else if(messageType IMAGE_TYPE){return makeImageMessage(chatSessionId, sender, content);}else if(messageType FILE_TYPE){return makeFileMessage(chatSessionId, sender, content, extraInfo);}else if(messageType SPEECH_TYPE){return makeSpeechMessage(chatSessionId, sender, content);}else{// 触发了未知的消息类型return Message();}}private:// 通过这个方法生成唯一的 messageIdstatic QString makeId(){return M QUuid::createUuid().toString().sliced(25, 12);}static Message makeTextMessage(const QString chatSessionId,const UserInfo sender, const QByteArray content){Message message;message.messageId makeId();message.chatSessionId chatSessionId;message.messageType TEXT_TYPE;message.content content;message.sender sender;message.time formatTime(getTime()); // 生成一个格式化时间// 对于文本消息来说, 这俩属性不使用, 设为 message.fileId ;message.fileName ;return message;}static Message makeImageMessage(const QString chatSessionId,const UserInfo sender, const QByteArray content){Message message;message.messageId makeId();message.chatSessionId chatSessionId;message.messageType IMAGE_TYPE;message.content content;message.sender sender;message.time formatTime(getTime()); // 生成一个格式化时间// fileId 后续使用的时候再进一步设置message.fileId ;// fileName 不使用, 直接设为 message.fileName ;return message;}static Message makeFileMessage(const QString chatSessionId, const UserInfo sender,const QByteArray content, const QString fileName){Message message;message.messageId makeId();message.chatSessionId chatSessionId;message.messageType FILE_TYPE;message.content content;message.sender sender;message.time formatTime(getTime()); // 生成一个格式化时间// fileId 后续使用的时候进一步设置message.fileId ;message.fileName fileName;return message;}static Message makeSpeechMessage(const QString chatSessionId,const UserInfo sender, const QByteArray content){Message message;message.messageId makeId();message.chatSessionId chatSessionId;message.messageType SPEECH_TYPE;message.content content;message.sender sender;message.time formatTime(getTime()); // 生成一个格式化时间// fileId 后续使用的时候进一步设置message.fileId ;// fileName 不使用, 直接设为 message.fileName ;return message;} };// /// 会话信息 // class ChatSessionInfo { public:QString chatSessionId ; // 会话编号QString chatSessionName ; // 会话名字, 如果会话是单聊, 名字就是对方的昵称; 如果是群聊, 名字就是群聊的名称.Message lastMessage; // 表示最新的消息.QIcon avatar; // 会话头像. 如果会话是单聊, 头像就是对方的头像; 如果是群聊, 头像群聊的头像.QString userId ; // 对于单聊来说, 表示对方的用户 id, 对于群聊设为 };3.2 工具函数的实现 1项目当中可能会在各个模块当中用到同一个功能函数所以将其存放在一起使用 // /// 工具函数. 后续很多模块可能都要用到 // static inline QString getFileName(const QString path) {QFileInfo fileInfo(path);return fileInfo.fileName(); }// 封装一个 宏 作为打印日志的方式. #define TAG QString([%1:%2]).arg(model::getFileName(FILE), QString::number(LINE)) // #define TAG [ LINE ]// qDebug 打印字符串的时候, 就会自动加上
    #define LOG() qDebug().noquote() TAG// 要求函数的定义如果写在 .h 中, 必须加 static 或者 inline (当然两个都加也可以), 避免链接阶段出现 函数重定义 的问题. static inline QString formatTime(int64_t timestamp) {// 先把时间戳, 转换成 QDateTime 对象QDateTime dateTime QDateTime::fromSecsSinceEpoch(timestamp);// 把 QDateTime 对象转成 格式化时间return dateTime.toString(MM-dd HH:mm:ss); }// 通过这个函数得到 秒级 的时间 static inline int64_t getTime() {return QDateTime::currentSecsSinceEpoch(); }// 根据 QByteArray, 转成 QIcon static inline QIcon makeIcon(const QByteArray byteArray) {QPixmap pixmap;pixmap.loadFromData(byteArray);QIcon icon(pixmap);return icon; }// 读写文件操作. // 从指定文件中, 读取所有的二进制内容. 得到一个 QByteArray static inline QByteArray loadFileToByteArray(const QString path) {QFile file(path);bool ok file.open(QFile::ReadOnly);if(ok false){LOG() 文件打开失败!;return QByteArray();}QByteArray content file.readAll();file.close();return content; }// 把 QByteArray 中的内容, 写入到某个指定文件里 static inline void writeByteArrayToFile(const QString path, const QByteArray content) {QFile file(path);bool ok file.open(QFile::WriteOnly);if(ok false){LOG() 文件打开失败!;return;}file.write(content);file.flush();file.close(); }3.3 创建编译开关 1创建debug.h的头文件 2具体实现的一些功能 #ifndef DEBUG_H #define DEBUG_H// 测试 UI , 显⽰构造的假数据 #define TEST_UI 0// 测试群组会话详情窗⼝ #define TEST_GROUP_SESSION_DETAIL 1// 测试跳过登录窗⼝ #define TEST_SKIP_LOGIN 0// 测试⽹络连通性 #define TEST_NETWORK 0// 从⽹络获取数据 #define LOAD_DATA_FROM_NETWORK 1// 是否连接测试服务器 #define CONNECT_TEST_SERVER 0#endif // DEBUG_H4. 界面整体布局的实现 4.1 主页面的布局和实现 1布局布局展示 2MainWidget.h的实现 class MainWidget : public QWidget {Q_OBJECTpublic:static MainWidget* getInstance();~MainWidget();public:void initMainWindow();void initLeftWindow();void initMidWindow();void initRightWindow();void initSignalSlot();void initWebsocket();void switchTabToSession();void switchTabToFriend();void switchTabToApply();void loadSessionList();void loadFriendList();void loadApplyList();void updateFriendList();void updateChatSessionList();void updateApplyList();void loadRecentMessage(const QString chatSessionId);void updateRecentMessage(const QString chatSessionId);// 点击好友项之后, 切换到会话列表的总的函数. 上方的 switchTabToSession 只是其中的一个环节.void switchSession(const QString userId);MessageShowArea* getMessageShowArea();private:// 对于单例模式来说, 最关键的部分, 不是 创建实例 , 而是限制别人创建实例.MainWidget(QWidget parent nullptr);static MainWidget instance;private:Ui::MainWidget ui;QWidget windowLeft; // 窗口最左侧部分QWidget* windowMid; // 窗口中间部分QWidget* windowRight;QPushButton* userAvatar; // 用户头像QPushButton* sessionTabBtn; // 会话标签页按钮QPushButton* friendTabBtn; // 好友标签页按钮QPushButton* applyTabBtn; // 好友申请标签页按钮QLineEdit* searchEdit; // 用户搜索框QPushButton* addFriendBtn; // 添加好友按钮QLabel* sessionTitleLabel; // 显示会话标题QPushButton* extraBtn; // 显示会话详情按钮SessionFriendArea* sessionFriendArea;MessageShowArea* messageShowArea; // 消息展示区MessageEditArea* messageEditArea; // 消息编辑区enum ActiveTab{SESSION_LIST,FRIEND_LIST,APPLY_LIST};ActiveTab activeTab SESSION_LIST; };3MainWidget.cpp的实现 MainWidget* MainWidget::instance nullptr;MainWidget* MainWidget::getInstance() {if(instance nullptr){// 此处不传入参数, 以桌面为父窗口.// 由于此处的窗口是整个程序的主窗口, 父窗口就设定为桌面, 本身就是常规设定.instance new MainWidget();}return instance; }MainWidget::~MainWidget() {delete ui; }MainWidget::MainWidget(QWidget parent): QWidget(parent), ui(new Ui::MainWidget) {ui-setupUi(this);this-setWindowTitle(我的微信);this-setWindowIcon(QIcon(:/resource/image/logo.png));initMainWindow(); // 初始化主窗口的样式布局initLeftWindow(); // 初始化左侧窗口布局initMidWindow(); // 初始化中间窗口布局initRightWindow(); // 初始化右侧窗口布局// 初始化信号槽initSignalSlot();// 初始化 websocketinitWebsocket(); }void MainWidget::initMainWindow() {QHBoxLayout layout new QHBoxLayout();// Spacing 就是 layout 内部元素之间的间隔距离. 设为 0 就是 紧挨着layout-setSpacing(0);// layout 里面的元素距离四个边界的距离.layout-setContentsMargins(0, 0, 0, 0);this-setLayout(layout);windowLeft new QWidget();windowMid new QWidget();windowRight new QWidget();windowLeft-setFixedWidth(70);windowMid-setFixedWidth(310);windowRight-setFixedWidth(800);windowLeft-setStyleSheet(QWidget { background-color: rgb(46, 46, 46); });windowMid-setStyleSheet(QWidget { background-color: rgb(247, 247, 247); });windowRight-setStyleSheet(QWidget { background-color: rgb(245, 245, 245); });layout-addWidget(windowLeft);layout-addWidget(windowMid);layout-addWidget(windowRight); }void MainWidget::initLeftWindow() {QVBoxLayout* layout new QVBoxLayout();layout-setSpacing(20);layout-setContentsMargins(0, 50, 0, 0);windowLeft-setLayout(layout);// 添加用户头像userAvatar new QPushButton();userAvatar-setFixedSize(45, 45);userAvatar-setIconSize(QSize(45, 45));// 把这个默认头像的代码干掉就可以避免头像的变化//userAvatar-setIcon(QIcon(:/resource/image/defaultAvatar.png));userAvatar-setStyleSheet(QPushButton { background-color: transparent; });layout-addWidget(userAvatar, 1, Qt::AlignTop | Qt::AlignHCenter);// 添加会话标签页按钮sessionTabBtn new QPushButton();sessionTabBtn-setFixedSize(45, 45);sessionTabBtn-setIconSize(QSize(30, 30));sessionTabBtn-setIcon(QIcon(:/resource/image/session_active.png));sessionTabBtn-setStyleSheet(QPushButton { background-color: transparent; });layout-addWidget(sessionTabBtn , 1, Qt::AlignTop | Qt::AlignHCenter);// 添加好友标签页按钮friendTabBtn new QPushButton();friendTabBtn-setFixedSize(45, 45);friendTabBtn-setIconSize(QSize(30, 30));friendTabBtn-setIcon(QIcon(:/resource/image/friend_inactive.png));friendTabBtn-setStyleSheet(QPushButton { background-color: transparent; });layout-addWidget(friendTabBtn , 1, Qt::AlignTop | Qt::AlignHCenter);// 添加好友申请标签页按钮applyTabBtn new QPushButton();applyTabBtn-setFixedSize(45, 45);applyTabBtn-setIconSize(QSize(30, 30));applyTabBtn-setIcon(QIcon(:/resource/image/apply_inactive.png));applyTabBtn-setStyleSheet(QPushButton { background-color: transparent; });layout-addWidget(applyTabBtn, 1, Qt::AlignTop | Qt::AlignHCenter);layout-addStretch(20); }4.2 主界面左侧页面各个功能的实现 1在MainWidget::initSignalSlot当中添加左侧按钮的信号槽申请按钮、好友列表按钮、会话按钮 / /// 连接信号槽, 处理标签页按钮切换的问题 / connect(sessionTabBtn, QPushButton::clicked, this, MainWidget::switchTabToSession); connect(friendTabBtn, QPushButton::clicked, this, MainWidget::switchTabToFriend); connect(applyTabBtn, QPushButton::clicked, this, MainWidget::switchTabToApply);2槽函数的实现 void MainWidget::switchTabToSession() {// 1. 记录当前切换到了哪个标签页activeTab SESSION_LIST;// 2. 调整图标显示情况, 把会话的按钮图标设为 active, 另外两个图标设为 inactive.sessionTabBtn-setIcon(QIcon(:/resource/image/session_active.png));friendTabBtn-setIcon(QIcon(:/resource/image/friend_inactive.png));applyTabBtn-setIcon(QIcon(:/resource/image/apply_inactive.png));// 3. 在主窗口的中间部分, 加载出会话列表数据this-loadSessionList(); }void MainWidget::switchTabToFriend() {// 1. 记录当前切换到了哪个标签页activeTab FRIEND_LIST;// 2. 调整图标显示情况, 把会话的按钮图标设为 active, 另外两个图标设为 inactive.friendTabBtn-setIcon(QIcon(:/resource/image/friend_active.png));sessionTabBtn-setIcon(QIcon(:/resource/image/session_inactive.png));applyTabBtn-setIcon(QIcon(:/resource/image/apply_inactive.png));// 3. 在主窗口的中间部分, 加载出会话列表数据this-loadFriendList(); }void MainWidget::switchTabToApply() {// 1. 记录当前切换到了哪个标签页activeTab APPLY_LIST;// 2. 调整图标显示情况, 把会话的按钮图标设为 active, 另外两个图标设为 inactive.applyTabBtn-setIcon(QIcon(:/resource/image/apply_active.png));sessionTabBtn-setIcon(QIcon(:/resource/image/session_inactive.png));friendTabBtn-setIcon(QIcon(:/resource/image/friend_inactive.png));// 3. 在主窗口的中间部分, 加载出会话列表数据this-loadApplyList(); }4.3 主界面中间页面各个功能的实现 1创建中间上方的搜索框和搜索按钮 void MainWidget::initMidWindow() {QGridLayout* layout new QGridLayout();// 距离上方有 20px 的距离, 另外三个方向都不要边距layout-setContentsMargins(0, 20, 0, 0);layout-setHorizontalSpacing(0);layout-setVerticalSpacing(10);windowMid-setLayout(layout);searchEdit new QLineEdit();searchEdit-setFixedHeight(30);searchEdit-setPlaceholderText(搜索);searchEdit-setStyleSheet(QLineEdit { border-radius: 5px; background-color: rgb(226, 226, 226); padding-left: 5px;});addFriendBtn new QPushButton();addFriendBtn-setFixedSize(30, 30);addFriendBtn-setIcon(QIcon(:/resource/image/cross.png));QString style QPushButton { border-radius: 5px; background-color: rgb(226, 226, 226); };style QPushButton:pressed { background-color: rgb(240, 240, 240); };addFriendBtn-setStyleSheet(style);sessionFriendArea new SessionFriendArea();// 为了更灵活的控制边距, 只影响搜索框按钮这一行, 不影响下方列表这一行// 创建空白的 widget 填充到布局管理器中.QWidget* spacer1 new QWidget();spacer1-setFixedWidth(10);QWidget* spacer2 new QWidget();spacer2-setFixedWidth(10);QWidget* spacer3 new QWidget();spacer3-setFixedWidth(10);layout-addWidget(spacer1, 0, 0);layout-addWidget(searchEdit, 0, 1);layout-addWidget(spacer2, 0, 2);layout-addWidget(addFriendBtn, 0, 3);layout-addWidget(spacer3, 0, 4);layout-addWidget(sessionFriendArea, 1, 0, 1, 5); }2实现中间的会话列表、消息列表、好友申请列表。需要创建SessionFriendArea类来实现此功能 // /// 整个滚动区域的实现 // class SessionFriendArea : public QScrollArea {Q_OBJECT public:explicit SessionFriendArea(QScrollArea parent nullptr);// 清空该区域中所有的 itemvoid clear();// 添加一个 item 到该区域中, itemType 表示添加哪种 item, id 跟着不同的 itemType 有不同的含义.// 如果是 SessionItem, id 就是 chatSessionId// 如果是 FriendItem / ApplyItem, id 就是 userIdvoid addItem(ItemType itemType, const QString id, const QIcon avatar, const QString name, const QString text);// 选中某个指定的 item, 通过 index 下标来进行选择void clickItem(int index);private:QWidget container; };SessionFriendArea类的具体实现 SessionFriendArea::SessionFriendArea(QScrollArea parent): QScrollArea{parent} {// 1. 设置必要的属性// 设置了这个属性, 才能够开启滚动效果this-setWidgetResizable(true);// 设置滚动条相关的样式this-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(46, 46, 46);});this-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal { height: 0px; });this-setStyleSheet(QWidget { border: none;});// 2. 把 widget 创建出来container new QWidget();this-setFixedWidth(310);this-setWidget(container);// 3. 给这个 widget 指定布局管理器, 以便后续添加元素进去QVBoxLayout layout new QVBoxLayout();layout-setContentsMargins(0, 0, 0, 0);layout-setSpacing(0);layout-setAlignment(Qt::AlignTop);container-setLayout(layout);// 构造出一些临时数据, 用来作为 界面调试 依据. 后续要删除掉 #if TEST_UIQIcon icon(:/resource/image/defaultAvatar.png);for (int i 0; i 30; i){this-addItem(ApplyItemType, QString::number(i), icon, 张三 QString::number(i), 最后一条消息 QString::number(i));} #endif }void SessionFriendArea::clear() {QLayout* layout container-layout();// 遍历布局管理器中的所有元素, 并依次从布局管理器中删除掉for(int i layout-count() - 1; i 0; –i){// takeAt 就能移除对应下标的元素QLayoutItem* item layout-takeAt(i);// 别忘了, 还需要对这个对象进行 释放if(item-widget()){// 把这个移除的内容的 widget 进行释放.// 正常使用的时候, new 出来的对象添加到布局管理器的….delete item-widget();}} }// 此时这个函数添加的就不是 SessionFriendItem 了, 而是 SessionFriendItem 的子类. // SessionItem, FriendItem, ApplyItem 其中的一个. void SessionFriendArea::addItem(ItemType itemType, const QString id, const QIcon avatar, const QString name, const QString text) {SessionFriendItem* item nullptr;if(itemType SessionItemType){item new SessionItem(this, id, avatar, name, text);}else if(itemType FriendItemType){item new FriendItem(this, id, avatar, name, text);}else if(itemType ApplyItemType){item new ApplyItem(this, id, avatar, name);}else{LOG() 错误的 ItemType! itemType itemType;return;}container-layout()-addWidget(item); }void SessionFriendArea::clickItem(int index) {if(index 0 || index container-layout()-count()){LOG() 点击元素的下标超出范围! index index;return;}QLayoutItem* layoutItem container-layout()-itemAt(index);if(layoutItem nullptr || layoutItem-widget() nullptr){LOG() 指定的元素不存在! index index;return;}SessionFriendItem* item dynamic_castSessionFriendItem(layoutItem-widget());item-select(); }3在与SessionFriendArea类同一个头文件当中创建SessionFriendItem类来实现列表的展示会话、好友、好友申请列表的基类 // /// 滚动区域中的 Item 的实现 // class SessionFriendItem : public QWidget {Q_OBJECTpublic:SessionFriendItem(QWidget owner, const QIcon avatar, const QString name, const QString text);void paintEvent(QPaintEvent* event) override;void mousePressEvent(QMouseEvent* event) override;void enterEvent(QEnterEvent* event) override;void leaveEvent(QEvent* event) override;void select();// active 函数期望实现 Item 被点击之后的业务逻辑.virtual void active();private:// owner 就指向了上述的 SessionFriendAreaQWidget* _owner;// 这个变量用来表示当前 Item 是否是 选中 状态bool selected false;protected:// 让这个成员被子类访问QLabel* messageLabel; };样式布局 SessionFriendItem::SessionFriendItem(QWidget* owner, const QIcon avatar, const QString name, const QString text):_owner(owner) {this-setFixedHeight(70);this-setStyleSheet(QWidget { background-color: rgb(231, 231, 231); });// 创建网格布局管理器QGridLayout* layout new QGridLayout();layout-setContentsMargins(20, 0, 0, 0);layout-setHorizontalSpacing(10);layout-setVerticalSpacing(0);this-setLayout(layout);// 创建头像QPushButton* avatarBtn new QPushButton();avatarBtn-setFixedSize(50, 50);avatarBtn-setIconSize(QSize(50, 50));avatarBtn-setIcon(avatar);avatarBtn-setStyleSheet(QPushButton {border: none;});avatarBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);// 创建名字QLabel* nameLabel new QLabel();nameLabel-setText(name);nameLabel-setStyleSheet(QLabel { font-size: 18px; font-weight: 600; });nameLabel-setFixedHeight(35);nameLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 创建消息预览的 labelmessageLabel new QLabel();messageLabel-setText(text);messageLabel-setFixedHeight(35);messageLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 头像处于 0, 0 位置, 占据 2 行, 占据 2 列layout-addWidget(avatarBtn, 0, 0, 2, 2);// 名字处于 0, 2 位置, 占据 1 行, 占据 1 列layout-addWidget(nameLabel, 0, 2, 1, 8);// 消息预览处于 1, 2 位置, 占据 1 行, 占据 1 列layout-addWidget(messageLabel, 1, 2, 1, 8); }实现鼠标悬停/选中元素效果 void SessionFriendItem::paintEvent(QPaintEvent* event) {(void)event;QStyleOption opt;opt.initFrom(this);QPainter p(this);style()-drawPrimitive(QStyle::PE_Widget, opt, p, this); }void SessionFriendItem::mousePressEvent(QMouseEvent* event) {(void)event;select(); }void SessionFriendItem::enterEvent(QEnterEvent* event) {(void)event;if(this-selected){return;}// 设置一个更深的颜色this-setStyleSheet(QWidget { background-color: rgb(215, 215, 215);}); }void SessionFriendItem::leaveEvent(QEvent* event) {(void)event;if(this-selected){return;}// 还原背景色this-setStyleSheet(QWidget { background-color: rgb(231, 231, 231);}); }void SessionFriendItem::select() {// 鼠标点击时会触发这个函数.// 拿到所有的兄弟元素const QObjectList children this-parentWidget()-children();for(QObject* child : children){if(!child-isWidgetType()){// 判定是否是 widget.continue;}SessionFriendItem* item dynamic_castSessionFriendItem(child);if(item-selected){item-selected false;item-setStyleSheet(QWidget { background-color: rgb(231, 231, 231); });}}// 点击时, 修改背景色.// 此处不仅仅要设置当前 item 背景色, 也要还原其他元素的背景色.this-setStyleSheet(QWidget { background-color: rgb(210, 210, 210); });this-selected true;// 调用 activethis-active(); }void SessionFriendItem::active() {// 父类的 active// 并不需要实现任何逻辑. }4创建列表元素 - 聊天会话。创建SessionItem类来继承SessionFriendItem类 enum ItemType {SessionItemType,FriendItemType,ApplyItemType };// /// 会话 Item 的实现 // class SessionItem : public SessionFriendItem {Q_OBJECTpublic:SessionItem(QWidget owner, const QString chatSessionId, const QIcon avatar,const QString name, const QString lastMessage);void active() override;void updateLastMessage(const QString chatSessionId);private:QString chatSessionId; // 当前会话 idQString text; // 最后一条消息的文本预览};SessionItem类功能的具体实现 SessionItem::SessionItem(QWidget* owner, const QString chatSessionId, const QIcon avatar,const QString name, const QString lastMessage):SessionFriendItem(owner, avatar, name, lastMessage),chatSessionId(chatSessionId),text(lastMessage) {}void SessionItem::active() {LOG() SessionItem active. chatSessionId chatSessionId;// TODO }void SessionItem::updateLastMessage(const QString chatSessionId) {// TODO }5创建列表元素 - 好友会话。创建FriendItem类来继承SessionFriendItem类 // /// 好友 Item 的实现 // class FriendItem : public SessionFriendItem {Q_OBJECTpublic:FriendItem(QWidget* owner, const QString userId, const QIcon avatar,const QString name, const QString description);void active() override;private:QString userId; // 好友的用户id};FriendItem类的具体实现 FriendItem::FriendItem(QWidget* owner, const QString userId, const QIcon avatar,const QString name, const QString description):SessionFriendItem(owner, avatar, name, description),userId(userId) {}void FriendItem::active() {// 点击之后, 要激活对应的会话列表元素LOG() 点击 FriendItem 触发的逻辑! userId userId;// TODO }5创建列表元素 - 好友申请会话。创建ApplyItem类来继承SessionFriendItem类 // /// 好友申请 Item 的实现 // class ApplyItem : public SessionFriendItem {Q_OBJECTpublic:// 此处不需要显示一个 附加的文本了. 比上面的两个 Item 的构造函数, 少了一个参数ApplyItem(QWidget* owner, const QString userId, const QIcon avatar, const QString name);void active() override;private:QString userId; // 申请人的 userId};ApplyItem类的具体实现 // /// 好友申请 Item 的实现 //ApplyItem::ApplyItem(QWidget* owner, const QString userId, const QIcon avatar, const QString name):SessionFriendItem(owner, avatar, name, ),userId(userId) {// 1. 移除父类的 messageLabelQGridLayout* layout dynamic_castQGridLayout(this-layout());layout-removeWidget(messageLabel);// 要记得释放内存, 否则会内存泄露.delete messageLabel;// 2.创建两个按钮出来QPushButton acceptBtn new QPushButton();acceptBtn-setText(同意);QPushButton* rejectBtn new QPushButton();rejectBtn-setText(拒绝);// 3. 添加到布局管理器中layout-addWidget(acceptBtn, 1, 2, 1, 1);layout-addWidget(rejectBtn, 1, 3, 1, 1); }void ApplyItem::active() {// 这个函数本身就不需要实现任何内容LOG() 点击 ApplyItem 触发的逻辑! userId userId; }4.4 主界面右侧页面各个功能的实现 1界面效果 点击右上角 … 按钮打开新的窗口显示这个消息会话的详情。点击用户头像打开新窗口显示用户详细信息。左侧下方四个按钮分别是发送图片、发送文件、发送语音、查看历史消息 (打开新的窗口)。 4.4.1 实现会话标题栏 1回到MainWidget的实现 void MainWidget::initRightWindow() {// 1. 创建右侧窗口的布局管理器QVBoxLayout* vlayout new QVBoxLayout();vlayout-setSpacing(0);vlayout-setContentsMargins(0, 0, 0, 0);vlayout-setAlignment(Qt::AlignTop);windowRight-setLayout(vlayout);// 2. 创建上方标题栏QWidget* titleWidget new QWidget();titleWidget-setFixedHeight(62);titleWidget-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);titleWidget-setObjectName(titleWidget);titleWidget-setStyleSheet(#titleWidget { border-bottom: 1px solid rgb(230, 230, 230); border-left: 1px solid rgb(230, 230, 230); });vlayout-addWidget(titleWidget);// 3. 给标题栏, 添加标题 label 和 一个按钮QHBoxLayout* hlayout new QHBoxLayout();hlayout-setSpacing(0);// 使标题的 label 和 按钮距离左右两侧的边界, 有点间距.hlayout-setContentsMargins(10, 0, 10, 0);titleWidget-setLayout(hlayout);sessionTitleLabel new QLabel();sessionTitleLabel-setStyleSheet(QLabel { font-size: 22px; border-bottom: 1px solid rgb(230, 230, 230);});// 为了测试界面临时增加的. 实际这里的内容, 应该是使用从服务器获取的数据来设置.sessionTitleLabel-setText(会话标题);hlayout-addWidget(sessionTitleLabel);extraBtn new QPushButton();extraBtn-setFixedSize(30, 30);extraBtn-setIconSize(QSize(30, 30));extraBtn-setIcon(QIcon(:/resource/image/more.png));extraBtn-setStyleSheet(QPushButton { border:none; background-color: rgb(245, 245, 245); } QPushButton:pressed { background-color: rgb(220, 220, 220); });hlayout-addWidget(extraBtn);// 4. 添加消息展示区messageShowArea new MessageShowArea();vlayout-addWidget(messageShowArea);// 5. 添加消息编辑区messageEditArea new MessageEditArea();// 确保消息编辑区, 处于窗口的下方.vlayout-addWidget(messageEditArea, 0, Qt::AlignBottom); }4.4.2 实现消息展示区域 1创建 MessageShowArea类来实现消息展示区 /// 表示消息展示区class MessageShowArea : public QScrollArea {Q_OBJECT public:MessageShowArea();// 尾插void addMessage(bool isLeft, const Message message);// 头插void addFrontMessage(bool isLeft, const Message message);// 清空消息void clear();// 滚动到末尾void scrollToEnd();private:QWidget* container; };MessageShowArea类的具体实现 MessageShowArea::MessageShowArea() {// 1. 初始化基本属性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; });// 2. 创建 Container 这样的 widget, 作为包含内部元素的容器container new QWidget();this-setWidget(container);// 3. 给 container 添加布局管理器QVBoxLayout* layout new QVBoxLayout();layout-setSpacing(0);layout-setContentsMargins(0, 0, 0, 0);container-setLayout(layout);// 添加 构造测试数据 逻辑. #if TEST_UImodel::UserInfo userInfo;userInfo.userId QString::number(1000);userInfo.nickname 张三;userInfo.description 从今天开始认真敲代码;userInfo.avatar QIcon(:/resource/image/defaultAvatar.png);userInfo.phone 18612345678;Message message Message::makeMessage(model::TEXT_TYPE, , userInfo, QString(这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息这是一条测试消息).toUtf8(), );this-addMessage(false, message);for (int i 1; i 30; i){model::UserInfo userInfo;userInfo.userId QString::number(1000 i);userInfo.nickname 张三 QString::number(i);userInfo.description 从今天开始认真敲代码;userInfo.avatar QIcon(:/resource/image/defaultAvatar.png);userInfo.phone 18612345678;Message message Message::makeMessage(model::TEXT_TYPE, , userInfo, (QString(这是一条测试消息) QString::number(i)).toUtf8(), );this-addMessage(true, message);} #endif }void MessageShowArea::addMessage(bool isLeft, const Message message) {// 构造 MessageItem, 添加到布局管理器中.MessageItem* messageItem MessageItem::makeMessageItem(isLeft, message);container-layout()-addWidget(messageItem); }void MessageShowArea::addFrontMessage(bool isLeft, const Message message) {MessageItem* messageItem MessageItem::makeMessageItem(isLeft, message);QVBoxLayout* layout dynamic_castQVBoxLayout(container-layout());layout-insertWidget(0, 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); }2创建消息对象MessageItem类来展示在MessageShowArea类当中 /// 表示一个消息元素 /// 这个里面要能同时支持 文本消息, 图片消息, 文件消息, 语音消息. /// 当前先只考虑文本消息. 另外几个后续慢慢添加.class MessageItem : public QWidget {Q_OBJECTpublic:// 此处的 isLeft 表示这个 Item 是否是一个 左侧消息MessageItem(bool isleft);// 通过 工厂方法 创建 MessageItem 实例static MessageItem* makeMessageItem(bool isLeft, const Message message);// 添加工厂函数static QWidget* makeTextMessageItem(bool isLeft, const QString text);static QWidget* makeImageMessageItem(bool isLeft, const QString fileId, const QByteArray content);static QWidget* makeFileMessageItem(bool isLeft, const Message message);static QWidget* makeSpeechMessageItem(bool isLeft, const Message message);private:bool isleft; };MessageItem类的具体实现 /// 表示一个消息元素 MessageItem::MessageItem(bool isleft):isleft(isleft) {}MessageItem* MessageItem::makeMessageItem(bool isLeft, const Message message) {// 1. 创建对象和布局管理器MessageItem* messageItem new MessageItem(isLeft);QGridLayout* layout new QGridLayout();layout-setContentsMargins(30, 10, 40, 0);layout-setSpacing(10);// 这个 MessageItem 最低不能低于 100messageItem-setMinimumHeight(100);messageItem-setLayout(layout);// 2. 创建头像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::AlignTop | Qt::AlignLeft);}else{layout-addWidget(avatarBtn, 0, 1, 2, 1, Qt::AlignTop | Qt::AlignLeft);}// 3. 创建名字和时间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);}// 4. 创建消息体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() 错误的消息类型! messageType message.messageType;}if (isLeft){layout-addWidget(contentWidget, 1, 1);}else{layout-addWidget(contentWidget, 1, 0);}// 5. 连接信号槽, 处理用户点击头像的操作connect(avatarBtn, QPushButton::clicked, messageItem, {MainWidget* mainwidget MainWidget::getInstance();UserInfoWidget* userinfowidget new UserInfoWidget(message.sender, mainwidget);userinfowidget-exec();});// 6. 当用户修改了昵称的时候, 同步修改此处的用户昵称.if(!isLeft){model::DataCenter* dataCenter model::DataCenter::getInstance();connect(dataCenter, model::DataCenter::changeNicknameDone, messageItem, {nameLabel-setText(dataCenter-getMyself()-nickname | message.time);});connect(dataCenter, model::DataCenter::changeAvatarDone, messageItem, {UserInfo* myself dataCenter-getMyself();avatarBtn-setIcon(myself-avatar);});}return messageItem; }QWidget* MessageItem::makeTextMessageItem(bool isLeft, const QString text) {MessageContentLabel* messageContentLabel new MessageContentLabel(text, isLeft, model::MessageType::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; }3创建文本消息MessageContentLabel类同时也可以用作显示文件消息和语音消息 class MessageContentLabel : public QWidget {Q_OBJECTpublic:MessageContentLabel(const QString text, bool isLeft, model::MessageType messageType, const QString fileId,const QByteArray content);void paintEvent(QPaintEvent* event) override;void mousePressEvent(QMouseEvent* event) override;void updateUI(const QString fileId, const QByteArray fileContent);void saveAsFile(const QByteArray content);void playDone();void contextMenuEvent(QContextMenuEvent* event) override;void speechConvertTextDone(const QString fileId, const QString text);private:QLabel* label;bool isLeft;model::MessageType messageType;QString fileId;QByteArray content;bool loadContentDone false;};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) {// 设置一下 SizePolicythis-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::AlignVCenter | 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()){model::DataCenter* dataCenter model::DataCenter::getInstance();connect(dataCenter, model::DataCenter::getSingleFileDone, this, MessageContentLabel::updateUI);dataCenter-getSingleFileAsync(this-fileId);}else{// content 不为空, 说明当前的这个数据就是已经现成. 直接就把 表示加载状态的变量设为 truethis-loadContentDone true;} }// 这个函数会该控件被显示的时候自动调用到. void MessageContentLabel::paintEvent(QPaintEvent* event) {(void)event;// 1. 获取到父元素的宽度QObject* object this-parent();if(!object-isWidgetType()){// 当前这个对象的父元素不是预期的 QWidget, 此时不需要进行任何后续的绘制操作.return;}QWidget* parent dynamic_castQWidget*(object);int width parent-width() * 0.6;// 2. 计算当前文本, 如果是一行放置, 需要多宽.QFontMetrics metrics(this-label-font());int totalWidth metrics.horizontalAdvance(this-label-text());// 3. 计算出此处的行数是多少 (40 表示左右各有 20px 的边距)int rows (totalWidth / (width - 40)) 1;if(rows 1){// 如果此时得到的行数就只有一行width totalWidth 40;}// 4. 根据行数, 计算得到高度. (20 表示上下各有 10px 的边距)int height rows * (this-label-font().pixelSize() * 1.2 ) 20;// 5. 绘制圆角矩形和箭头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();painter.drawPath(path);this-label-setGeometry(leftPos, 0, width, height);}// 6. 重新设置父元素的高度, 确保父元素足够高, 能够容纳下上述绘制的消息显示的区域// 注意高度要涵盖之前名字和时间的 label 的高度, 以及留点冗余空间.parent-setFixedHeight(height 50); }void MessageContentLabel::mousePressEvent(QMouseEvent* event) {// 实现鼠标点击之后, 触发文件另存为if(event-button() Qt::LeftButton){if(this-messageType model::MessageType::FILE_TYPE){// 真正触发另存为if(!this-loadContentDone){Toast::showMessage(数据尚未加载成功, 请稍后重试);return;}saveAsFile(this-content);}else if(this-messageType model::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);}} }void MessageContentLabel::updateUI(const QString fileId, const QByteArray fileContent) {// 也和刚才图片消息的处理一样, 就需要判定收到的数据属于哪个 fileId 的.if(fileId ! this-fileId){return;}this-content fileContent;this-loadContentDone true;// 对于文件消息来说, 要在界面上显示 [文件] test.txt 这样形式. 这个内容和文件 content 无关.// 在从服务器拿到文件正文之前, 界面内容应该就是绘制好了. 此时拿到正文之后, 界面应该也不必做出任何实质性的调整.// 所以下列的 this-update(), 没有也行.this-update(); }void MessageContentLabel::saveAsFile(const QByteArray content) {// 弹出对话框, 让用户选择路径QString filePath QFileDialog::getSaveFileName(this, 另存为, QDir::homePath(), *);if(filePath.isEmpty()){LOG() 用户取消了文件另存为;return;}model::writeByteArrayToFile(filePath, content); }void MessageContentLabel::playDone() {if(this-label-text() 播放中…){this-label-setText([语音]);} }void MessageContentLabel::contextMenuEvent(QContextMenuEvent event) {(void) event;if (messageType ! model::MessageType::SPEECH_TYPE){LOG() 非语音消息暂时不支持右键菜单;return;}QMenu menu new QMenu(this);QAction* action menu-addAction(语音转文字);menu-setStyleSheet(QMenu { color: rgb(0, 0, 0); });connect(action, QAction::triggered, this, {model::DataCenter* dataCenter model::DataCenter::getInstance();connect(dataCenter, model::DataCenter::speechConvertTextDone, this, MessageContentLabel::speechConvertTextDone, Qt::UniqueConnection);dataCenter-speechConvertTextAsync(this-fileId, this-content);});// 此处弹出 模态对话框 显示菜单/菜单项. exec 会在用户进一步操作之前, 阻塞.menu-exec(event-globalPos());delete menu; }void MessageContentLabel::speechConvertTextDone(const QString fileId, const QString text) {if(this-fileId ! fileId){// 直接跳过, 此时的结果不是针对这一条语音消息的结果.return;}// 修改界面内容this-label-setText([语音转文字] text);this-update(); }4创建一个MessageImageLabel类来表示图片消息 /// 创建类表示 图片消息 正文部分class MessageImageLabel : public QWidget {Q_OBJECTpublic:MessageImageLabel(const QString fileId, const QByteArray content, bool isLeft);void updateUI(const QString fileId, const QByteArray content);void paintEvent(QPaintEvent* event);private:QPushButton* imageBtn;QString fileId; // 该图片在服务器对应的文件 id.QByteArray content; // 图片的二进制数据bool isLeft; };MessageImageLabel类的具体实现 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()){// 此处这个控件, 是针对 从服务器拿到图片消息 这种情况.// 拿着 fileId, 去服务器获取图片内容model::DataCenter* dataCenter model::DataCenter::getInstance();connect(dataCenter, model::DataCenter::getSingleFileDone, this, MessageImageLabel::updateUI);dataCenter-getSingleFileAsync(fileId);} }void MessageImageLabel::updateUI(const QString fileId, const QByteArray content) {if(this-fileId ! fileId){// 没对上 fileId, 当前响应的图片是其他的 图片消息 请求的.return;}// 对上了, 真正显示图片内容this-content content;// 进行绘制图片到界面上的操作.this-update(); }void MessageImageLabel::paintEvent(QPaintEvent* event) {(void)event;// 1. 先拿到该元素的父元素, 看父元素的宽度是多少.// 此处显示的图片宽度的上限 父元素宽度的 60% .QObject* object this-parent();if(!object-isWidgetType()){// 这个逻辑理论上来说是不会存在的.return;}QWidget* parent dynamic_castQWidget*(object);int width parent-width() * 0.6;// 2. 加载二进制数据为图片对象QImage image;if(content.isEmpty()){// 此时图片的响应数据还没回来.// 此处先拿一个 固定默认图片 顶替一下.QByteArray tmpContent model::loadFileToByteArray(:/resource/image/image.png);image.loadFromData(tmpContent);}else{// 此处的 load 操作 QImage 能够自动识别当前图片是啥类型的 (png, jpg….)image.loadFromData(content);}// 3. 针对图片进行缩放.int height 0;if(image.width() width){// 发现图片更宽, 就需要把图片缩放一下, 使用 width 作为实际的宽度// 等比例缩放.height ((double)image.height() / image.width()) * width;}else{// 图片本身不太宽, 不需要缩放.width image.width();height image.height();}// pixmap 只是一个中间变量. QImage 不能直接转成 QIcon, 需要 QPixmap 中转一下QPixmap pixmap QPixmap::fromImage(image);// imageBtn-setFixedSize(width, height);imageBtn-setIconSize(QSize(width, height));imageBtn-setIcon(QIcon(pixmap));// 4. 由于图片高度是计算算出来的. 该元素的父对象的高度, 能够容纳下当前的元素.// 此处 50 是为了能够容纳下 上方的 名字 部分. 同时留下一点 冗余 空间.parent-setFixedHeight(height 50);// 5. 确定按钮所在的位置.// 左侧消息, 和右侧消息, 要显示的位置是不同的.if(isLeft){imageBtn-setGeometry(10, 0, width, height);}else{int leftPos this-width() - width - 10;imageBtn-setGeometry(leftPos, 0, width, height);} }4.4.3 实现消息编辑区域 1创建MessageEditArea类来实现消息编辑区 class MessageEditArea : public QWidget {Q_OBJECT public:explicit MessageEditArea(QWidget parent nullptr);private:QPushButton sendImageBtn; // 发送图⽚消息QPushButton* sendFileBtn; // 发送⽂件消息QPushButton* sendSpeechBtn; // 发送语⾳按钮QPushButton* showHistoryBtn; // 显⽰历史消息按钮QPlainTextEdit* textEdit; // 消息输⼊框QPushButton* sendTextBtn; // 发送消息按钮QLabel* tipLabel; // 提⽰信息 labelsignals: };MessageEditArea类的具体实现 MessageEditArea::MessageEditArea(QWidget parent): QWidget{parent} {// 1. 设置必要的属性this-setFixedHeight(200);this-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 2. 创建垂直方向的布局管理器QVBoxLayout vlayout new QVBoxLayout();vlayout-setSpacing(0);vlayout-setContentsMargins(3, 2, 10, 10);this-setLayout(vlayout);// 3. 创建水平方向的布局管理器QHBoxLayout* hlayout new QHBoxLayout();hlayout-setSpacing(0);hlayout-setContentsMargins(10, 0, 0, 0);hlayout-setAlignment(Qt::AlignLeft | Qt::AlignTop);vlayout-addLayout(hlayout);// 4. 把上方的四个按钮, 创建好并添加到水平布局中QString btnStyle QPushButton { background-color: rgb(245, 245, 245); border: none; } QPushButton:pressed { background-color: rgb(255, 255, 255); };QSize btnSize(35, 35);QSize iconSize(25, 25);sendImageBtn new QPushButton();sendImageBtn-setFixedSize(btnSize);sendImageBtn-setIconSize(iconSize);sendImageBtn-setIcon(QIcon(:/resource/image/image.png));sendImageBtn-setStyleSheet(btnStyle);hlayout-addWidget(sendImageBtn);sendFileBtn new QPushButton();sendFileBtn-setFixedSize(btnSize);sendFileBtn-setIconSize(iconSize);sendFileBtn-setIcon(QIcon(:/resource/image/file.png));sendFileBtn-setStyleSheet(btnStyle);hlayout-addWidget(sendFileBtn);sendSpeechBtn new QPushButton();sendSpeechBtn-setFixedSize(btnSize);sendSpeechBtn-setIconSize(iconSize);sendSpeechBtn-setIcon(QIcon(:/resource/image/sound.png));sendSpeechBtn-setStyleSheet(btnStyle);hlayout-addWidget(sendSpeechBtn);showHistoryBtn new QPushButton();showHistoryBtn-setFixedSize(btnSize);showHistoryBtn-setIconSize(iconSize);showHistoryBtn-setIcon(QIcon(:/resource/image/history.png));showHistoryBtn-setStyleSheet(btnStyle);hlayout-addWidget(showHistoryBtn);// 5. 添加多行编辑框textEdit new QPlainTextEdit();textEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);textEdit-setStyleSheet(QPlainTextEdit { border: none; background-color: transparent; font-size: 14px; padding: 10px; });textEdit-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(45, 45, 45); });vlayout-addWidget(textEdit);// 6. 添加提示 录制中 这样的 QLabeltipLabel new QLabel();tipLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);tipLabel-setText(录音中…);tipLabel-setAlignment(Qt::AlignCenter);tipLabel-setFont(QFont(微软雅黑, 24, 600));vlayout-addWidget(tipLabel);tipLabel-hide();// 7. 添加发送文本消息的按钮sendTextBtn new QPushButton();sendTextBtn-setText(发送);sendTextBtn-setFixedSize(120, 40);QString style QPushButton { font-size: 16px; color: rgb(7, 193, 96); border: none; background-color: rgb(233, 233, 233); border-radius: 10px; } ;style QPushButton:hover { background-color: rgb(210, 210, 210); };style QPushButton:pressed { background-color: rgb(190, 190, 190); };sendTextBtn-setStyleSheet(style);vlayout-addWidget(sendTextBtn, 0, Qt::AlignRight | Qt::AlignVCenter); }5. 实现主界面各个按钮的点击功能 5.1 实现个人信息详细界面 1实现点击自己的头像弹出对话框显示个人主页。我们需要重新创建一个新的类SelfInfoWidget来实现此功能 2个人主页的主要界面如下 3selfinfowidget.h的实现 class SelfInfoWidget : public QDialog {Q_OBJECT public:SelfInfoWidget(QWidget* parent);private:QGridLayout* layout;QPushButton* avatarBtn;QLabel* idTag; // 显示 序号QLabel* idLabel; // 显示 1234QLabel* nameTag; // 显示 昵称QLabel* nameLabel; // 显示 张三QLineEdit* nameEdit; // 编辑昵称QPushButton* nameModifyBtn; // 修改名字QPushButton* nameSubmitBtn; // 提交修改QLabel* descTag; // 显示 签名QLabel* descLabel; // 显示 从今天开始认真敲代码QLineEdit* descEdit; // 编辑签名QPushButton* descModifyBtn; // 修改签名QPushButton* descSubmitBtn; // 提交修改QLabel* phoneTag; // 显示 电话QLabel* phoneLabel; // 显示 18612345678QLineEdit* phoneEdit; // 编辑电话QPushButton* phoneModifyBtn; // 修改电话QPushButton* phoneSubmitBtn; // 提交修改QLabel* verifyCodeTag; // 显示 验证码QLineEdit* verifyCodeEdit; // 输入验证码QPushButton* getVerifyCodeBtn; // 获取验证码按钮// 要修改的新的手机号码QString phoneToChange;// 倒计时的时间int leftTime 30;};4selfinfowidget.cpp的实现 SelfInfoWidget::SelfInfoWidget(QWidget* parent):QDialog(parent) {// 1. 设置整个窗口的属性this-setFixedSize(500, 250);this-setWindowTitle(个人信息);this-setWindowIcon(QIcon(:/resource/image/logo.png));// 窗口被关闭时, 自动销毁这个对话框对象.this-setAttribute(Qt::WA_DeleteOnClose);// 把窗口移动到鼠标当前的位置this-move(QCursor::pos());// 2. 创建布局管理器layout new QGridLayout();// layout-setSpacing(0);layout-setHorizontalSpacing(10);layout-setVerticalSpacing(3);layout-setContentsMargins(20, 20, 20, 0);layout-setAlignment(Qt::AlignTop);this-setLayout(layout);// 3. 创建头像avatarBtn new QPushButton();avatarBtn-setFixedSize(75, 75);avatarBtn-setIconSize(QSize(75, 75));avatarBtn-setStyleSheet(QPushButton { border: none; background-color: transparent; });layout-addWidget(avatarBtn, 0, 0, 3, 1);QString labelStyle QLabel { font-size: 14px; font-weight: 800; };QString btnStyle QPushButton { border: none; background-color: transparent; };btnStyle QPushButton:pressed { background-color: rgb(210, 210, 210); };QString editStyle QLineEdit { border: none; border-radius:5px; padding-left:2px; };int height 30;// 4. 添加用户的 id 的显示idTag new QLabel();idTag-setFixedSize(50, height);idTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);idTag-setText(序号);idTag-setStyleSheet(labelStyle);idLabel new QLabel();idLabel-setFixedHeight(height);idLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 5. 添加用户的名字的显示nameTag new QLabel();nameTag-setFixedSize(50, height);nameTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);nameTag-setText(昵称);nameTag-setStyleSheet(labelStyle);nameLabel new QLabel();nameLabel-setFixedHeight(height);nameLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);nameModifyBtn new QPushButton();nameModifyBtn-setFixedSize(70, 25);nameModifyBtn-setIconSize(QSize(20, 20));nameModifyBtn-setIcon(QIcon(:/resource/image/modify.png));nameModifyBtn-setStyleSheet(btnStyle);nameEdit new QLineEdit();nameEdit-setFixedHeight(height);nameEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);nameEdit-setStyleSheet(editStyle);nameEdit-hide();nameSubmitBtn new QPushButton();nameSubmitBtn-setFixedSize(70, 25);nameSubmitBtn-setIconSize(QSize(20, 20));nameSubmitBtn-setIcon(QIcon(:/resource/image/submit.png));nameSubmitBtn-setStyleSheet(btnStyle);nameSubmitBtn-hide();// 6. 添加个性签名descTag new QLabel();descTag-setFixedSize(50, height);descTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);descTag-setText(签名);descTag-setStyleSheet(labelStyle);descLabel new QLabel();descLabel-setFixedHeight(height);descLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);descModifyBtn new QPushButton();descModifyBtn-setFixedSize(70, 25);descModifyBtn-setIconSize(QSize(20, 20));descModifyBtn-setIcon(QIcon(:/resource/image/modify.png));descModifyBtn-setStyleSheet(btnStyle);descEdit new QLineEdit();descEdit-setFixedHeight(height);descEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);descEdit-setStyleSheet(editStyle);descEdit-hide();descSubmitBtn new QPushButton();descSubmitBtn-setFixedSize(70, 25);descSubmitBtn-setIconSize(QSize(20, 20));descSubmitBtn-setIcon(QIcon(:/resource/image/submit.png));descSubmitBtn-setStyleSheet(btnStyle);descSubmitBtn-hide();// 7. 添加电话phoneTag new QLabel();phoneTag-setFixedSize(50, height);phoneTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);phoneTag-setText(电话);phoneTag-setStyleSheet(labelStyle);phoneLabel new QLabel();phoneLabel-setFixedHeight(height);phoneLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);phoneModifyBtn new QPushButton();phoneModifyBtn-setFixedSize(70, 25);phoneModifyBtn-setIconSize(QSize(20, 20));phoneModifyBtn-setIcon(QIcon(:/resource/image/modify.png));phoneModifyBtn-setStyleSheet(btnStyle);phoneEdit new QLineEdit();phoneEdit-setFixedHeight(height);phoneEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);phoneEdit-setStyleSheet(editStyle);phoneEdit-hide();phoneSubmitBtn new QPushButton();phoneSubmitBtn-setFixedSize(70, 25);phoneSubmitBtn-setIconSize(QSize(20, 20));phoneSubmitBtn-setIcon(QIcon(:/resource/image/submit.png));phoneSubmitBtn-setStyleSheet(btnStyle);phoneSubmitBtn-hide();// 8. 添加验证码verifyCodeTag new QLabel();verifyCodeTag-setFixedSize(50, height);verifyCodeTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);verifyCodeTag-setText(验证码);verifyCodeTag-setStyleSheet(labelStyle);verifyCodeTag-hide();verifyCodeEdit new QLineEdit();verifyCodeEdit-setFixedHeight(height);verifyCodeEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);verifyCodeEdit-setStyleSheet(editStyle);verifyCodeEdit-hide();getVerifyCodeBtn new QPushButton();getVerifyCodeBtn-setText(获取验证码);getVerifyCodeBtn-setStyleSheet(QPushButton { border: none; background-color: transparent; } QPushButton:pressed { background-color: rgb(231, 231, 231); });getVerifyCodeBtn-setFixedSize(70, height);getVerifyCodeBtn-hide();// 9. 添加到布局管理器. 第 0 列被头像占用了. 下列内容都是从第一列开始往后排layout-addWidget(idTag, 0, 1);layout-addWidget(idLabel, 0, 2);layout-addWidget(nameTag, 1, 1);layout-addWidget(nameLabel, 1, 2);layout-addWidget(nameModifyBtn, 1, 3);layout-addWidget(descTag, 2, 1);layout-addWidget(descLabel, 2, 2);layout-addWidget(descModifyBtn, 2, 3);layout-addWidget(phoneTag, 3, 1);layout-addWidget(phoneLabel, 3, 2);layout-addWidget(phoneModifyBtn, 3, 3);// 测试代码 #if TEST_UIidLabel-setText(1234);nameLabel-setText(张三);descLabel-setText(从今天开始认真敲代码);phoneLabel-setText(18612345678);avatarBtn-setIcon(QIcon(:/resource/image/defaultAvatar.png)); #endif }5修改 MainWidget::initSignalSlot添加弹出该窗口的信号和槽函数 / /// 点击自己的头像, 弹出对话框显示个人主页 / connect(userAvatar, QPushButton::clicked, this, {SelfInfoWidget* selfInfoWidget new SelfInfoWidget(this);selfInfoWidget-exec(); // 弹出模态对话框// selfInfoWidget-show(); // 弹出非模态 });5.2 实现用户详细信息界面 1点击其用户头像时打开如下界面 2创建UserInfoWidget类来实现用户信息窗口 class UserInfoWidget : public QDialog {Q_OBJECT public:UserInfoWidget(const UserInfo userInfo, QWidget* parent);private:const UserInfo userInfo;QPushButton* avatarBtn;QLabel* idTag;QLabel* idLabel;QLabel* nameTag;QLabel* nameLabel;QLabel* phoneTag;QLabel* phoneLabel;QPushButton* applyBtn;QPushButton* sendMessageBtn;QPushButton* deleteFriendBtn; };3UserInfoWidget类的具体实现 UserInfoWidget::UserInfoWidget(const UserInfo userInfo, QWidget* parent):QDialog(parent),userInfo(userInfo) {// 1. 设置基本属性this-setFixedSize(400, 200);this-setWindowTitle(用户详情);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setAttribute(Qt::WA_DeleteOnClose);this-move(QCursor::pos());// 2. 创建布局管理器QGridLayout* layout new QGridLayout();layout-setVerticalSpacing(10);layout-setHorizontalSpacing(20);layout-setContentsMargins(40, 20, 0, 0);layout-setAlignment(Qt::AlignTop);this-setLayout(layout);// 3. 添加头像avatarBtn new QPushButton();avatarBtn-setFixedSize(75, 75);avatarBtn-setIconSize(QSize(75, 75));avatarBtn-setIcon(userInfo.avatar);QString labelStyle QLabel { font-weight: 800; padding-left: 20px;};QString btnStyle QPushButton { border: 1px solid rgb(100, 100, 100); border-radius: 5px; background-color: rgb(240, 240, 240); };btnStyle QPushButton:pressed { background-color: rgb(205, 205, 205); };int width 80;int height 30;// 4. 添加用户序号idTag new QLabel();idTag-setText(序号);idTag-setStyleSheet(labelStyle);idTag-setFixedSize(width, height);idTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);idLabel new QLabel();idLabel-setText(userInfo.userId);idLabel-setFixedSize(width, height);idLabel-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);// 5. 添加用户昵称nameTag new QLabel();nameTag-setText(昵称);nameTag-setStyleSheet(labelStyle);nameTag-setFixedSize(width, height);nameTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);nameLabel new QLabel();nameLabel-setText(userInfo.nickname);nameLabel-setFixedSize(width, height);nameLabel-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);// 6. 设置电话phoneTag new QLabel();phoneTag-setText(电话);phoneTag-setStyleSheet(labelStyle);phoneTag-setFixedSize(width, height);phoneTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);phoneLabel new QLabel();phoneLabel-setText(userInfo.phone);phoneLabel-setFixedSize(width, height);phoneLabel-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);// 7. 添加功能按钮applyBtn new QPushButton();applyBtn-setText(好友申请);applyBtn-setFixedSize(80, 30);applyBtn-setStyleSheet(btnStyle);applyBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);sendMessageBtn new QPushButton();sendMessageBtn-setText(发送消息);sendMessageBtn-setFixedSize(80, 30);sendMessageBtn-setStyleSheet(btnStyle);sendMessageBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);deleteFriendBtn new QPushButton();deleteFriendBtn-setText(删除好友);deleteFriendBtn-setFixedSize(80, 30);deleteFriendBtn-setStyleSheet(btnStyle);deleteFriendBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);// 8. 添加上述内容到布局管理器中layout-addWidget(avatarBtn, 0, 0, 3, 1);layout-addWidget(idTag, 0, 1);layout-addWidget(idLabel, 0, 2);layout-addWidget(nameTag, 1, 1);layout-addWidget(nameLabel, 1, 2);layout-addWidget(phoneTag, 2, 1);layout-addWidget(phoneLabel, 2, 2);layout-addWidget(applyBtn, 3, 0);layout-addWidget(sendMessageBtn, 3, 1);layout-addWidget(deleteFriendBtn, 3, 2); }5.3 实现单聊消息会话详细信息界面 1点击单聊点击 … 时打开的界面如下 2创建SessionDetailWidget类实现会话详情窗口 class SessionDetailWidget : public QDialog {Q_OBJECT public:SessionDetailWidget(QWidget* parent, const UserInfo userInfo);private:QPushButton* deleteFriendBtn;UserInfo userInfo; };3SessionDetailWidget类的具体实现 SessionDetailWidget::SessionDetailWidget(QWidget* parent, const UserInfo userInfo):QDialog(parent),userInfo(userInfo) {// 1. 设置基本属性this-setWindowTitle(会话详情);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setFixedSize(300, 300);this-setStyleSheet(QWidget { background-color: rgb(255, 255, 255); });this-setAttribute(Qt::WA_DeleteOnClose);// 2. 创建布局管理器QGridLayout* layout new QGridLayout();layout-setSpacing(10);layout-setContentsMargins(50, 0, 50, 0);this-setLayout(layout);// 3. 添加 创建群聊 按钮AvatarItem* createGroupBtn new AvatarItem(QIcon(:/resource/image/cross.png), 添加);layout-addWidget(createGroupBtn, 0, 0);// 4. 添加当前用户的信息 (临时构造的假数据) #if TEST_UIAvatarItem* currentUser new AvatarItem(QIcon(:/resource/image/defaultAvatar.png), 张三123456);layout-addWidget(currentUser, 0, 1); #endifAvatarItem* currentUser new AvatarItem(userInfo.avatar, userInfo.nickname);layout-addWidget(currentUser, 0, 1);// 5. 添加 删除好友 按钮deleteFriendBtn new QPushButton();deleteFriendBtn-setFixedHeight(50);deleteFriendBtn-setText(删除好友);QString style QPushButton { border: 1px solid rgb(90, 90, 90); border-radius: 5px; } ;style QPushButton:pressed { background-color: rgb(235, 235, 235); };deleteFriendBtn-setStyleSheet(style);layout-addWidget(deleteFriendBtn, 1, 0, 1, 3); }4创建AvatarItem类来实现头像昵称的组合控件 class AvatarItem : public QWidget {Q_OBJECTpublic:AvatarItem(const QIcon avatar, const QString name);QPushButton* getAvatar(){return avatarBtn;}private:QPushButton* avatarBtn;QLabel* nameLabel;};5AvatarItem类的具体实现 AvatarItem::AvatarItem(const QIcon avatar, const QString name) {// 1. 设置自身的基本属性this-setFixedSize(70, 80);// 2. 创建布局管理器QVBoxLayout* layout new QVBoxLayout();layout-setSpacing(0);layout-setContentsMargins(0, 0, 0, 0);layout-setAlignment(Qt::AlignHCenter);this-setLayout(layout);// 3. 创建头像avatarBtn new QPushButton();avatarBtn-setFixedSize(45, 45);avatarBtn-setIconSize(QSize(45, 45));avatarBtn-setIcon(avatar);avatarBtn-setStyleSheet(QPushButton { border: none; });// 4. 创建名字nameLabel new QLabel();nameLabel-setText(name);QFont font(微软雅黑, 12);nameLabel-setFont(font);nameLabel-setAlignment(Qt::AlignCenter);// 5. 对名字做 截断操作const int MAX_WIDTH 65;QFontMetrics metrics(font);int totalWidth metrics.horizontalAdvance(name);if(totalWidth MAX_WIDTH){// 需要截断QString tail …;int tailWidth metrics.horizontalAdvance(tail);int availableWidth MAX_WIDTH - tailWidth;int availableSize name.size() * ((double)availableWidth / totalWidth);QString newName name.left(availableSize);nameLabel-setText(newName tail);}// 6.将按钮和文本设置到布局当中layout-addWidget(avatarBtn);layout-addWidget(nameLabel); }6实现弹出对话框在MainWidget::initSignalSlot当中实现槽函数 / /// 点击会话详情按钮, 弹出会话详情窗口 / connect(extraBtn, QPushButton::clicked, this, {// 判定当前会话是单聊还是群聊// 获取到当前会话详细信息, 通过会话中的 userId 属性ChatSessionInfo* chatSessionInfo dataCenter-findChatSessionById(dataCenter-getCurrentChatSessionId());if(chatSessionInfo nullptr){LOG() 当前会话不存在, 无法弹出会话详情对话框;return;}bool isSingleChat chatSessionInfo-userId ! ;if(isSingleChat ){// 单聊, 弹出这个窗口UserInfo* userInfo dataCenter-findFriendById(chatSessionInfo-userId);if(userInfo nullptr){LOG() 单聊会话对应的用户不存在, 无法弹出会话详情窗口;return;}SessionDetailWidget* sessiondetailwidget new SessionDetailWidget(this, userInfo);sessiondetailwidget-exec();}else{GroupSessionDetailWidget groupsessiondetailwidget new GroupSessionDetailWidget(this);groupsessiondetailwidget-exec();} });5.4 实现创建群聊会话选择好友界面 1当点击如下按钮是跳转出来选择已有好友进入群聊界面 2创建ChooseFriendDialog类来实现好友选择窗口 class ChooseFriendDialog : public QDialog {Q_OBJECT public:ChooseFriendDialog(QWidget* parent, const QString userId);// 针对左侧窗口进行初始化void initLeft(QHBoxLayout *layout);// 针对右侧窗口进行初始化void initRight(QHBoxLayout layout);void clickOkBtn();QListQString generateMemberList();void addFriend(const QString userId, const QIcon avatar, const QString name, bool checked);void addSelectedFriend(const QString userId, const QIcon avatar, const QString name);void deleteSelectedFriend(const QString userId);private:// 保存左侧全部好友列表的 QWidgetQWidget totalContainer;// 保存右侧选中好友列表的 QWidgetQWidget* selectedContainer;// 当前选择窗口是点击哪个用户弹出来QString userId; };3ChooseFriendDialog类的具体实现 ChooseFriendDialog::ChooseFriendDialog(QWidget* parent, const QString userId):QDialog(parent),userId(userId) {// 1. 设置窗口的基本属性this-setWindowTitle(选择好友);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setFixedSize(750, 550);this-setStyleSheet(QDialog { background-color: rgb(255, 255, 255);});this-setAttribute(Qt::WA_DeleteOnClose);// 2. 创建布局管理器QHBoxLayout* layout new QHBoxLayout();layout-setContentsMargins(0, 0, 0, 0);layout-setSpacing(0);this-setLayout(layout);// 3. 针对左侧窗口进行初始化initLeft(layout);// 4. 针对右侧窗口进行初始化initRight(layout); }实现筛选好友列表 // 针对左侧窗口进行初始化 void ChooseFriendDialog::initLeft(QHBoxLayout layout) {// 1. 创建滚动区域QScrollArea scrollArea new QScrollArea();scrollArea-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);scrollArea-setWidgetResizable(true);scrollArea-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal { height: 0px;});scrollArea-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(255, 255, 255) });scrollArea-setStyleSheet(QScrollArea { border:none; });layout-addWidget(scrollArea, 1);// 2. 创建 QWidget 设置到滚动区域中.totalContainer new QWidget();totalContainer-setObjectName(totalContainer);totalContainer-setStyleSheet(#totalContainer { background-color: rgb(255, 255, 255); });scrollArea-setWidget(totalContainer);// 3. 创建左侧子窗口内部的 垂直布局管理器QVBoxLayout* vlayout new QVBoxLayout();vlayout-setSpacing(0);vlayout-setContentsMargins(0, 0, 0, 0);vlayout-setAlignment(Qt::AlignTop);totalContainer-setLayout(vlayout);// 还需要进一步的添加 vlayout 内部的元素, 才能看到效果!// 此处也是先构造测试数据, 后续接入服务器之后, 从服务器拿到真实的好友列表, 再添加真实的数据 #if TEST_UIQIcon defaultAvatar(:/resource/image/defaultAvatar.png);for (int i 0; i 30; i){this-addFriend(QString::number(1000 i), defaultAvatar, 张三 QString::number(i), false);} #endif }void ChooseFriendDialog::addFriend(const QString userId, const QIcon avatar, const QString name, bool checked) {ChooseFriendItem* item new ChooseFriendItem(this, userId, avatar, name, checked);totalContainer-layout()-addWidget(item); }创建ChooseFriendItem类来实现好友元素 class ChooseFriendItem : public QWidget {Q_OBJECT public:ChooseFriendItem(ChooseFriendDialog* owner, const QString userId, const QIcon avatar, const QString name, bool checked);void paintEvent(QPaintEvent* event) override;void enterEvent(QEnterEvent* event) override;void leaveEvent(QEvent* event) override;const QString getUserId() const{return userId;}QCheckBox* getCheckBox(){return checkBox;}private:bool isHover false;QCheckBox* checkBox;QPushButton* avatarBtn;QLabel* nameLabel;ChooseFriendDialog* owner; // 记录了哪个 QWidget 持有了这个 Item. 此处的 QWidget 应该是一个 ChooseFriendDialogQString userId; // 记录了当前 Item 对应的 userId 是啥.};ChooseFriendItem类的具体实现 ChooseFriendItem::ChooseFriendItem(ChooseFriendDialog* owner, const QString userId, const QIcon avatar, const QString name, bool checked):userId(userId),owner(owner) {// 1. 设置控件的基本属性this-setFixedHeight(50);this-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 2. 设置布局管理器QHBoxLayout* layout new QHBoxLayout();layout-setSpacing(10);layout-setContentsMargins(20, 0, 20, 0);this-setLayout(layout);// 3. 创建复选框checkBox new QCheckBox();checkBox-setChecked(checked);checkBox-setFixedSize(25, 25);QString style QCheckBox { background-color: transparent; } QCheckBox::indicator { width: 20px; height: 20px; image: url(:/resource/image/unchecked.png);};style QCheckBox::indicator:checked { image: url(:/resource/image/checked.png);};checkBox-setStyleSheet(style);// 4. 创建头像avatarBtn new QPushButton();avatarBtn-setFixedSize(40, 40);avatarBtn-setIconSize(QSize(40, 40));avatarBtn-setIcon(QIcon(avatar));// 5. 创建名字nameLabel new QLabel();nameLabel-setText(name);nameLabel-setStyleSheet(QLabel {background-color: transparent;});// 6. 添加上述内容到布局管理器中layout-addWidget(checkBox);layout-addWidget(avatarBtn);layout-addWidget(nameLabel);// 7. 连接信号槽connect(checkBox, QCheckBox::toggled, this, {if (checked){// 勾选了复选框, 把当前这个 Item, 添加到右侧的已选择区域owner-addSelectedFriend(userId, avatar, name);}else{// 取消勾选owner-deleteSelectedFriend(userId);}}); }void ChooseFriendItem::paintEvent(QPaintEvent* event) {(void)event;QPainter painter(this);if(isHover){// 绘制成深色painter.fillRect(this-rect(), QColor(230, 230, 230));}else{// 绘制成浅色painter.fillRect(this-rect(), QColor(255, 255, 255));} }void ChooseFriendItem::enterEvent(QEnterEvent* event) {(void)event;isHover true;// update 相当于 更新界面this-update();// 或者使用下列代码// this-repaint(); }void ChooseFriendItem::leaveEvent(QEvent* event) {(void)event;isHover false;this-update(); }实现已选中好友列表 // 针对右侧窗口进行初始化 void ChooseFriendDialog::initRight(QHBoxLayout layout) {// 1. 创建右侧的布局管理器QGridLayout gridLayout new QGridLayout();gridLayout-setContentsMargins(20, 0, 20, 20);gridLayout-setSpacing(10);layout-addLayout(gridLayout, 1);// 2. 创建 提示 labelQLabel* tipLabel new QLabel();tipLabel-setText(选择联系人);tipLabel-setFixedHeight(30);tipLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);tipLabel-setAlignment(Qt::AlignLeft | Qt::AlignVCenter);tipLabel-setStyleSheet(QLabel { font-size: 16px; font-weight: 700});// 3. 创建滚动区域QScrollArea* scrollArea new QScrollArea();scrollArea-setWidgetResizable(true);scrollArea-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(255, 255, 255);});scrollArea-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal {height: 0px;});scrollArea-setStyleSheet(QScrollArea {border: none;});scrollArea-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);// 4. 创建滚动区域中的 QWidgetselectedContainer new QWidget();selectedContainer-setObjectName(selectedContainer);selectedContainer-setStyleSheet(#selectedContainer { background-color: rgb(255, 255, 255); });scrollArea-setWidget(selectedContainer);// 5. 创建 selectedContainer 中的 垂直布局QVBoxLayout* vlayout new QVBoxLayout();vlayout-setSpacing(0);vlayout-setContentsMargins(0, 0, 0, 0);vlayout-setAlignment(Qt::AlignTop);selectedContainer-setLayout(vlayout);// 6. 创建底部按钮QString style QPushButton { color: rgb(7, 191, 96); background-color: rgb(240, 240, 240); border: none; border-radius: 5px;};style QPushButton:hover { background-color: rgb(220, 220, 220); } QPushButton:pressed { background-color: rgb(200, 200, 200); };QPushButton* okBtn new QPushButton();okBtn-setFixedHeight(40);okBtn-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);okBtn-setText(完成);okBtn-setStyleSheet(style);QPushButton* cancelBtn new QPushButton();cancelBtn-setFixedHeight(40);cancelBtn-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);cancelBtn-setText(取消);cancelBtn-setStyleSheet(style);// 7. 把上述控件添加到布局中gridLayout-addWidget(tipLabel, 0, 0, 1, 9);gridLayout-addWidget(scrollArea, 1, 0, 1, 9);gridLayout-addWidget(okBtn, 2, 1, 1, 3);gridLayout-addWidget(cancelBtn, 2, 5, 1, 3);// 构造一些数据用来进行测试界面// 此处的数据通过勾选左侧列表来生成.// QIcon defaultAvatar(:/resource/image/defaultAvatar.png);// for (int i 0; i 10; i)// {// this-addSelectedFriend(QString::number(1000 i), defaultAvatar, 张三 QString::number(i));// }// 8. 添加信号槽, 处理 ok 和 cancel 的点击connect(okBtn, QPushButton::clicked, this, ChooseFriendDialog::clickOkBtn);connect(cancelBtn, QPushButton::clicked, this, {// 关闭窗口this-close();}); }void ChooseFriendDialog::addSelectedFriend(const QString userId, const QIcon avatar, const QString name) {ChooseFriendItem* item new ChooseFriendItem(this, userId, avatar, name, true);selectedContainer-layout()-addWidget(item); }void ChooseFriendDialog::deleteSelectedFriend(const QString userId) {// 遍历 selectedContainer 中的每个 Item, 对比每个 Item 里的 userId , 是否是要删除的 userId.QVBoxLayout* vlayout dynamic_castQVBoxLayout(selectedContainer-layout());// 由于是要 遍历 删除 需要从后往前进行for (int i vlayout-count() - 1; i 0; –i){auto item vlayout-itemAt(i);if (item nullptr || item-widget() nullptr){continue;}ChooseFriendItem* chooseFriendItem dynamic_castChooseFriendItem(item-widget());// 判定当前的 Item 的 userId 是否是要删除的 userIdif (chooseFriendItem-getUserId() ! userId){continue;}vlayout-removeWidget(chooseFriendItem);// 此处直接使用 delete 可能导致程序直接崩溃. 因为 delete 该对象的时候, 该对象内部的 QCheckBox 还在使用中 (触发着信号槽呢)// 改成 deleteLater, 就相当于把 delete 操作委托给 Qt 自身来完成了. 告诉 Qt 框架说, 你要删除这个对象. 至于啥时候删除 Qt// 会确保在 Qt 自身用完了之后, 去真正删除.// delete chooseFriendItem;chooseFriendItem-deleteLater();}// 再遍历一下左侧列表, 把左侧列表中对应 item 的 checkBox 勾选状态取消掉.QVBoxLayout vlayoutLeft dynamic_castQVBoxLayout(totalContainer-layout());for (int i 0; i vlayoutLeft-count(); i){auto item vlayoutLeft-itemAt(i);if(item nullptr || item-widget() nullptr){continue;}ChooseFriendItem* chooseFriendItem dynamic_castChooseFriendItem(item-widget());if(chooseFriendItem-getUserId() ! userId){continue;}// 取消 checkBox 选中状态chooseFriendItem-getCheckBox()-setChecked(false);} }5.5 实现群聊消息会话详细信息界面 1点击群聊 … 时打开的界面如下 2创建GroupSessionDetailWidget类实现群组会话详情窗口 class GroupSessionDetailWidget : public QDialog {Q_OBJECT public:GroupSessionDetailWidget(QWidget parent);void addMember(AvatarItem* avatarItem);private:QGridLayout* glayout;QLabel* groupNameLabel;// 表示当前要添加的 AvatarItem 处在的行和列// 由于整个界面上存在 这个按钮, 占据了 (0, 0) 位置. 接下来添加 AvatarItem 就要从// (0, 1) 位置添加了int curRow 0;int curCol 1; };3GroupSessionDetailWidget类的具体实现 GroupSessionDetailWidget::GroupSessionDetailWidget(QWidget* parent):QDialog(parent) {// 1. 设置窗口的基本属性.this-setFixedSize(410, 600);this-setWindowTitle(群组详情);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setStyleSheet(QDialog { background-color: rgb(255, 255, 255); });this-setAttribute(Qt::WA_DeleteOnClose);// 2. 创建布局管理器QVBoxLayout* vlayout new QVBoxLayout();vlayout-setSpacing(0);vlayout-setContentsMargins(50, 20, 50, 50);vlayout-setAlignment(Qt::AlignTop);this-setLayout(vlayout);// 3. 创建滚动区域// 3.1 创建 QScrollArea 对象QScrollArea* scrollArea new QScrollArea();scrollArea-setWidgetResizable(true);scrollArea-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(255, 255, 255); });scrollArea-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal { height: 0; });scrollArea-setFixedSize(310, 350);scrollArea-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);scrollArea-setStyleSheet(QWidget { background-color: transparent; border: none; });// 3.2 创建一个 QScrollArea 内部的 QWidgetQWidget* container new QWidget();scrollArea-setWidget(container);// 3.3 给 container 里面添加一个 网格布局glayout new QGridLayout();glayout-setSpacing(10);glayout-setContentsMargins(0, 0, 0, 0);glayout-setAlignment(Qt::AlignTop | Qt::AlignLeft);container-setLayout(glayout);// 3.4 把滚动区域, 添加到布局管理器中vlayout-addWidget(scrollArea);// 4. 添加 添加按钮AvatarItem* addBtn new AvatarItem(QIcon(:/resource/image/cross.png), 添加);glayout-addWidget(addBtn, 0, 0);// 5. 添加 群聊名称QLabel* groupNameTag new QLabel();groupNameTag-setText(群聊名称);groupNameTag-setStyleSheet(QLabel {font-weight: 700; font-size: 16px;});groupNameTag-setFixedHeight(50);groupNameTag-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);// 设置文字在 QLabel 内部的对齐方式.groupNameTag-setAlignment(Qt::AlignBottom);// 这里设置的 QLabel 在布局管理器中的对齐方式.vlayout-addWidget(groupNameTag);// 6. 添加 真实的群聊名字 和 修改按钮// 6.1 创建水平布局QHBoxLayout* hlayout new QHBoxLayout();hlayout-setSpacing(0);hlayout-setContentsMargins(0, 0, 0, 0);vlayout-addLayout(hlayout);// 6.2 创建真实群聊名字的 labelgroupNameLabel new QLabel();groupNameLabel-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);groupNameLabel-setFixedHeight(50);groupNameLabel-setStyleSheet(QLabel { font-size: 18px; });hlayout-addWidget(groupNameLabel, 0, Qt::AlignLeft | Qt::AlignVCenter);// 6.3 创建 修改按钮QPushButton* modifyBtn new QPushButton();modifyBtn-setFixedSize(30, 30);modifyBtn-setIconSize(QSize(30, 30));modifyBtn-setIcon(QIcon(:/resource/image/modify.png));modifyBtn-setStyleSheet(QPushButton { border: none; background-color: transparent; } QPushButton:pressed { background-color: rgb(230, 230, 230); });hlayout-addWidget(modifyBtn, 0, Qt::AlignRight | Qt::AlignVCenter);// 7. 退出群聊按钮QPushButton* exitGroupBtn new QPushButton();exitGroupBtn-setText(退出群聊);exitGroupBtn-setFixedHeight(50);exitGroupBtn-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);QString btnStyle QPushButton { border: 1px solid rgb(90, 90, 90); border-radius: 5px; background-color: transparent;};btnStyle QPushButton:pressed { background-color: rgb(230, 230, 230); };exitGroupBtn-setStyleSheet(btnStyle);vlayout-addWidget(exitGroupBtn);// 此处构造假的数据用来测试界面 #if TEST_UIgroupNameLabel-setText(人类吃喝行为研究小组);QIcon avatar(:/resource/image/defaultAvatar.png);for(int i 0; i 20; i){AvatarItem* item new AvatarItem(avatar, 张三 QString::number(i));this-addMember(item);} #endif }void GroupSessionDetailWidget::addMember(AvatarItem* avatarItem) {const int MAX_COL 4;if(curCol MAX_COL){// 换行操作curRow;curCol 0;}glayout-addWidget(avatarItem, curRow, curCol);curCol; }5.6 实现添加好友界面 1点击主界面上方的 “” 按钮弹出添加好友界面 2创建AddFriendDialog类实现添加好友窗口 class AddFriendDialog : public QDialog {Q_OBJECT public:AddFriendDialog(QWidget* parent);// 往窗口中新增一个好友搜索结果void addResult(const UserInfo userInfo);// 清空界面上所有的好友结果void clear();void setSearchKey(const QString searchKey);private:QLineEdit* searchEdit;// 整个窗口总的网格布局QGridLayout* layout;// 保存搜索好友的结果QWidget* resultContainer; };3AddFriendDialog类的具体实现 AddFriendDialog::AddFriendDialog(QWidget* parent):QDialog(parent) {// 1. 设置基本属性this-setFixedSize(500, 500);this-setWindowTitle(添加好友);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setStyleSheet(QDialog {background-color: rgb(255, 255, 255); });this-setAttribute(Qt::WA_DeleteOnClose); // 不要忘记这个属性!!!// 2. 添加布局管理器layout new QGridLayout();layout-setSpacing(10);layout-setContentsMargins(20, 20, 20, 0);this-setLayout(layout);// 3. 创建搜索框searchEdit new QLineEdit();searchEdit-setFixedHeight(50);searchEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);QString style QLineEdit { border: none; border-radius: 10px; font-size: 16px; background-color: rgb(240, 240, 240); padding-left: 5px;};searchEdit-setStyleSheet(style);searchEdit-setPlaceholderText(按手机号/用户序号/昵称搜索);layout-addWidget(searchEdit, 0, 0, 1, 8);// 4. 创建搜索按钮QPushButton* searchBtn new QPushButton();searchBtn-setFixedSize(50, 50);searchBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);searchBtn-setIconSize(QSize(30, 30));searchBtn-setIcon(QIcon(:/resource/image/search.png));QString btnStyle QPushButton { border: none; background-color: rgb(240, 240, 240); border-radius: 10px; };btnStyle QPushButton:hover { background-color: rgb(220, 220, 220); } QPushButton:pressed { background-color: rgb(200, 200, 200); } ;searchBtn-setStyleSheet(btnStyle);layout-addWidget(searchBtn, 0, 8, 1, 1);// 5. 添加滚动区域initResultArea();// 构造假的数据, 验证界面效果 #if TEST_UIQIcon avatar(:/resource/image/defaultAvatar.png);for(int i 0; i 20; i){// new 出来这个对象, 再往 addResult 中添加. FriendResultItem 中持有了 UserInfo 的 const 引用. 需要确保引用是有效的引用UserInfo* userInfo new UserInfo();userInfo-userId QString::number(1000 i);userInfo-nickname 张三 QString::number(i);userInfo-description 这是一段个性签名;userInfo-avatar avatar;this-addResult(userInfo);} #endif }void AddFriendDialog::initResultArea() {// 1. 创建滚动区域对象QScrollArea scrollArea new QScrollArea();scrollArea-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);scrollArea-setWidgetResizable(true);scrollArea-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal {height: 0;} );scrollArea-verticalScrollBar()-setStyleSheet(QScrollBar:vertical {width: 2px; background-color: rgb(255, 255, 255);});scrollArea-setStyleSheet(QScrollArea { border: none; });layout-addWidget(scrollArea, 1, 0, 1, 9);// 2. 创建 QWidgetresultContainer new QWidget();resultContainer-setObjectName(resultContainer);resultContainer-setStyleSheet(#resultContainer { background-color: rgb(255, 255, 255); } );scrollArea-setWidget(resultContainer);// 3. 给这个 QWidget 里面添加元素, 需要给它创建垂直的布局管理器QVBoxLayout* vlayout new QVBoxLayout();vlayout-setSpacing(0);vlayout-setContentsMargins(0, 0, 0, 0);resultContainer-setLayout(vlayout); }// 往窗口中新增一个好友搜索结果 void AddFriendDialog::addResult(const UserInfo userInfo) {FriendResultItem* item new FriendResultItem(userInfo);resultContainer-layout()-addWidget(item); }// 清空界面上所有的好友结果 void AddFriendDialog::clear() {// 从后往前遍历QVBoxLayout* layout dynamic_castQVBoxLayout(resultContainer-layout());for(int i layout-count(); i 0; –i){QLayoutItem layoutItem layout-takeAt(i);if(layoutItem nullptr || layoutItem-widget() nullptr){continue;}// 删除这里面持有的元素delete layoutItem-widget();} }void AddFriendDialog::setSearchKey(const QString searchKey) {searchEdit-setText(searchKey); }4创建FriendResultItem类实现好友搜索结果元素 class FriendResultItem : public QWidget {Q_OBJECTpublic:FriendResultItem(const UserInfo userInfo);private:const UserInfo userInfo;QPushButton* addBtn; };5FriendResultItem类的具体实现 FriendResultItem::FriendResultItem(const UserInfo userInfo):userInfo(userInfo) {// 1. 设置基本属性this-setFixedHeight(70);this-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 2. 创建布局管理器QGridLayout* layout new QGridLayout();layout-setSpacing(0);layout-setHorizontalSpacing(10);layout-setContentsMargins(0, 0, 20, 0);this-setLayout(layout);// 3. 创建头像QPushButton* avatarBtn new QPushButton();avatarBtn-setFixedSize(50, 50);avatarBtn-setIconSize(QSize(50, 50));avatarBtn-setIcon(userInfo.avatar);layout-addWidget(avatarBtn);// 4. 创建昵称QLabel* nameLabel new QLabel();nameLabel-setFixedHeight(35); // 整个 Item 高度是 70. 昵称和个性签名各自占一半.nameLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);nameLabel-setAlignment(Qt::AlignLeft | Qt::AlignVCenter);nameLabel-setStyleSheet(QLabel { font-size: 16px; font-weight: 700;});nameLabel-setText(userInfo.nickname);// 5. 创建个性签名QLabel* descLabel new QLabel();descLabel-setFixedHeight(35);descLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);descLabel-setAlignment(Qt::AlignLeft | Qt::AlignVCenter);descLabel-setStyleSheet(QLabel { font-size: 14px; });descLabel-setText(userInfo.description);// 6. 创建添加好友按钮addBtn new QPushButton();addBtn-setFixedSize(100, 40);addBtn-setText(添加好友);QString btnStyle QPushButton { border: none; background-color: rgb(137, 217, 97); color: rgb(255, 255, 255); border-radius: 10px;} ;btnStyle QPushButton:pressed { background-color: rgb(200, 200, 200); };addBtn-setStyleSheet(btnStyle);// 7. 把上述内容, 添加到布局管理器中layout-addWidget(avatarBtn, 0, 0, 2, 1);layout-addWidget(nameLabel, 0, 1);layout-addWidget(descLabel, 1, 1);layout-addWidget(addBtn, 0, 2, 2, 1); }6在MainWidget::initSignalSlot中弹出添加好友对话框 处理 按钮的点击 connect(addFriendBtn, QPushButton::clicked, this, {AddFriendDialog* addFriendDialog new AddFriendDialog(this);addFriendDialog-exec(); });处理输入框输入 connect(searchEdit, QLineEdit::textEdited, this, {const QString searchKey searchEdit-text();AddFriendDialog* addFriendDialog new AddFriendDialog(this);addFriendDialog-setSearchKey(searchKey);// 清空主窗口的文本内容searchEdit-setText();addFriendDialog-exec(); });5.7 实现历史消息界面 1点击查看历史消息按钮时弹出如下该窗口 2创建HistoryMessageWidget类实现历史消息窗口 class HistoryMessageWidget : public QDialog {Q_OBJECTpublic:HistoryMessageWidget(QWidget* parent);// 在窗口中添加一个历史消息void addHistoryMessage(const Message message);// 清空窗口中所有的历史消息void clear();private:// 持有所有的历史消息结果的容器对象QWidget* container;QLineEdit* searchEdit;QRadioButton* keyRadioBtn;QRadioButton* timeRadioBtn;QDateTimeEdit* begTimeEdit;QDateTimeEdit* endTimeEdit;void initScrollArea(QGridLayout* layout); };3HistoryMessageWidget类的具体实现 HistoryMessageWidget::HistoryMessageWidget(QWidget* parent):QDialog(parent) {// 1. 设置窗口本身属性this-setFixedSize(600, 600);this-setWindowTitle(历史消息);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setStyleSheet(QWidget { background-color: rgb(255, 255, 255); });this-setAttribute(Qt::WA_DeleteOnClose);// 2. 创建布局管理器.QGridLayout* layout new QGridLayout();layout-setSpacing(10);layout-setContentsMargins(30, 30, 30, 0);this-setLayout(layout);// 3. 创建单选按钮keyRadioBtn new QRadioButton();timeRadioBtn new QRadioButton();keyRadioBtn-setText(按关键字查询);timeRadioBtn-setText(按时间查询);// 默认按照关键词查询keyRadioBtn-setChecked(true);layout-addWidget(keyRadioBtn, 0, 0, 1, 2);layout-addWidget(timeRadioBtn, 0, 2, 1, 2);// 4. 创建搜索框searchEdit new QLineEdit();searchEdit-setFixedHeight(50);searchEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);searchEdit-setPlaceholderText(要搜索的关键词);searchEdit-setStyleSheet(QLineEdit { border: none; border-radius: 10px; background-color: rgb(240, 240, 240); font-size: 16px; padding-left: 10px; });layout-addWidget(searchEdit, 1, 0, 1, 8);// 5. 创建搜索按钮QPushButton* searchBtn new QPushButton();searchBtn-setFixedSize(50, 50);searchBtn-setIconSize(QSize(50, 50));searchBtn-setIcon(QIcon(:/resource/image/search.png));QString btnStyle QPushButton { border: none; background-color: rgb(240, 240, 240); border-radius: 10px; };btnStyle QPushButton:pressed { background-color: rgb(220, 220, 220); };searchBtn-setStyleSheet(btnStyle);layout-addWidget(searchBtn, 1, 8, 1, 1);// 6. 创建时间相关的部分控件, 初始情况下要隐藏QLabel* begTag new QLabel();begTag-setText(开始时间);QLabel* endTag new QLabel();endTag-setText(结束时间);begTimeEdit new QDateTimeEdit();endTimeEdit new QDateTimeEdit();// [联调新增]begTimeEdit-setDisplayFormat(yyyy-MM-dd hh:mm);endTimeEdit-setDisplayFormat(yyyy-MM-dd hh:mm);begTimeEdit-setFixedHeight(40);endTimeEdit-setFixedHeight(40);begTag-hide();endTag-hide();begTimeEdit-hide();endTimeEdit-hide();// 7. 创建滚动区域initScrollArea(layout);// 8. 设置槽函数connect(keyRadioBtn, QRadioButton::clicked, this, {// 把时间相关的控件, 隐藏起来layout-removeWidget(begTag);layout-removeWidget(begTimeEdit);layout-removeWidget(endTag);layout-removeWidget(endTimeEdit);begTag-hide();begTimeEdit-hide();endTag-hide();endTimeEdit-hide();// 把关键词搜索框显示加入布局layout-addWidget(searchEdit, 1, 0, 1, 8);searchEdit-show();});connect(timeRadioBtn, QRadioButton::clicked, this, {layout-removeWidget(searchEdit);searchEdit-hide();// 把时间相关的控件, 添加到布局中, 并且进行显示.layout-addWidget(begTag, 1, 0, 1, 1);layout-addWidget(begTimeEdit, 1, 1, 1, 3);layout-addWidget(endTag, 1, 4, 1, 1);layout-addWidget(endTimeEdit, 1, 5, 1, 3);begTag-show();begTimeEdit-show();endTag-show();endTimeEdit-show();});connect(searchBtn, QPushButton::clicked, this, HistoryMessageWidget::clickSearchBtn);// 构造测试数据 #if TEST_UIfor (int i 0; i 30; i){// 注意此处代码和前面的差别.// 前面有个代码, UserInfo 必须要 new 出来才能构造. 当时 Item 对象里, 持有了 const UserInfo , 不是 new 的话// 就可能使引用指向的对象失效的.// 此处后续的代码, 都是按照传值的方式来使用 message 的内容, 不 new 也行.model::UserInfo sender;sender.userId ;sender.nickname 张三 QString::number(i);sender.avatar QIcon(:/resource/image/defaultAvatar.png);sender.description ;sender.phone 18612345678;Message message Message::makeMessage(model::TEXT_TYPE, , sender, QString(消息内容 QString::number(i)).toUtf8(), );this-addHistoryMessage(message);} #endif }// 在窗口中添加一个历史消息 void HistoryMessageWidget::addHistoryMessage(const Message message) {HistoryItem* item HistoryItem::makeHistoryItem(message);container-layout()-addWidget(item); }// 清空窗口中所有的历史消息 void HistoryMessageWidget::clear() {QVBoxLayout* layout dynamic_castQVBoxLayout(container-layout());for(int i layout-count() - 1; i 0; –i){// 之前使用的是 takeAt. 效果和这个是一样的.QWidget w layout-itemAt(i)-widget();if(w nullptr){continue;}layout-removeWidget(w);w-deleteLater();} }// 展示消息内容区域 void HistoryMessageWidget::initScrollArea(QGridLayout* layout) {// 1. 创建滚动区域对象QScrollArea* scrollArea new QScrollArea();scrollArea-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);scrollArea-setWidgetResizable(true);scrollArea-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(255, 255, 255); });scrollArea-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal { height: 0; });scrollArea-setStyleSheet(QScrollArea { border: none; });// 2. 创建 QWidget, 持有要加入的新的内容container new QWidget();scrollArea-setWidget(container);// 3. 创建 container 中的布局管理器.QVBoxLayout* vlayout new QVBoxLayout();vlayout-setSpacing(10);vlayout-setContentsMargins(0, 0, 0, 0);vlayout-setAlignment(Qt::AlignTop);container-setLayout(vlayout);// 4. 把滚动区加入到整个 layout 中layout-addWidget(scrollArea, 2, 0, 1, 9); }4创建HistoryItem类实现历史消息条目 class HistoryItem : public QWidget {Q_OBJECTpublic:HistoryItem() {}static HistoryItem* makeHistoryItem(const Message message); };5HistoryItem类的具体实现 HistoryItem* HistoryItem::makeHistoryItem(const Message message) {// 1. 创建出对象HistoryItem* item new HistoryItem();item-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);// 2. 创建布局QGridLayout* layout new QGridLayout();layout-setVerticalSpacing(0);layout-setHorizontalSpacing(10);layout-setContentsMargins(0, 0, 0, 0);item-setLayout(layout);// 3. 创建头像QPushButton* avatarBtn new QPushButton();avatarBtn-setFixedSize(40, 40);avatarBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);avatarBtn-setIconSize(QSize(40, 40));// 当前消息发送者的头像avatarBtn-setIcon(message.sender.avatar);avatarBtn-setStyleSheet(QPushButton { border: none; });// 4. 创建昵称和时间QLabel* nameLabel new QLabel();nameLabel-setText(message.sender.nickname | message.time);nameLabel-setFixedHeight(20); // 高度设置为头像高度的一半nameLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 5. 消息内容部分QWidget* contentWidget nullptr;if(message.messageType model::TEXT_TYPE){// 文本消息QLabel* label new QLabel();label-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);label-setWordWrap(true);label-setText(QString(message.content));label-adjustSize(); // 设置让 label 能够自动调整大小contentWidget label;}else if(message.messageType model::IMAGE_TYPE){// 图片消息contentWidget new ImageButton(message.fileId, message.content);}else if(message.messageType model::FILE_TYPE){// 文件消息contentWidget new FileLabel(message.fileId, message.fileName);}else if(message.messageType model::SPEECH_TYPE){// 语言消息contentWidget new SpeechLabel(message.fileId);}else{LOG() 错误的消息类型! messageType message.messageType;}// 6. 把上述控件添加到布局中layout-addWidget(avatarBtn, 0, 0, 2, 1);layout-addWidget(nameLabel, 0, 1, 1, 1);layout-addWidget(contentWidget, 1, 1, 5, 1);return item; }图片消息, 文件消息, 语音消息 放到后面再实现 6弹出历史消息对话框在 MessageEditArea::initSignalSlot 中连接信号槽 connect(showHistoryBtn, QPushButton::clicked, this, {HistoryMessageWidget* historyMessageWidget new HistoryMessageWidget(this);historyMessageWidget-exec(); });6. 用户名登录/注册界面的实现 1程序启动会先打开登录注册窗口 2创建LoginWidget类实现用户注册登录窗口 class LoginWidget : public QDialog {Q_OBJECTpublic:LoginWidget(QWidget* parent);void switchMode();private:bool isLoginMode true;QLineEdit* usernameEdit;QLineEdit* passwordEdit;QLineEdit* verifyCodeEdit;VerifyCodeWidget* verifyCodeWidget;QLabel* titleLabel;QPushButton* submitBtn;QPushButton* phoneModeBtn;QPushButton* switchModeBtn;};3LoginWidget类的具体实现 LoginWidget::LoginWidget(QWidget* parent):QDialog(parent) {// 1. 设置本窗口的基本属性this-setFixedSize(400, 350);this-setWindowTitle(登录);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setStyleSheet(QWidget { background-color: rgb(255, 255, 255); });this-setAttribute(Qt::WA_DeleteOnClose);// 2. 创建布局管理器QGridLayout* layout new QGridLayout();layout-setSpacing(0);layout-setContentsMargins(50, 0, 50, 0);this-setLayout(layout);// 3. 创建标题titleLabel new QLabel();titleLabel-setText(登录);titleLabel-setAlignment(Qt::AlignCenter);titleLabel-setFixedHeight(50);titleLabel-setStyleSheet(QLabel { font-size: 40px; font-weight: 600; });// 4. 创建用户名输入框QString editStyle QLineEdit { border: none; border-radius: 10px; font-size: 20px; background-color: rgb(240, 240, 240); padding-left:5px; };usernameEdit new QLineEdit();usernameEdit-setFixedHeight(40);usernameEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);usernameEdit-setPlaceholderText(输入用户名);usernameEdit-setStyleSheet(editStyle);// 5. 创建密码输入框passwordEdit new QLineEdit();passwordEdit-setFixedHeight(40);passwordEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);passwordEdit-setPlaceholderText(输入密码);passwordEdit-setStyleSheet(editStyle);passwordEdit-setEchoMode(QLineEdit::Password);// 6. 创建验证码输入框verifyCodeEdit new QLineEdit();verifyCodeEdit-setFixedHeight(40);verifyCodeEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);verifyCodeEdit-setPlaceholderText(输入验证码);verifyCodeEdit-setStyleSheet(editStyle);// 7. 创建显示验证码图片的控件 (此处先用 QPushButton 来表示一下, 后续进一步编写这里的逻辑)// 后续会自定义 QWidget, 通过画图 api 来实现这里的验证码功能.// QPushButton* verifyCodeWidget new QPushButton();// verifyCodeWidget-setText(验证码);// verifyCodeWidget-setStyleSheet(QWidget { border: none; });verifyCodeWidget new VerifyCodeWidget();// 8. 创建登录按钮submitBtn new QPushButton();submitBtn-setText(登录);submitBtn-setFixedHeight(40);submitBtn-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);QString btnGreenStyle QPushButton { border: none; border-radius: 10px; background-color: rgb(44, 182, 61); color: rgb(255, 255, 255); };btnGreenStyle QPushButton:pressed { background-color: rgb(240, 240, 240); };submitBtn-setStyleSheet(btnGreenStyle);// 9. 创建切换到手机号登录按钮phoneModeBtn new QPushButton();phoneModeBtn-setFixedSize(100, 40);phoneModeBtn-setText(手机号登录);QString btnWhiteStyle QPushButton { border: none; border-radius: 10px; background-color: transparent; };btnWhiteStyle QPushButton:pressed { background-color: rgb(240, 240, 240); };phoneModeBtn-setStyleSheet(btnWhiteStyle);// 10. 创建切换模式(登录和注册)按钮switchModeBtn new QPushButton();switchModeBtn-setFixedSize(100, 40);switchModeBtn-setText(注册);switchModeBtn-setStyleSheet(btnWhiteStyle);// 11. 添加到布局管理器中layout-addWidget(titleLabel, 0, 0, 1, 5);layout-addWidget(usernameEdit, 1, 0, 1, 5);layout-addWidget(passwordEdit, 2, 0, 1, 5);layout-addWidget(verifyCodeEdit, 3, 0, 1, 4);layout-addWidget(verifyCodeWidget, 3, 4, 1, 1);layout-addWidget(submitBtn, 4, 0, 1, 5);layout-addWidget(phoneModeBtn, 5, 0, 1, 1);layout-addWidget(switchModeBtn, 5, 4, 1, 1);// 12. 处理信号槽connect(switchModeBtn, QPushButton::clicked, this, LoginWidget::switchMode);connect(phoneModeBtn, QPushButton::clicked, this, {// 此处还可以把 isLoginMode 这个值传到新的窗口中, 让新的窗口决定自己是登录状态还是注册状态. 大家自行尝试实现.PhoneLoginWidget* phoneLoginWidget new PhoneLoginWidget(nullptr);phoneLoginWidget-show();// 关闭当前窗口this-close();});connect(submitBtn, QPushButton::clicked, this, LoginWidget::clickSubmitBtn); }// 实现界⾯切换 void LoginWidget::switchMode() {if (isLoginMode){// 当前是登录模式, 切换到注册模式this-setWindowTitle(注册);titleLabel-setText(注册);submitBtn-setText(注册);phoneModeBtn-setText(手机号注册);switchModeBtn-setText(登录);}else{// 当前是注册模式, 切换到登录模式this-setWindowTitle(登录);titleLabel-setText(登录);submitBtn-setText(登录);phoneModeBtn-setText(手机号登录);switchModeBtn-setText(注册);}isLoginMode !isLoginMode; }7. 实现手机号登录/注册界面 1注册界面展示 2创建PhoneLoginWidget类实现手机号注册登录窗口 class PhoneLoginWidget : public QDialog {Q_OBJECT public:PhoneLoginWidget(QWidget* parent);void switchMode();private:QLineEdit* phoneEdit;QPushButton* sendVerifyCodeBtn;QLineEdit* verifyCodeEdit;QLabel* titleLabel;QPushButton* submitBtn;QPushButton* switchModeBtn;bool isLoginMode true;QString currentPhone ; // 记录是使用哪个手机号发送的验证码QTimer* timer;int leftTime 30;};3PhoneLoginWidget类的具体实现 PhoneLoginWidget::PhoneLoginWidget(QWidget* parent):QDialog(parent) {// 1. 设置窗口的基本属性this-setFixedSize(400, 350);this-setWindowTitle(登录);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setStyleSheet(QWidget { background-color: rgb(255, 255, 255); });this-setAttribute(Qt::WA_DeleteOnClose);// 2. 创建核心布局管理器QGridLayout* layout new QGridLayout();layout-setSpacing(10);layout-setContentsMargins(50, 0, 50, 0);this-setLayout(layout);// 3. 创建标题titleLabel new QLabel();titleLabel-setText(登录);titleLabel-setFixedHeight(50);titleLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);titleLabel-setStyleSheet(QLabel { font-size: 40px; font-weight: 600; });titleLabel-setAlignment(Qt::AlignCenter);// 4. 创建手机号输入框QString editStyle QLineEdit { border: none; background-color: rgb(240, 240, 240); font-size: 20px; border-radius: 10px; padding-left: 5px;};phoneEdit new QLineEdit();phoneEdit-setPlaceholderText(输入手机号);phoneEdit-setFixedHeight(40);phoneEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);phoneEdit-setStyleSheet(editStyle);// 5. 创建验证码输入框verifyCodeEdit new QLineEdit();verifyCodeEdit-setPlaceholderText(输入短信验证码);verifyCodeEdit-setFixedHeight(40);verifyCodeEdit-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);verifyCodeEdit-setStyleSheet(editStyle);// 6. 创建发送验证码按钮QString btnWhiteStyle QPushButton { border: none; border-radius: 10px; background-color: transparent; };btnWhiteStyle QPushButton:pressed { background-color: rgb(240, 240, 240); };sendVerifyCodeBtn new QPushButton();sendVerifyCodeBtn-setFixedSize(100, 40);sendVerifyCodeBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);sendVerifyCodeBtn-setText(发送验证码);sendVerifyCodeBtn-setStyleSheet(btnWhiteStyle);// 7. 创建提交按钮submitBtn new QPushButton();submitBtn-setFixedHeight(40);submitBtn-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);submitBtn-setText(登录);QString btnGreenStyle QPushButton { border: none; border-radius: 10px; background-color: rgb(44, 182, 61); color: rgb(255, 255, 255); };btnGreenStyle QPushButton:pressed { background-color: rgb(240, 240, 240); };submitBtn-setStyleSheet(btnGreenStyle);// 8. 创建 切换到用户名 模式按钮QPushButton* userModeBtn new QPushButton();userModeBtn-setFixedSize(100, 40);userModeBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);userModeBtn-setText(切换到用户名);userModeBtn-setStyleSheet(btnWhiteStyle);// 9. 切换登录注册模式switchModeBtn new QPushButton();switchModeBtn-setFixedSize(100, 40);switchModeBtn-setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);switchModeBtn-setText(注册);switchModeBtn-setStyleSheet(btnWhiteStyle);// 10. 添加到布局管理器layout-addWidget(titleLabel, 0, 0, 1, 5);layout-addWidget(phoneEdit, 1, 0, 1, 5);layout-addWidget(verifyCodeEdit, 2, 0, 1, 4);layout-addWidget(sendVerifyCodeBtn, 2, 4, 1, 1);layout-addWidget(submitBtn, 3, 0, 1, 5);layout-addWidget(userModeBtn, 4, 0, 1, 1);layout-addWidget(switchModeBtn, 4, 4, 1, 1); }// 切换注册/登录模式 void PhoneLoginWidget::switchMode() {if(isLoginMode){// 切换到注册模式this-setWindowTitle(注册);titleLabel-setText(注册);submitBtn-setText(注册);switchModeBtn-setText(登录);}else{// 切换到登录模式this-setWindowTitle(登录);titleLabel-setText(登录);submitBtn-setText(登录);switchModeBtn-setText(注册);}isLoginMode !isLoginMode; }8. 实现全局通知类 1创建Toast 类 class Toast : public QDialog {Q_OBJECTpublic:// 此处不需要指定父窗口. 全局通知的父窗口就是 桌面.Toast(const QString text);// 并不需要手动来 new 这个对象, 而是通过 showMessage 来弹出窗口static void showMessage(const QString text);};2Toast 类具体实现 Toast::Toast(const QString text) {// 1. 设置窗口的基本参数this-setFixedSize(800, 150);this-setWindowTitle(消息通知);this-setWindowIcon(QIcon(:/resource/image/logo.png));this-setAttribute(Qt::WA_DeleteOnClose);this-setStyleSheet(QDialog { background-color: rgb(255, 255, 255); });// 去掉窗口的标题栏this-setWindowFlags(Qt::FramelessWindowHint);// 2. 先考虑一下窗口的位置.// 获取到整个屏幕的尺寸, 通过 primaryScreen 来获取.QScreen* screen QApplication::primaryScreen();int width screen-size().width();int height screen-size().height();int x (width - this-width()) / 2;int y height - this-height() - 100; // 此处的 100 是窗口底边距离屏幕底边的间隔this-move(x, y);// 3. 添加一个布局管理器QVBoxLayout* layout new QVBoxLayout();layout-setSpacing(0);layout-setContentsMargins(0, 0, 0, 0);this-setLayout(layout);// 4. 创建显示文本的 LabelQLabel* label new QLabel();label-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);label-setAlignment(Qt::AlignCenter);label-setStyleSheet(QLabel { font-size: 32px; });label-setText(text);layout-addWidget(label);// 5. 实现 2s 之后自动关闭.QTimer* timer new QTimer(this);connect(timer, QTimer::timeout, this, {timer-stop();// 核心代码, 关闭当前窗口this-close();});timer-start(2000); }void Toast::showMessage(const QString text) {Toast* toast new Toast(text);toast-show(); }9. 构建界面注意事项 1直接通过 QSS 给 QWidget 设置背景色有时候会失效。尤其是 QWidget 的子类的时候.具体原因还不清楚。官方文档说 原因没有解释 void CustomWidget::paintEvent(QPaintEvent ) {QStyleOption opt;opt.init(this);QPainter p(this);style()-drawPrimitive(QStyle::PE_Widget, opt, p, this); }2QScrollArea 不能通过 QSS 直接设置背景色。要给 QScrollArea 中持有的 QWidget 设置。 3QCheckBox 不能通过 QSS 的 border-radius 设置圆形。形如下列代码, 不能生效 QString style QCheckBox { border-radius: 12.5px; background-color: white; } QCheckBox::indicator { width:25px; height: 25px; border-radius: 12.5px;} ; style QCheckBox::indicator:checked{ color: white; background-color: rgb(7, 193, 96); }; checkBox-setStyleSheet(style);需要使用替换背景图的方式来完成. 4滚动区域代码示例 使用 QScrollBar::vertical 设置垂直滚动条样式。使用 QScrollBar::horizontal 设置水平滚动条样式。使用 QScrollBar::handle:vertical 设置垂直滚动条滑块样式。setWidgetResizable(true) 务必要添加。 QScrollArea scrollArea new QScrollArea(); scrollArea-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // ⼀定要添加这个设置, 否则⽆法正确显⽰. scrollArea-setWidgetResizable(true); // 隐藏⽔平滚动条. 把垂直滚动条设置的细⼀些. scrollArea-verticalScrollBar()-setStyleSheet(QScrollBar:vertical { width: 2px; background-color: rgb(255, 255, 255); } QScrollBar::handle:vertical {background-color: rgb(205, 205, 205);}); scrollArea-horizontalScrollBar()-setStyleSheet(QScrollBar:horizontal { height: 0px;}); scrollArea-setStyleSheet(QScrollArea { border: none; }); selectedContainer new QWidget(); selectedContainer-setObjectName(selectedContainer);selectedContainer-setStyleSheet(#selectedContainer {background-color: rgb(255, 255, 255);}); scrollArea-setWidget(selectedContainer);5针对登录窗口进行 delete 后程序崩溃 使用 deleteLater 也不行。使用 this-setAttribute(Qt::WA_DeleteOnClose)也不行。原因是这个变量 LoginWidget 是在 main 中定义在栈上的不能 delete ! void LoginWidget::switchToPhone() {PhoneLoginWidget* widget new PhoneLoginWidget();widget-show();// 关闭当前窗⼝this-close();// 注意!!! 此处不能 delete, 否则程序会崩溃.// 因为该 LoginWidget 是在 main 中定义在栈上的变量, 是不能 delete 的!delete this;// this-deleteLater(); }10. 将项目所需要的图片导入Qt项目中 1创建qrc目录 2文件名resource
    3将图添加到resource的根目录当中 4后续代码就是前后端交互接口的设计和实现。见博客https://blog.csdn.net/m0_65558082/article/details/143770334?spm1001.2014.3001.5502。 客户端整体代码链接https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client。