目录

实现实时qq好友搜索框


项目背景

想要用纯C++实现一个QQ,包括客户端的ui和通信,以及服务端的数据收发通信。

客户端:使用C++的Qt框架实现UI(正在进行),使用自己手写的网络框架进行通信(还未开始),以及手写的bencode编码进本地序列化(已经完成)。

服务端:使用自己造好的网络框架进行数据的收发和通信(还未开始)。

本篇进度:已经实现了登录界面和基本的主界面,本次是想拓展一个实时的搜索框,结果碰到了坑,然后在此写下记录。

目前UI实现情况如下图:

登录界面:

https://img-blog.csdnimg.cn/63bd72e215da4cb8a3660f64175bb058.gif

主界面:

https://img-blog.csdnimg.cn/30906168548f49e4bc2e130f9b2db885.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQytH,size_20,color_FFFFFF,t_70,g_se,x_16

实时QQ好友搜索框实现思路

实现目标:当用户在搜索框输入名字的时候,会实时将当前列表内的包含该名字的好友给列举出来。

实现方式:通过连接信号 QLineEdit::textChanged 来对应用户按钮的存储控件中的槽函数。

具体思路

由于我是通过 QListWidget 存下多个自己重写的 QPushButton 来实现好友列表,故想要进行列表中的删除和增加就是对 QListWidget 增加或删除 QListWidgetItem 。由于 QListWidget 只提供了 QListWidget::addItem 方法进行添加,而这个方法并不能保证在具体哪一行插入,只会在列表的尾部进行添加,所以我通过这样一个方法实现实时的列表搜索项:清空再重新创建符合条件的 QPushButton

实践

实践一:通过保存一份拷贝(扑街bug)

根据这个思路,我很快想到,需要保存好搜索前的所有列表项,然后再通过用户输入的文字筛选,将符合条件的列表项添加进去。

这样很快就出现了问题:由于清空列表会导致列表项里面的所有内存都被析构!所以如果你去按照这样一个思路去实现,我们需要在每次调用 addUserBtn 添加用户按钮的时候存下一份拷贝,然后你把列表清空后内存析构也一直能用这一份拷贝再生成新的拷贝按钮来添加到列表中去,这样问题应该就解决了。

然后我马不停蹄的马上写好了代码,跑起来准备测试,立马出现新的问题:每次输入时确实会显示对应的用户按钮是没错,但图片都没了,甚至很多按钮是空的???

经过注意排查,发现是我写拷贝构造函数的时候把很多 QString 成员转了右值用了移动构造,所以每次发送拷贝时,实际上资源已经转移了。。。

改好这个bug,又继续测试,然后果不其然,又出现了新的问题:用户输入的时候会有符合条件的项被创建没错,且都有对应的数据,但是随着继续输入创建的对象是越来越多了???

又经过仔细排查,发现是因为每次 addUserBtn 的时候都会把对应的数据添加一份拷贝到缓存,但是如果是用户在输入框立马输入信息进行查询时,调用了这个add函数,那么就会不断的添加重复的信息到缓存中去!!!

此时很多人可能会想着用 set 替代 list 做缓存,但是我就此发现了我这个实现思路就很有问题:

一、每次重新添加控件需要把整个信息全拷贝一遍,如果数据多了会是一个很耗时的工作!

二、我发现了罪恶之源:QListWidget 把控件的生命周期牢牢把握!!!要不是每次删除控件都得把立马的东西析构才能让这个控件不显示,才不会出现这等问题!也才不会需要重复不断的创建对象!

实践二:通过前后端分离+智能指针完美解决

这次推倒重来,前面的试探也并非是在做无用功,至少通过前面的实践发现,既然我们没法把 Button 的生命周期拿在手里(每次还是需要重新new出新的Button),但我们至少把 Button 里数据的生命周期拿在手里啊,我们利用数据和界面分离的思想,将数据以一个智能指针的方式保存在 Button 内部,这样一来按钮被析构,数据也不一定就会被析构,只有当智能指针的引用计数为0的时候才被析构。

所以回到前面的实践一,这次我们还是以一个缓存存下原本的数据,但是这次我们存储的不是 Button 对象,而是 Button 对象中的智能指针数据,所以只要缓存还存在,那么 Button 里面的数据就还是可以复用的!所以每次 new 出新的 Button 去显示的代价是很小的 ,因为指针的copy几乎是没有代价的。。。

