IM项目-----客户端部分未读消息实现
文章目录
- 前言
- 会话好友类
- 滑动区域的Item
- 三个子item
- 会话Item
- 好友Item
- 好友申请Item
前言
客户端部分关于未读消息的实现。
会话好友类
会话好友类继承自QScrollArea类,内部有一个QWidget成员。在Widget内部添加了垂直布局管理器。
可以添加item。
class SessionFriendArea : public QScrollArea
{
Q_OBJECT
public:
explicit SessionFriendArea(QWidget *parent = nullptr);
//清空滑动区域所有的 item
void clear();
//向滑动区域添加一个 item ,这里的id 随着itemType的不同有不同的含义.
//当是SessionItemType时,id代表SessionId ,当时FriendItem和ApplyItem时,代表userId
void addItem(ItemType itemType,const QString &id,const QIcon& avatar, const QString& name, const QString& text);
//通过下标选中滑动区域一个 item
void clieckItem(int index);
private:
//后续的 item 往container 中的 layout 中添加,就可以实现 滑动区域 的效果
QWidget* container;
signals:
};
滑动区域的Item
这个item控件的构造函数中定义了网格布局管理器,添加了头像/昵称/消息的组合。
这个类主要是做界面样式的设计,重写了三个事件:鼠标点击/鼠标移上/鼠标移走。
鼠标移上和鼠标移走主要是通过设置qss来实现背景颜色的修改。
鼠标点击就复杂一点,在类中有一个变量来记录是否被是选中状态。通过接口获取父元素的孩子元素。编译孩子元素,把他们的状态都变为false,并修改qss,把当前item修改为true。
另外,由于直接对QWidget设置qss可能不生效,我们需要重写paintEvent事件。
class SessionFriendItem : public QWidget
{
Q_OBJECT
public:
//owner 是这个item所属的SessionFriendArea
SessionFriendItem(QWidget* owner, const QIcon& avatar, const QString& name, const QString& text);
//选中该控件
void select();
//虚函数,子类来重写
virtual void active();
//重写这个事件,让QSS生效
void paintEvent(QPaintEvent *event);
//鼠标点击事件,让item更改背景色
void mousePressEvent(QMouseEvent *event);
//鼠标移动到该控件的事件
void enterEvent(QEnterEvent *event);
//鼠标从该控件上移开的事件
void leaveEvent(QEvent *event);
private:
//owner 就指向了上述的 SessionFriendArea
QWidget* owner;
//描述了当前 item 是否被选中
bool selected = false;
protected:
//将该控件设为成员变量且访问权限为保护权限 方便好友申请Item 进行删除
QLabel *messageLabel;
};
在父类中有一个select函数,这个函数会在鼠标点击事件中被调用,这个函数中回去调用active函数,而这个active函数父类是虚函数,父类不实现,子类根据自身的逻辑实现自己的功能。
三个子item
由于存在会话item/好友item/和好友申请item三个,所以我们这里使用继承来复用代码。
在滑动区域也是添加三个子类对象,而不是父类。
会话Item
会话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;
void updateLastMessage(const QString& chatSessionId);
private:
// 当前会话 id
QString chatSessionId;
// 最后一条消息的文本预览
QString text;
};
在构造中需要绑定信号槽,当收到消息和发送一条消息时,dataCenter会发送一个updateLastMessage信号,我们需要更新我们的消息预览。
另外我们需要读取当前会话的未读消息条数,显示在消息预览上。
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)
{
DataCenter* dataCenter = DataCenter::getInstance();
connect(dataCenter,&DataCenter::updateLastMessage,this,&SessionItem::updateLastMessage);
//读取未读消息个数
int unreadCount = dataCenter->getUnread(chatSessionId);
if(unreadCount > 0){
this->messageLabel->setText(QString("[未读%1条] ").arg(unreadCount) + text);
}
}
当他被点击时,加载会话最近消息,同时清空未读消息,并更新消息预览。
void SessionItem::active()
{
LOG() << "SessionItem点击触发逻辑" << ", chatSessionId = " << chatSessionId;
//把加载会话最近消息放到 MainWidget 中实现
MainWidget* mainWidget = MainWidget::GetInstance();
mainWidget->loadRecentMessage(chatSessionId);
//清空未读消息
DataCenter::getInstance()->clearUnread(chatSessionId);
this->messageLabel->setText(text);
}
这是新消息到来时的槽函数,需要获取最新一条消息,然后进行UI的更新。
void SessionItem::updateLastMessage(const QString &chatSessionId)
{
//1.判断与当前sessionId是否一致
if(this->chatSessionId != chatSessionId){
return;
}
//2.把最后一条消息获取到
DataCenter* dataCenter = DataCenter::getInstance();
QList<Message> *messageList = dataCenter->getRecentMessageList(chatSessionId);
if(messageList == nullptr || messageList->size() == 0){
return;
}
const Message& lastMessage = messageList->back();
// 3. 明确显示的文本内容
// 由于消息有四种类型.
// 文本消息, 直接显示消息的内容; 图片消息, 直接显示 "[图片]"; 文件消息, 直接显示 "[文件]"; 语音消息, 直接显示 "[语音]"
if (lastMessage.messageType == TEXT_TYPE) {
text = lastMessage.content;
} else if (lastMessage.messageType == IMAGE_TYPE) {
text = "[图片]";
} else if (lastMessage.messageType == FILE_TYPE) {
text = "[文件]";
} else if (lastMessage.messageType == SPEECH_TYPE) {
text = "[语音]";
} else {
LOG() << "错误的消息类型!";
return;
}
// 4. 把这个内容, 显示到界面上
// 针对这里的逻辑, 后续还需要考虑到 "未读消息" 情况. 关于未读消息的处理, 后续编写 "接收消息" 的时候再处理.
if(chatSessionId == dataCenter->getCurrentChatSessionId()){
this->messageLabel->setText(text);
}else{
int unreadCount = dataCenter->getUnread(chatSessionId);
this->messageLabel->setText(QString("[未读%1条] ").arg(unreadCount) + text);
}
}
未读消息的实现,就是通过给会话item连接槽函数,当有新消息时会发送信号,此时消息已经被尾插到列表中,槽函数中就是消息获取,根据消息类型进行判断更改UI。
另外在构造时,会读取会话的未读消息个数进行UI更新。
好友Item
在好友item中只需要实现active就行,构造函数不需要额外操作。
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;
};
点击触发的逻辑就是切换到会话列表。
void FriendItem::active()
{
LOG() << "FriendItem点击触发逻辑, userId = " << userId;
MainWidget* mainWidget = MainWidget::GetInstance();
mainWidget->switchSession(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;
void acceptFriendApply();
void rejectFriendApply();
private:
// 好友的用户id
QString userId;
};
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);
//连接信号槽,处理同意好友申请
connect(acceptBtn,&QPushButton::clicked,this,&ApplyItem::acceptFriendApply);
connect(rejectBtn,&QPushButton::clicked,this,&ApplyItem::rejectFriendApply);
}