一、主界面布局
把MainWidget类做成“单例类”(实现单例模式。让某个类在指定的进程中,只能有唯一实例,我用到的是懒汉模式)
指针指向的唯一实例:
static MainWidget* instance;
声明:
private:
static MainWidget* instance;
public:
static MainWidget* getInstance();
定义:
//给静态成员初始化
MainWidget* MainWidget::instance=nullptr;
MainWidget *MainWidget::getInstance()
{
if (instance == nullptr) {
// 此处不传入参数, 以桌面为父窗口.
// 由于此处的窗口是整个程序的主窗口, 父窗口就设定为桌面, 本身就是常规设定.
instance = new MainWidget();
}
return instance;
}
1.1 窗口左侧部分
mainwidget.h文件
#include <QWidget>
#include <QPushButton>
#include <QLineEdit>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWidget;
}
QT_END_NAMESPACE
class MainWidget : public QWidget
{
Q_OBJECT
private:
static MainWidget* instance;
// 对于单例模式来说, 最关键的部分, 不是 "创建实例" , 而是限制别人创建实例.
MainWidget(QWidget *parent = nullptr);
public:
static MainWidget* getInstance();
~MainWidget();
private:
Ui::MainWidget *ui;
// 窗口最左侧部分
QWidget* windowLeft;
// 窗口中间部分
QWidget* windowMid;
// 窗口右侧部分
QWidget* windowRight;
// 用户头像
QPushButton* userAvatar;
// 会话标签页按钮
QPushButton* sessionTabBtn;
// 好友标签页按钮
QPushButton* friendTabBtn;
// 好友申请标签页按钮
QPushButton* applyTabBtn;
public:
void initMainWindow();
void initLeftWindow();
void initMidWindow();
void initRightWindow();
};
mainwidget.cpp文件
#include "mainwidget.h"
#include "./ui_mainwidget.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QCloseEvent>
//给静态成员初始化
MainWidget* MainWidget::instance=nullptr;
MainWidget *MainWidget::getInstance()
{
if (instance == nullptr) {
// 此处不传入参数, 以桌面为父窗口.
// 由于此处的窗口是整个程序的主窗口, 父窗口就设定为桌面, 本身就是常规设定.
instance = new MainWidget();
}
return instance;
}
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();
}
MainWidget::~MainWidget()
{
delete ui;
}
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->setMinimumWidth(500);
windowLeft->setStyleSheet("QWidget { background-color: rgb(46, 46, 46); }");
windowMid->setStyleSheet("QWidget { background-color: rgb(247, 247, 247); }");
windowRight->setStyleSheet("QWidget { background-color: rgb(225, 225, 225); }");
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);
}
void MainWidget::initMidWindow()
{
}
void MainWidget::initRightWindow()
{
}
展示:
1.2 窗口中间部分
中间部分搜索框和中间部分列表区设置
mainwidget.h文件
// 用户搜索框
QLineEdit* searchEdit;
// 添加好友按钮
QPushButton* addFriendBtn;
SessionFriendArea* sessionFriendArea;
mainwidget.cpp文件
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);
}
注意,列表区需要新建一个sessionFriendArea的qt文件继承QWidget
1.2.1 窗口中间部分sessionFriendArea的实现
包含会话好友列表
sessionFriendArea.h文件
#include <QWidget>
#include <QScrollArea>
#include <QLabel>
//
/// 滚动区域中的 Item 的类型
//
enum ItemType {
SessionItemType,
FriendItemType,
ApplyItemType
};
///
/// 整个滚动区域的实现
///
class SessionFriendArea : public QScrollArea
{
Q_OBJECT
public:
explicit SessionFriendArea(QWidget *parent = nullptr);
private:
// 后续往 container 内部的 layout 中添加元素, 就能够触发 QScrollArea 滚动效果.
QWidget* container;
// 清空该区域中所有的 item
void clear();
// 添加一个 item 到该区域中, itemType 表示添加哪种 item, id 跟着不同的 itemType 有不同的含义.
// 如果是 SessionItem, id 就是 chatSessionId
// 如果是 FriendItem / ApplyItem, id 就是 userId
void addItem(ItemType itemType, const QString& id, const QIcon& avatar, const QString& name, const QString& text);
// 选中某个指定的 item, 通过 index 下标来进行选择
void clickItem(int index);
signals:
};
///
/// 滚动区域中的 Item 的实现
///
class SessionFriendItem : public QWidget {
Q_OBJECT
public:
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 就指向了上述的 SessionFriendArea
QWidget* owner;
// 这个变量用来表示当前 Item 是否是 "选中" 状态
bool selected = false;
protected:
// 让这个成员被子类访问
QLabel* messageLabel;
};
///
/// 会话 Item 的实现
///
class SessionItem : public SessionFriendItem {
Q_OBJECT
public:
SessionItem(QWidget* owner, const QString& chatSessionId, const QIcon& avatar,
const QString& name, const QString& lastMessage);
void active() override;
private:
// 当前会话 id
QString chatSessionId;
};
/
/// 好友 Item 的实现
//
class FriendItem : public SessionFriendItem {
Q_OBJECT
public:
FriendItem(QWidget* owner, const QString& userId, const QIcon& avatar, const QString& name,
const QString& description);
void active() override;
private:
// 好友的用户id
QString userId;
};
//
/// 好友申请 Item 的实现
//
class ApplyItem : public SessionFriendItem {
Q_OBJECT
public:
// 此处不需要显示一个 附加的文本了. 比上面的两个 Item 的构造函数, 少了一个参数
ApplyItem(QWidget* owner, const QString& userId, const QIcon& avatar, const QString& name);
void active() override;
private:
// 申请人的 userId
QString userId;
};
sessionFriendArea.cpp文件
#include <QScrollBar>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QStyleOption>
#include <QPainter>
#include "model/data.h"
#include "debug.h"
SessionFriendArea::SessionFriendArea(QWidget *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();
container->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);
// 简单测试一下滚动的效果, 先去掉, 后续添加 Item
// for (int i = 0; i < 50; ++i) {
// QPushButton* btn = new QPushButton();
// btn->setText("按钮");
// layout->addWidget(btn);
// }
// 构造出一些临时数据, 用来作为 "界面调试" 依据. 后 续要删除掉
#if TEST_UI
QIcon icon(":/resource/image/defaultAvatar.png");
for (int i = 0; i < 30; ++i) {
this->addItem(SessionItemType, 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();
}
///
/// 滚动区域中的 Item 的实现
///
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);
// 创建消息预览的 label
messageLabel = 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, 1);
// 消息预览处于 1, 2 位置, 占据 1 行, 占据 1 列
layout->addWidget(messageLabel, 1, 2, 1, 1);
}
void SessionFriendItem::select()
{
// 鼠标点击时会触发这个函数
// 拿到所有的兄弟元素
const QObjectList children = this->parentWidget()->children();
for (QObject* child : children) {
if (!child->isWidgetType()) {
// 判定是否是 widget.
continue;
}
// 确实是 widget, 就把这里的child 强转成 SessionFriendItem
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;
}
void SessionFriendItem::active()
{
}
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;
// 当前这个 item 是选中状态, 则背景色不受到该逻辑影响
if (this->selected) {
return;
}
// 设置一个更深的颜色
this->setStyleSheet("QWidget { background-color: rgb(215, 215, 215);}");
}
void SessionFriendItem::leaveEvent(QEvent *event)
{
(void) event;
// 当前这个 item 是选中状态, 则背景色不受到该逻辑影响
if (this->selected) {
return;
}
// 还原背景色
this->setStyleSheet("QWidget { background-color: rgb(231, 231, 231);}");
}
///
/// 会话 Item 的实现
///
SessionItem::SessionItem(QWidget *owner, const QString &chatSessionId, const QIcon &avatar, const QString &name, const QString &lastMessage)
: SessionFriendItem(owner, avatar, name, lastMessage), chatSessionId(chatSessionId)
{
}
void SessionItem::active()
{
}
/
/// 好友 Item 的实现
//
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()
{
}
//
/// 好友申请 Item 的实现
//
ApplyItem::ApplyItem(QWidget *owner, const QString &userId, const QIcon &avatar, const QString &name)
:SessionFriendItem(owner, avatar, name, ""), userId(userId)
{
// 1. 移除父类的 messageLabel
QGridLayout* 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()
{
}
1.3 窗口右侧部分
mainwidget.h文件
// 消息展示区
MessageShowArea* messageShowArea;
// 消息编辑区
MessageEditArea* messageEditArea;
mainwidget.cpp文件
// 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);}");
#if TEST_UI
// 为了测试界面临时增加的. 实际这里的内容, 应该是使用从服务器获取的数据来设置.
sessionTitleLabel->setText("这是会话标题");
#endif
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(225, 225, 225); } QPushButton:pressed { background-color: rgb(210, 220, 220); }");
hlayout->addWidget(extraBtn);
// 4. 添加消息展示区
messageShowArea = new MessageShowArea();
vlayout->addWidget(messageShowArea);
// 5. 添加消息编辑区
messageEditArea = new MessageEditArea();
// 确保消息编辑区, 处于窗口的下方.
vlayout->addWidget(messageEditArea, 0, Qt::AlignBottom);
一、额外操作
1.为了设置图标,需要把图标图片导入到项目中,通过qrc文件管理
添加文件,创建文件,然后把需要的图片放入文件夹里,进行打开。
2.设置测试文件debug
然后界面测试
// 构造出一些临时数据, 用来作为 "界面调试" 依据. 后 续要删除掉
#if TEST_UI
QIcon icon(":/resource/image/defaultAvatar.png");
for (int i = 0; i < 30; ++i) {
this->addItem(SessionItemType, QString::number(i), icon, "张三" + QString::number(i), "最后一条消息" + QString::number(i));
}
#endif
3.编辑右侧界面时,新建消息展示区,新建消息编辑区