而且如果用set存储指针的话,直接通过判断地址就能清楚是否是相同的数据了!这样的存储性能会比直接存下数据要高很多。

像这样把数据和界面分离开的思路,我觉得就和web端的前后端分离的思路是一模一样,所以我愿意称之为桌面端的前后端分离

附录

最后我贴上我具体实现的代码:

能看懂的应该会有收获,看不懂以后总会看得懂😁

UserButton.h

class UserButton :public QPushButton{
    Q_OBJECT

private:
    UserButton(QWidget*parent);
public:
    
    //按钮的类型
    enum class Type{
        Meg_Btn,
        User_Btn
    };

    //存储按钮数据的类型
    struct ButtonInfo{
        int _unread = 0;
        int _hide = 0;
        int _uid = 0;
        State _state = State::OffLine;
        Type _type = Type::Meg_Btn;
        QString _iconPath;
        QString _name;
        QString _text;
        static std::shared_ptr<ButtonInfo> getDefaultButtonInfo(QString const&name,QString const&text,QString const&iconPath);
    };
    using shared_info = std::shared_ptr<ButtonInfo>;

    //根据外界数据创建按钮
    static UserButton* fromButtonInfo(shared_info const& info,QWidget* parent=nullptr);

    /**
     * 持久化成功返回存储的路径,否则返回空optional
     * @param imageSrc
     * @param name
     * @return
     */
   static std::optional<QString> StorageIcon(QByteArray const&imageSrc,QString const& name);

   
    ~UserButton() override;

protected:
    void paintEvent(QPaintEvent *) override;

    
public:
    //画出Message按钮
    void PainMessageBtn();
	//画出User按钮
    void PainUserBtn();

    QRect adjustSize(QString&dest);

    //一大堆getter和setter
    QString getName();
    
    void setName(QString name);

    int getNumUnread() const;

    void setNumUnread(int numUnread);

    State getMState() const;

    void setMState(State mState);

    const QString &getMIconPath() const;

    void setMIconPath(const QString &mIconPath);

    Type getMType() const;

    void setMType(Type mType);

    void setHideMsgStyle(bool isHide);

    int getMisHide();
    
    int getUid() const;
    
    void setUid(int uid);

    void setContent(QString const& text);

    QString getContent();

    shared_info getSharedInfo();
public slots:
    //用于外界实时更新消息框的显示内容的槽函数
    void updateContent(QString const& text);


private:
    //数据
    shared_info m_info_;
};

UserButton.cpp

//
// Created by Alone on 2022-4-18.
//

#include "UserButton.h"
#include <QFile>
#include <QByteArray>
#include <utility>
#include <QDir>
#include <config_path.h>
#include <iostream>
#include <QPainter>



//方便进行测试的信shared_info快速创建
UserButton::shared_info UserButton::ButtonInfo::getDefaultButtonInfo(const QString &name,
                                                                                     const QString &text,
                                                                                     const QString &iconPath) {

    auto* src = new UserButton::ButtonInfo;
    src->_name = name;
    src->_text = text;
    src->_iconPath = iconPath;
    return std::shared_ptr<UserButton::ButtonInfo>(src);
}

//qss样式
inline QString My_StyleSheet(QString const& icon_path){
        return
        QString(R"(
QPushButton{
border:none;
    image: url(%1);
    image-position:left;
    min-width:120px;
    padding-top:7px;
    padding-left:10px;
    padding-bottom:7px;
    background:rgb(248, 249, 249);
}
QPushButton:hover{
border:none;
 background:rgb(242, 242, 242);
}
QPushButton:checked{
border:none;
background:rgb(235, 235, 235);
}
)").arg(icon_path);
}

UserButton::UserButton(QWidget *parent): QPushButton(parent) {
    setCheckable(true);
    setAutoExclusive(true);
}

UserButton::~UserButton() {

}

UserButton* UserButton::fromButtonInfo(shared_info const& info,QWidget* parent){
    auto* btn = new UserButton(parent);
    btn->m_info_ = info;
    btn->setStyleSheet(My_StyleSheet(btn->getMIconPath()));
    return btn;
}

