微服务即时通讯系统的实现(客户端)----(1)

目录

  • 1. 项目整体介绍
    • 1.1 项目概况
    • 1.2 界面预览和功能介绍
    • 1.3 技术重点和服务器架构
  • 2. 项目环境搭建
    • 2.1 安装Qt6
    • 2.3 安装vcpkg
    • 2.3 安装protobuf
    • 2.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:项目工程的⼀键式部署工具。

2. 项目环境搭建

2.1 安装Qt6

(1)Qt6链接:https://www.qt.io/download-qt-installer-oss?hsCtaTracking=99d9dd4f-5681-48d2-b096-470725510d34%7C074ddad0-fdef-4e53-8aa8-5e8a876d6ab4


(2)注册登录Qt账户:


(3)进行到选择组件的时候勾选6.7及以上:


Qt6项目中,一旦代码规模变大了,使用MinGW编译速度就会慢很多。MSVC:vs自带编译器,必须配合VS2019 及其以上版。

(4)将Additional Libraries全部勾选上:

(5)开发工具勾选:


之后的安装只需要一直next即可。

2.3 安装vcpkg

(1)vcpkg 是一个开源的跨平台 C++ 包管理工具,用于简化库的下载、构建和管理过程。它支持多种操作系统,并自动处理依赖关系。参考文档:https://learn.microsoft.com/zh-cn/vcpkg/get_started/get-started?pivots=shell-powershell

(2)打开cmd命令行将vcpkg克隆下来:

git clone https://github.com/microsoft/vcpkg.git

(3)进入vcpkg并运行:

cd vcpkg && bootstrap-vcpkg.bat

2.3 安装protobuf

(1)在cmd命令行当中运行如下代码(必须在vcpkg文件当中运行):

./vcpkg.exe install protobuf protobuf:x64-windows

2.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)如果未能正确配置,会出现形如:


这样的错误。

3. 项目核心数据结构的实现

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_H

4. 界面整体布局的实现

4.1 主页面的布局和实现

(1)布局布局展示:

(2)MainWidget.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;
};

