IM项目-----客户端部分未读消息实现

时间:2024-11-21 16:27:16

文章目录

  • 前言
  • 会话好友类
  • 滑动区域的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);

}