//调整内容显示
QRect UserButton::adjustSize(QString &dest){
    assert(m_info_!=nullptr);
    // 三档调节
    int num_width = 0;
    int span = 13;
    if(m_info_->_unread<10){
        dest = QString::number(m_info_->_unread);
        num_width = span;
    }else if(m_info_->_unread<99){
        dest = QString::number(m_info_->_unread);
        num_width = span*2;
    }else{
        dest.append("99+");
        num_width = span*3;
    }
    return {this->width()-10-num_width,this->height()/2+5,num_width,13};
}

//根据不同的按钮进行不同的绘制
void UserButton::paintEvent(QPaintEvent *e) {
    assert(m_info_!=nullptr);

    QPushButton::paintEvent(e);
    switch (m_info_->_type) {
        case Type::User_Btn:
            PainUserBtn();
            break;
        case Type::Meg_Btn:
            PainMessageBtn();
            break;
    }
}


//持久化二进制数据到本地(主要是图片)
std::optional<QString> UserButton::StorageIcon(QByteArray const& imageSrc,QString const& name) {
    QString base_path = QDir::homePath()+DirName_Base;
    QString user_icon_path = base_path +DirName_Icon;
    //如果两级文件夹不存在,则先创建文件夹
    QDir dir(base_path);
    if(!dir.exists()){
        qDebug()<<"base_dir not exists,being created";
        if(!dir.mkdir(base_path) )
            return {};
    }
    dir.setPath(user_icon_path);
    if(!dir.exists() ) {
        qDebug()<<"icon_dir not exists,being created";
        if(!dir.mkdir(user_icon_path))
            return {};
    }
    //开始写入本地持久化
    QString user_icon = user_icon_path+"/"+name;
    qDebug()<<user_icon;
    QFile file(user_icon);
    file.open(QIODevice::WriteOnly);
    file.write(imageSrc);

    return user_icon;
}


QString UserButton::getName() {
    assert(m_info_!=nullptr);
    return m_info_->_name;
}

void UserButton::setName(QString name) {
    assert(m_info_!=nullptr);
    m_info_->_name = std::move(name);
}

int UserButton::getNumUnread() const {
    assert(m_info_!=nullptr);
    return m_info_->_unread;
}

void UserButton::setNumUnread(int numUnread) {
    assert(m_info_!=nullptr);
    m_info_->_unread = numUnread;
}

State UserButton::getMState() const {
    assert(m_info_!=nullptr);

    return m_info_->_state;
}

void UserButton::setMState(State mState) {
    assert(m_info_!=nullptr);

    m_info_->_state = mState;
}

const QString &UserButton::getMIconPath() const {
    assert(m_info_!=nullptr);

    return m_info_->_iconPath;
}

void UserButton::setMIconPath(const QString &mIconPath) {
    assert(m_info_!=nullptr);

    m_info_->_iconPath = mIconPath;
}

UserButton::Type UserButton::getMType() const {
    assert(m_info_!=nullptr);

    return m_info_->_type;
}

void UserButton::setMType(UserButton::Type mType) {
    assert(m_info_!=nullptr);

    m_info_->_type = mType;
}

void UserButton::setHideMsgStyle(bool isHide) {
    assert(m_info_!=nullptr);

    m_info_->_hide = isHide;
}

int UserButton::getUid() const {
    assert(m_info_!=nullptr);

    return m_info_->_uid;
}

void UserButton::setUid(int uid) {
    assert(m_info_!= nullptr);

    m_info_->_uid = uid;
}

void UserButton::setContent(const QString &text) {
    assert(m_info_!=nullptr);

    m_info_->_text = text;
}

QString UserButton::getContent() {
    assert(m_info_!=nullptr);

    return m_info_->_text;
}

int UserButton::getMisHide() {
    assert(m_info_!=nullptr);
    return m_info_->_hide;
}

UserButton::shared_info UserButton::getSharedInfo() {
    return m_info_;
}