(3)MainWidget.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_cast<SessionFriendItem*>(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_cast<SessionFriendItem*>(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_cast<QGridLayout*>(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_cast<QVBoxLayout*>(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_cast<QWidget*>(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_cast<QWidget*>(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)个人主页的主要界面如下:

(3)selfinfowidget.h的实现:

class SelfInfoWidget : public QDialog
{Q_OBJECT
public:SelfInfoWidget(QWidget* parent);private:QGridLayout* layout;QPushButton* avatarBtn;QLabel* idTag;								// 显示 "序号"QLabel* idLabel;							// 显示 "1234"QLabel* nameTag;							// 显示 "昵称"QLabel* nameLabel; 							// 显示 "张三"QLineEdit* nameEdit;						// 编辑昵称QPushButton* nameModifyBtn;					// 修改名字QPushButton* nameSubmitBtn;					// 提交修改QLabel* descTag;							// 显示 "签名"QLabel* descLabel;							// 显示 "从今天开始认真敲代码"QLineEdit* descEdit;						// 编辑签名QPushButton* descModifyBtn;					// 修改签名QPushButton* descSubmitBtn;					// 提交修改QLabel* phoneTag;							// 显示 "电话"QLabel* phoneLabel;							// 显示 "18612345678"QLineEdit* phoneEdit;						// 编辑电话QPushButton* phoneModifyBtn;				// 修改电话QPushButton* phoneSubmitBtn;				// 提交修改QLabel* verifyCodeTag;						// 显示 "验证码"QLineEdit* verifyCodeEdit;					// 输入验证码QPushButton* getVerifyCodeBtn;				// 获取验证码按钮// 要修改的新的手机号码QString phoneToChange;// 倒计时的时间int leftTime = 30;};

(4)selfinfowidget.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;
};

(3)UserInfoWidget类的具体实现:

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

(3)SessionDetailWidget类的具体实现:

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

(5)AvatarItem类的具体实现:

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();QList<QString> 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;
};

(3)ChooseFriendDialog类的具体实现:

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, [=](bool checked){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_cast<QVBoxLayout*>(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_cast<ChooseFriendItem*>(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_cast<QVBoxLayout*>(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_cast<ChooseFriendItem*>(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;
};

(3)GroupSessionDetailWidget类的具体实现:

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

(3)AddFriendDialog类的具体实现:

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_cast<QVBoxLayout*>(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;
};

(5)FriendResultItem类的具体实现:

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

(3)HistoryMessageWidget类的具体实现:

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_cast<QVBoxLayout*>(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);
};

(5)HistoryItem类的具体实现:

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

(3)LoginWidget类的具体实现:

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

(3)PhoneLoginWidget类的具体实现:

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

(2)Toast 类具体实现:

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

(2)QScrollArea 不能通过 QSS 直接设置背景色。要给 QScrollArea 中持有的 QWidget 设置。

(3)QCheckBox 不能通过 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)后续代码就是前后端交互接口的设计和实现。见博客:(还没有完成)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/471461.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyBatis——增删查改(XML 方式)

1. 查询 1.1. 简单查询 使用注解的方式主要是完成一些简单的增删查改功能&#xff0c;如果要实现复杂的 SQL 功能&#xff0c;还是建议使用 XML 来配置映射语句&#xff0c;将 SQL 语句写在 XML 配置文件中 如果要操作数据库&#xff0c;需要做以下的配置&#xff0c;与注解…

A029-基于Spring Boot的物流管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

华为路由策略配置

一、AS_Path过滤 要求&#xff1a; AR1与AR2、AR2与AR3之间建立EBGP连接 AS10的设备和AS30的设备无法相互通信 1.启动设备 2.配置IP地址 3.配置路由器的EBGP对等体连接&#xff0c;引入直连路由 [AR1]bgp 10 [AR1-bgp]router-id 1.1.1.1 [AR1-bgp]peer 200.1.2.2 as-nu…

如何向函数模块 FM 中传递 Range 参数

有时候需要在选择屏幕之后调用一个函数模块&#xff0c;那么如果利用 SE37 在函数模块定义 Range 参数呢。 解决方法很简单&#xff0c;系统有很多预定义的 Range_* 类型&#xff1a; 如上图&#xff0c;这里有很常用的 Range 结构&#xff0c;如订单号、发票号、公司代码等等…

工作和学习遇到的技术问题

写在前面 记录工作和学习遇到的技术问题,以求再次遇到可以快速解决。 1&#xff1a;Ubuntu TSL换源报错&#xff1a;Err:1 http://mirrors.aliyun.com/ubuntu focal InRelease 执行如下操作&#xff08;已经操作的则忽略&#xff09;&#xff0c;首先在文件/etc/apt/sources…

研究生如何远控实验室电脑?远程办公功能使用教程

如果你是研究生&#xff0c;是不是会遇到需要远程控制实验室电脑进行查看文献、调代码和拉数据的时候&#xff1f;有时候就是这么棘手&#xff0c;不过你可以借助一些工具来帮助你随时随地远控实验室电脑。这样就不用担心导师催促&#xff0c;无法及时完成科研了。常见的工具比…

重卡穿越商都,ROG DAY 2024郑州站高燃来袭

野塘菡萏正新秋,红藕香中过郑州!2024年11月9日~10日,ROG DAY 2024信仰集结的号角正式吹响,首战据点落地郑州局外太格茂。炫酷涂装的战车如同未来战士般震撼登陆,ROG硬核科技闪耀亮相,现场氛围瞬间点燃!活动现场人流不息,年轻学子、数码爱好者、极客玩家、科技博主以及周末悠闲惬…

web安全测试渗透案例知识点总结(上)——小白入狱

目录 一、Web安全渗透测试概念详解1. Web安全与渗透测试2. Web安全的主要攻击面与漏洞类型3. 渗透测试的基本流程 二、知识点详细总结1. 常见Web漏洞分析2. 渗透测试常用工具及其功能 三、具体案例教程案例1&#xff1a;SQL注入漏洞利用教程案例2&#xff1a;跨站脚本&#xff…

浪潮信息“源”Embedding模型登顶MTEB榜单第一名

在自然语言处理&#xff08;NLP&#xff09;和机器学习领域&#xff0c;Embedding模型是将文本数据转换为高维向量表示的核心技术&#xff0c;直接影响NLP任务&#xff08;如文本分类、情感分析等&#xff09;的效果&#xff0c;对于提升模型性能和深入理解文本语义具有至关重要…

catchadmin-webman 宝塔 部署

1&#xff1a;宝塔的php 中删除禁用函数 putenv 问题&#xff1a; 按照文档部署的时候linux&#xff08;php&#xff09; vue (本地) 无法访问后端api/login 的接口 。 解决办法&#xff1a; webman 没有配置nginx 反向代理 配置就能正常访问了

【AutoGen 】简介

学习笔记AutoGen。它可以使用多个代理来开发 LLM 应用程序,这些代理可定制、可相互对话,可在各种模式下运行,且无缝允许人的参与,进一步在更大程度上为开发者提供助力。AutoGen 智能应用开发(一)|AutoGen 基础 学习笔记

【月之暗面kimi-注册/登录安全分析报告】

前言 由于网站注册入口容易被机器执行自动化程序攻击&#xff0c;存在如下风险&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露&#xff0c;不符合国家等级保护的要求。短信盗刷带来的拒绝服务风险 &#xff0c;造成用户无法登陆、注册&#xff0c;大量收到垃圾短信的…

统信UOS开发接口DTK

DTK(Development ToolKit)是基于 Qt 开发的简单且实用的通用开发框架。提供丰富的开发接口与支持工具,能有效提升开发效率。 文章目录 一、简介DTK 常见模块介绍概述二、框架创建开发环境准备使用 cmake三、常见模块窗口和对话框一、简介 DTK 常见模块介绍 概述 DTK(Dev…

城市轨道交通数据可视化的应用与优势

通过图扑可视化技术将复杂的数据转化为易于理解的图像&#xff0c;助力交通管理者优化线路规划、提升运营效率和乘客信息服务。轨道交通管理者能够更直观地分析乘客流量、运营效率等关键指标&#xff0c;从而优化线路设计与调度&#xff0c;提高服务质量&#xff0c;为乘客提供…

【JavaEE初阶 — 多线程】生产消费模型 阻塞队列

1. 阻塞队列 (1) 阻塞队列 1. 概念 阻塞队列是一种特殊的队列&#xff0c;也遵守"先进先出"的原则&#xff1b;阻塞队列能是一种线程安全的数据结构&#xff0c;主要用来阻塞队列的插入和获取操作&#xff1a; 当队列满了的时候&#xff0c;插入操作会被…

重构开发之道,Blackbox.AI为技术注入智能新动力

本文目录 一、引言二、Blackbox.AI实战体验2.1 基于网页界面生成前端代码进行应用开发2.2 与AI助手实现实时智能对话2.3 重塑大型文件交互方式2.4 链接Github仓库进行对话编程 三、总结 一、引言 在生产力工具加速进化的浪潮中&#xff0c;Blackbox.AI开始崭露头角&#xff0c…

idea 弹窗 delete remote branch origin/develop-deploy

想删除远程分支&#xff0c;就选delete&#xff0c;仅想删除本地分支&#xff0c;选cancel&#xff1b; 在 IntelliJ IDEA 中遇到弹窗提示删除远程分支 origin/develop-deploy&#xff0c;这通常是在 Git 操作过程中出现的情况&#xff0c;可能是在执行如 git branch -d 或其他…

第四十五章 Vue之Vuex模块化创建(module)

目录 一、引言 二、模块化拆分创建方式 三、模块化拆分完整代码 3.1. index.js 3.2. module1.js 3.3. module2.js 3.4. module3.js 3.5. main.js 3.6. App.vue 3.7. Son1.vue 3.8. Son2.vue 四、访问模块module的state ​五、访问模块中的getters ​六、mutati…

【OpenEuler】配置虚拟ip

OpenEuler系统手动配置虚ip 介绍操作方法临时生效永久生效 验证 介绍 我们知道通过keepalived服务可以为linux服务器设置虚拟ip&#xff0c;但是有些特殊场景下若无法安装部署keepalived服务&#xff0c;则需要通过手动设置的方式&#xff0c;配置服务器的虚拟ip。 本方案提供…

CCI3.0-HQ:用于预训练大型语言模型的高质量大规模中文数据集

摘要 我们介绍了 CCI3.0-HQ&#xff0c;它是中文语料库互联网 3.0&#xff08;CCI3.0&#xff09;的一个高质量500GB子集&#xff0c;采用新颖的两阶段混合过滤管道开发&#xff0c;显著提高了数据质量。为了评估其有效性&#xff0c;我们在不同数据集的100B tokens上从头开始…