void UserButton::PainMessageBtn() {

    QPainter painter(this);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);//消锯齿
    QRect pos_round(this->width()-25,this->height()/2+5,12,12);
    QRect pos_time(this->width()-50,this->height()/2-15,40,10);
    QRect pos_name(65,0,this->width()-pos_time.width()-5,28);
    QRect pos_content(65,28,this->width()-pos_time.width()-5,15);
    QColor color_time(134, 139, 152);
    QFont font_time("微软雅黑", 9);
    QString time_cur = QDateTime::currentDateTime().toString("hh:mm");

    QString m_name = getName();
    if(!m_name.isEmpty()){
        //画名字
        int max_size = 6;
        QPen pen;
        pen.setColor(QColor(4, 8, 26));
        painter.setPen(pen);
        painter.setFont(QFont("微软雅黑",14));
        QString name;
        name.reserve(max_size);
        if(m_name.size()>max_size){
            std::copy(m_name.begin(), m_name.begin()+max_size, std::back_inserter(name));
            name.append("...");
        }else{
            name = m_name;
        }
        painter.drawText(pos_name,name,QTextOption(Qt::AlignLeft));

        //画内容
        max_size = 10;
        pen.setColor(QColor(164, 176, 190));
        painter.setPen(pen);
        painter.setFont(QFont("微软雅黑",10));
        QString content;
        content.reserve(max_size);
        QString m_Content = getContent();
        if(m_Content.size()>max_size){
            std::copy(m_Content.begin(), m_Content.begin()+max_size, std::back_inserter(content));
            content.append("...");
        }else{
            content = std::move(m_Content);
        }
        painter.drawText(pos_content,content,QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
    }

    //画右边的提示消息,以及对应颜色提示小球
    State m_state = getMState();
    int m_isHide = getMisHide();

    if(m_state == State::MegTip){
        //TODO 注意根据未读消息的多少调整区域的大小
        QString text_unreadNum;
        if(m_info_->_unread>0){
            pos_round = adjustSize(text_unreadNum);
        }
        QColor color_tmp(246, 75, 49);
        painter.setBrush(QBrush(color_tmp));
        painter.setPen(Qt::NoPen);
        painter.drawRoundedRect(pos_round,6,6);

        if(!text_unreadNum.isEmpty()){
            //画未读信息数目
            QPen pen(QColor(255,255,255));
            painter.setPen(pen);
            painter.setFont(QFont("微软雅黑",8,550));
            painter.drawText(pos_round,text_unreadNum,QTextOption(Qt::AlignHCenter | Qt::AlignVCenter));
        }
    }else if(m_state == State::OffLine && !m_isHide){
        QColor color_tmp(186, 183, 180);
        painter.setBrush(QBrush(color_tmp));
        painter.setPen(Qt::NoPen);
        painter.drawRoundedRect(pos_round,6,6);
    }else if(m_state == State::OnLine && !m_isHide){
        QColor color_tmp(75, 209, 102);
        painter.setBrush(QBrush(color_tmp));
        painter.setPen(Qt::NoPen);
        painter.drawRoundedRect(pos_round,6,6);
    }

    //画时间
    QPen pen(color_time);
    painter.setPen(pen);
    QTextOption option(Qt::AlignRight | Qt::AlignVCenter);
    option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    painter.setFont(font_time);
    painter.drawText(pos_time,time_cur,option);
}

void UserButton::PainUserBtn() {
    QPainter painter(this);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);//消锯齿
    QRect pos_round(this->width()-25,this->height()/2+5,12,12);
    QRect pos_name(65,0,this->width()-pos_round.width()-5,50);
    QFont font_time("微软雅黑", 9);

    QString m_name = getName();
    if(!m_name.isEmpty()){
        //画名字
        int max_size = 6;
        QPen pen;
        pen.setColor(QColor(4, 8, 26));
        painter.setPen(pen);
        painter.setFont(QFont("微软雅黑",14));
        QString name;
        name.reserve(max_size);
        if(m_name.size()>max_size){
            std::copy(m_name.begin(), m_name.begin()+max_size, std::back_inserter(name));
            name.append("...");
        }else{
            name = m_name;
        }

        painter.drawText(pos_name,name,QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
    }

    State m_state = getMState();
    //好友列表中不需要画时间
    if(m_state == State::OffLine){
        QColor color_tmp(186, 183, 180);
        painter.setBrush(QBrush(color_tmp));
        painter.setPen(Qt::NoPen);
        painter.drawRoundedRect(pos_round,6,6);
    }else if(m_state == State::OnLine){
        QColor color_tmp(75, 209, 102);
        painter.setBrush(QBrush(color_tmp));
        painter.setPen(Qt::NoPen);
        painter.drawRoundedRect(pos_round,6,6);
    }
}

void UserButton::updateContent(QString const& text){
    if(m_info_){
        m_info_->_text = text;
    }
}