Chap17-SearchUsers

Chap17-SearchUsers

如标题所见,这一节是关于好友搜索的。分为前端和后端的实现。在此之前,我要先修改mysql的底层封装,即抛弃c的官方api转而使用mysql++的封装。原因很简单,c的api虽然简单,但是代码及流程繁杂无比,为了简化mysql的使用,转而使用cpp的客户端。

mysql++的使用

安装mysql++

sudo pacman -S mysql++

下载源码

./configure

make

sudo make install

基本使用流程

#include<mysql++/mysql++.h>

// 1.连接
mysqlpp::Connection conn(false);
conn.connect(
	"chat_server", // 数据库
    "localhost" ,  // 地址
    "username" ,   // 用户
    "password" ,   // 密码
    "port" ,       // 端口
);

if (!conn.connected()){
    ...
}

// 2.查询
mysqlpp::Query query = conn.query();
query << "Select id,name,email form users";

// 3.存储结果
mysqlpp::StoreQueryResult res = query.store();

// 4.处理结果
std::cout << res.size() << "条记录" << std::endl;

for(std::size_t i = 0;i<res.size();++i){
    std::cout << res[i]["id"] << ","
        	  << res[i]["name"] << ","
        	  << res[i]["email"] << std::endl;
}



// 5.插入
mysqlpp::Query query = conn.query();
query << "insert into users (name,email) values("
      << mysqlpp::quote << name << ","
      << mysqlpp::quote << email <<")";

mysqlpp::SimpleResult res = query.execute();
if (res){
    std::cout << "影响" <<res.rows() << std::endl;
    std::cout << "插入ID:" << res.insert_id() << std::endl;
}

// 6.预处理语句
mysqlpp::Query query = conn.query();
query << "Select * from users where name = %0q and email = %1q";

query.parse();
mysqlpp::StoreQueryResult res;

mysqlpp::SSQLS::execute(query,res,"John","vivek@163.com");

for(const auto&row:res){
    std::cout << "找到用户:" << row["name"] << std::endl;
}


// 7.事务处理
try{
    // 开始事务
	mysqlpp::Transaction trans(conn);
    
    mysqlpp::Query query = conn.query();
    
    // 执行多个操作
    query << "";
    query.execute();
    
    query << "";
    query.execute();
    
    // 事务提交
    trans.commit();
}catch(const mysqlpp::Execption&e){
    std::cerr << "事务错误" << std::endl;
}

// 8.SSQLS
#include<mysql++/ssqls.h>

// 定义与表结构相对应的结构体
sql_create_3(users,1,3,
	mysqlpp::sql_int ,id,
	mysqlpp::sql_varchar ,name,
	mysqlpp::sql_varchat ,email
);

try{
    mysqlpp::Query query = conn.query();
    
    std::vector<users>user_list;
    query << "Select * from users";
    query.storein(user_list);
    
    for(const auto&user:user_list){
        std::cout << ....
    }
    
    users new_user;;
    new_user.name = ...;
    new_user.email = ...;
    
    query.insert(new_user);
    query.execute();
}catch(const mysqlpp::Exception &e){
    ...
}

后端

MysqlDao的修改

由于我们是分层设计的,MysqlManager是使用者调用的接口,我们不需要改动,只需要修改Dao层即可。

头文件中,我们替换了MysqlCApi的MYSQL*连接对象,也移除了自定义的删除器,换成了mysql++的mysqlpp::Connection。

class MysqlPool {
public:
    MysqlPool(const std::string& url, const std::string& user, const std::string& password, const std::string& schedma, const std::string& port, int poolSize = std::thread::hardware_concurrency());

    std::unique_ptr<mysqlpp::Connection> GetConnection() noexcept;
    void ReturnConnection(std::unique_ptr<mysqlpp::Connection> conn) noexcept;
    void Close() noexcept;
    ~MysqlPool();

private:
    std::string _schedma;
    std::string _user;
    std::string _password;
    std::string _url;
    std::string _port;
    std::size_t _poolSize;
    std::queue<std::unique_ptr<mysqlpp::Connection>> _connections;
    std::mutex _mutex;
    std::condition_variable _cv;
    std::atomic<bool> _stop;
};

struct UserInfo;
class MysqlDao {
public:
    MysqlDao();
    ~MysqlDao();
    int TestUidAndEmail(const std::string& uid, const std::string& email);
    int RegisterUser(const std::string& name, const std::string& email, const std::string& password);
    int ResetPassword(const std::string& email, const std::string& password);
    bool CheckPwd(const std::string& user, const std::string& password, UserInfo& userInfo);
    std::shared_ptr<UserInfo> GetUser(int uid);
    std::vector<std::shared_ptr<UserInfo>> GetUser(const std::string& name);

private:
    std::unique_ptr<MysqlPool> _pool;
};

#endif

其次是cpp的改动,可以看到我们的代码非常的简洁。总体思路变得非常的简单,先获取连接对象,创建一个defer保证能够Return回去这个对象。然后写入查询语句,执行,处理结果。

部分查询中,由于还未创建对应的数据库字段,比如desc,icon等,所以并未设置。

#include "MysqlDao.h"
#include "../data/UserInfo.h"
#include "../global/ConfigManager.h"
#include "../global/const.h"
#include <exception>
#include <iostream>
#include <mariadb_com.h>
#include <mysql++/connection.h>
#include <mysql++/result.h>
#include <spdlog/spdlog.h>
#include <string>

MysqlPool::MysqlPool(const std::string& url, const std::string& user, const std::string& password, const std::string& schedma, const std::string& port, int poolSize)
    : _url(url)
    , _user(user)
    , _password(password)
    , _schedma(schedma)
    , _port(port)
    , _poolSize(poolSize)
    , _stop(false)
{
    for (std::size_t i = 0; i < _poolSize; ++i) {
        try {
            auto conn = std::make_unique<mysqlpp::Connection>();
            if (conn->connect(_schedma.c_str(), _url.c_str(), _user.c_str(), _password.c_str(), std::stoi(_port))) {
                _connections.push(std::move(conn));
            } else {
                SPDLOG_ERROR("Failed To Create Database Connection: {}", conn->error());
            }
        } catch (const mysqlpp::Exception& e) {
            SPDLOG_ERROR("Failed to connect to mysql:{}", e.what());
        }
    }

    if (_connections.size() < _poolSize) {
        SPDLOG_WARN("Connection Pool Initialized With Only {}/{} Connections",
            _connections.size(), _poolSize);
    } else {
        SPDLOG_INFO("Mysql Connection Pool Initialized");
    }
}

MysqlPool::~MysqlPool()
{
    Close();
}

std::unique_ptr<mysqlpp::Connection> MysqlPool::GetConnection() noexcept
{
    std::unique_lock<std::mutex> lock(_mutex);
    _cv.wait(lock, [this] {
        return _stop || !_connections.empty();
    });
    if (_stop) {
        return nullptr;
    }
    auto conn = std::move(_connections.front());
    _connections.pop();
    return conn;
}

void MysqlPool::ReturnConnection(std::unique_ptr<mysqlpp::Connection> conn) noexcept
{
    std::unique_lock<std::mutex> lock(_mutex);
    if (_stop) {
        return;
    }
    _connections.push(std::move(conn));
    _cv.notify_one();
}

void MysqlPool::Close() noexcept
{
    std::unique_lock<std::mutex> lock(_mutex);
    _stop = true;
    _cv.notify_all();
    while (!_connections.empty()) {
        auto p = std::move(_connections.front());
        _connections.pop();
    }
}

MysqlDao::MysqlDao()
{
    auto& cfgMgr = ConfigManager::GetInstance();
    const auto& host = cfgMgr["Mysql"]["host"];
    const auto& port = cfgMgr["Mysql"]["port"];
    const auto& schema = cfgMgr["Mysql"]["schema"];
    const auto& password = cfgMgr["Mysql"]["password"];
    const auto& user = cfgMgr["Mysql"]["user"];
    _pool = std::make_unique<MysqlPool>(host, user, password, schema, port);
}

MysqlDao::~MysqlDao()
{
    _pool->Close();
}

int MysqlDao::TestUidAndEmail(const std::string& uid, const std::string& email)
{
    auto conn = _pool->GetConnection();
    if (!conn) {
        return -1;
    }
    Defer defer([this, &conn] {
        _pool->ReturnConnection(std::move(conn));
    });

    try {
        mysqlpp::Query query = conn->query();
        query << "select * from user where uid = %0q or email = %1q";
        query.parse();

        mysqlpp::StoreQueryResult res = query.store(uid, email);

        std::size_t count = res.num_rows();
        if (count != 1) {
            return -1;
        }
        return 1;
    } catch (const std::exception& e) {
        SPDLOG_ERROR("MySQL++ exception in TestUidAndEmail: {}", e.what());
        return -1;
    }
}

int MysqlDao::RegisterUser(const std::string& name, const std::string& email, const std::string& password)
{
    auto conn = _pool->GetConnection();
    if (!conn) {
        return -1;
    }
    Defer defer([&conn, this] {
        _pool->ReturnConnection(std::move(conn));
    });

    try {
        mysqlpp::Transaction trans(*conn);
        mysqlpp::Query query = conn->query();

        // 先检查是否用户已经存在
        query << "SELECT id FROM user WHERE name = %0q OR email = $1q FOR UPDATE";
        auto check_result = query.store(name, email);

        if (check_result && check_result.num_rows() > 0) {
            trans.rollback();
            return -1;
        }

        // 如果不存在就插入,注册成功
        query << "INSERT INTO user (name, email, password) VALUES (?, ?, ?)";
        query.parse();
        auto insert_result = query.execute(name, email, password);

        if (insert_result) {
            int new_id = query.insert_id();
            trans.commit();
            return new_id;
        } else {
            trans.rollback();
            return -1;
        }

    } catch (const mysqlpp::Exception& e) {
        SPDLOG_ERROR("Exception: {}", e.what());
        return -1;
    }
}

int MysqlDao::ResetPassword(const std::string& email, const std::string& password)
{
    auto conn = _pool->GetConnection();
    if (!conn) {
        return -1;
    }
    Defer defer([this, &conn]() {
        _pool->ReturnConnection(std::move(conn));
    });
    if (!conn) {
        SPDLOG_ERROR("Failed to get connection from pool");
        return -1;
    }

    try {
        // 使用 mysql++ 预处理语句
        mysqlpp::Query query = conn->query();
        query << "UPDATE user SET password = ? WHERE email = ?";

        // 执行预处理更新
        mysqlpp::SimpleResult res = query.execute(password, email);

        if (res) {
            int affected_rows = res.rows();
            if (affected_rows > 0) {
                SPDLOG_INFO("Password reset successfully for email: {}, affected rows: {}", email, affected_rows);
                return 1; // 成功重置密码
            } else {
                SPDLOG_WARN("No user found with email: {}", email);
                return 0; // 没有找到用户,返回0
            }
        } else {
            SPDLOG_ERROR("Failed to reset password: {}", query.error());
            return -1;
        }

    } catch (const mysqlpp::BadQuery& e) {
        SPDLOG_ERROR("Bad query in ResetPassword: {}", e.what());
        return -1;
    } catch (const mysqlpp::Exception& e) {
        SPDLOG_ERROR("MySQL++ exception in ResetPassword: {}", e.what());
        return -1;
    } catch (const std::exception& e) {
        SPDLOG_ERROR("Exception in ResetPassword: {}", e.what());
        return -1;
    }
}

bool MysqlDao::CheckPwd(const std::string& user, const std::string& password, UserInfo& userInfo)
{
    auto conn = _pool->GetConnection();
    if (!conn) {
        return false;
    }

    Defer defer([this, &conn]() {
        _pool->ReturnConnection(std::move(conn));
    });

    if (!conn) {
        SPDLOG_ERROR("Failed to get connection from pool");
        return false;
    }
    try {
        mysqlpp::Query query = conn->query();
        query << "Select * from user where name = ? or email = ?";
        mysqlpp::StoreQueryResult res = query.store(user, user);
        std::size_t count = res.num_rows();
        if (count != 1) {
            return false;
        }
        return true;
    } catch (const mysqlpp::BadQuery& e) {
        SPDLOG_ERROR("Bad query in CheckPwd: {}", e.what());
        return false;
    } catch (const mysqlpp::Exception& e) {
        SPDLOG_ERROR("MySQL++ exception in CheckPwd: {}", e.what());
        return false;
    } catch (const std::exception& e) {
        SPDLOG_ERROR("Exception in CheckPwd: {}", e.what());
        return false;
    }
}

std::shared_ptr<UserInfo> MysqlDao::GetUser(int uid)
{
    auto conn = _pool->GetConnection();
    if (!conn) {
        SPDLOG_ERROR("Failed to get connection from pool");
        return nullptr;
    }

    try {
        mysqlpp::Query query = conn->query();
        query << "SELECT name, email, password FROM user WHERE uid = ?";

        mysqlpp::StoreQueryResult res = query.store(uid);
        
        if (res && res.num_rows() == 1) {
            auto user_info = std::make_shared<UserInfo>();
            user_info->uid = uid;
            user_info->name = std::string(res[0]["name"]);
            user_info->email = std::string(res[0]["email"]);

            _pool->ReturnConnection(std::move(conn));
            return user_info;
        } else {
            _pool->ReturnConnection(std::move(conn));
            SPDLOG_DEBUG("User not found with uid: {}", uid);
            return nullptr;
        }

    } catch (const mysqlpp::Exception& e) {
        SPDLOG_ERROR("MySQL++ exception: {}", e.what());
        if (conn)
            _pool->ReturnConnection(std::move(conn));
        return nullptr;
    }
}

std::vector<std::shared_ptr<UserInfo>> MysqlDao::GetUser(const std::string& name)
{
    auto conn = _pool->GetConnection();
    if (!conn) {
        SPDLOG_ERROR("Failed to get connection from pool");
        return {};
    }

    Defer defer([this, &conn]() {
        _pool->ReturnConnection(std::move(conn));
    });

    try {
        mysqlpp::Query query = conn->query();

        // 使用预处理语句进行模糊查询
        query << "SELECT * FROM user WHERE name LIKE ?";

        std::string search_pattern = "%" + name + "%";
        mysqlpp::StoreQueryResult res = query.store(search_pattern);

        std::vector<std::shared_ptr<UserInfo>> users;

        if (res) {
            users.reserve(res.num_rows()); // 预分配内存
            for (size_t i = 0; i < res.num_rows(); ++i) {
                auto user_info = std::make_shared<UserInfo>();
                user_info->uid = res[i]["uid"];
                user_info->name = std::string(res[i]["name"]);
                user_info->email = std::string(res[i]["email"]);

                users.push_back(user_info);
            }
            SPDLOG_DEBUG("Found {} users matching pattern: '{}'", users.size(), search_pattern);
        }
        return users;
    } catch (const mysqlpp::Exception& e) {
        SPDLOG_ERROR("MySQL++ exception: {}", e.what());
        return {};
    }
}

LogicSystem(ChatServer)

首先看基本的处理用户搜索回调的函数,先解析得到的字符串,根据要查询的uid(toUid),先判断是否是纯数字,然后进入GetSearchedUsers进行处理。

先要说明,我们在搜索用户的回包json格式:

{
    "error":"0",
    "fromUid":"0",
    "toUid":"1",
    "users":[
        {
          "uid":"",
          "email":"",
          "status":"",
          ....
        },
        ...
    ],
}

也就是说,我们的用户信息是放在在数组中的。原因是涉及到name的模糊查询。如果uid精确匹配,确实只有一个用户结果,但如果是name模糊查询可能能有多个结果。因此为了扩展性,我们放置在了数组中。

/**
     * @brief 搜索用户回调函数
     *
     */
    _function_callbacks[MsgId::ID_SEARCH_USER_REQ] = [this](std::shared_ptr<Session> session, uint16_t msg_id, const std::string& msg) {
        json j = json::parse(msg);
        j["error"] = static_cast<int>(ErrorCodes::SUCCESS);
        SPDLOG_INFO("json:{}", j.dump());
        auto uid_str = j["toUid"].get<std::string>();
        Defer defer([this, session, &j]() {
            session->Send(j.dump(), static_cast<int>(MsgId::ID_SEARCH_USER_RSP));
        });

        bool only_digit = isPureDigit(uid_str);

        GetSearchedUsers(uid_str, j, only_digit);
    };

此处贴出isPureDigit函数参考

bool LogicSystem::isPureDigit(const std::string& str)
{
    if (str.empty())
        return false;
    return std::all_of(str.begin(), str.end(), [](char c) { return std::isdigit(c); });
}

接下来是处理的大头GetSearchedUsers.

前面的解析,处理和Defer不必多数,下面的分支主要是根据only_digit,判断,如果是纯数字,就精确匹配(uid唯一,应该只有一个结果),如果非纯数字,我们就模糊匹配(判断为name)。

在每个分支内部又分为两个分支:Redis查询到/未查询到。如果查询到直接返回,未查询到,那么就先进行mysql的查询,然后再写入Redis。

void LogicSystem::GetSearchedUsers(const std::string& uid, json& j, bool only_digit)
{
    // 根据only决定使用uid还是name搜索
    j["error"] = ErrorCodes::SUCCESS;
    std::string base_key = USER_BASE_INFO_PREFIX + uid;
    std::string info_str = "";
    json users = json::array();

    Defer defer([this, &j, users = &users]() {
        j["users"] = *users;
    });

    if (only_digit) {
        bool b_base = RedisManager::GetInstance()->Get(base_key, info_str);
        if (b_base) {
            json jj = json::parse(info_str);
            users.push_back(jj);
            return;
        } else {
            std::shared_ptr<UserInfo> user_info = nullptr;
            user_info = MysqlManager::GetInstance()->GetUser(std::stoi(uid));
            if (user_info == nullptr) {
                j["error"] = ErrorCodes::ERROR_UID_INVALID;
                return;
            }
            json jj;
            jj["uid"] = user_info->uid;
            jj["name"] = user_info->name;
            jj["email"] = user_info->email;
            jj["nick"] = user_info->nick;
            jj["sex"] = user_info->sex;
            jj["desc"] = user_info->desc;
            jj["icon"] = user_info->icon;
            RedisManager::GetInstance()->Set(base_key, jj.dump());
            users.push_back(jj);
            return;
        }
    } else {
        // 通过name查询
        std::string name_key = USER_BASE_INFOS_PREFIX + uid;
        std::string name_str = "";
        bool b_base = RedisManager::GetInstance()->Get(name_key, name_str);
        if (b_base) {
            users = json::parse(name_str);
            return;
        } else {
            std::vector<std::shared_ptr<UserInfo>> user_infos = MysqlManager::GetInstance()->GetUser(uid);
            if (user_infos.empty()) {
                j["error"] = ErrorCodes::ERROR_UID_INVALID;
                return;
            } else {
                for (auto user_info : user_infos) {
                    json jj = json::object();
                    jj["uid"] = user_info->uid;
                    jj["name"] = user_info->name;
                    jj["email"] = user_info->email;
                    jj["nick"] = user_info->nick;
                    jj["sex"] = user_info->sex;
                    jj["desc"] = user_info->desc;
                    jj["icon"] = user_info->icon;
                    users.push_back(jj);
                }
                RedisManager::GetInstance()->Set(name_key, users.dump());
                return;
            }
        }
    }
}

最后在最外层的defer中,会发送出去,前端接受。

session->Send(j.dump(), static_cast<int>(MsgId::ID_SEARCH_USER_RSP));

前端

发送的处理:如何触发发送请求?

在聊天区域的上侧,我们之前实现了一个伸缩的搜索框,当我们输入搜索内容之后,点击回车,捕获回车事件,触发信号on_add_friend

void ChatTopArea::keyPressEvent(QKeyEvent *event)
{
    if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter){
        emit on_add_friend(this->searchBox->getContent());
        return ;
    }
    else{
        QWidget::keyPressEvent(event);
    }
}

而这个信号连接了do_text_changed

connect(this,&ChatTopArea::on_add_friend,searchBox,&AnimatedSearchBox::do_text_changed);

在do_text_changed先检查输入内容的有效性再决定是否发送请求。

void AnimatedSearchBox::do_text_changed(const QString &text)
{
    if (text.length() >= 1){
        getSearchUsers(text.trimmed());
    }else{
        hideResults();
    }
}

如果内容有效,数量达标,我们就设置双方的uid信息,发送请求:

void AnimatedSearchBox::getSearchUsers(const QString &uid)
{
    QJsonObject obj;
    obj["fromUid"] = UserManager::GetInstance()->GetUid();
    obj["toUid"] = uid.trimmed();

    QJsonDocument doc(obj);

    emit TcpManager::GetInstance()->on_send_data(RequestType::ID_SEARCH_USER_REQ,doc.toJson(QJsonDocument::Compact));
}

这时候服务器就会收到请求,然后我们等待回包触发回调函数:

_handlers[RequestType::ID_SEARCH_USER_RSP] = [this](RequestType requestType,int len,QByteArray data){
        QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
        if (jsonDoc.isNull()){
            qDebug() << "Error occured about Json";
            return;
        }
        QJsonObject jsonObj = jsonDoc.object();
        if (!jsonObj.contains("error")){
            int err = static_cast<int>(ErrorCodes::ERROR_JSON);
            qDebug() << "AddFriend Failed,Error Is Json Parse Error " <<err;
            return;
        }

        int err = jsonObj["error"].toInt();
        if (err != static_cast<int>(ErrorCodes::SUCCESS)){
            qDebug() << "AddFriend Failed,Error Is " << err;
            return;
        }

        // 解析查询的用户列表
        QList<std::shared_ptr<UserInfo>> userList;
        if (jsonObj.contains("users") && jsonObj["users"].isArray()) {
            QJsonArray usersArray = jsonObj["users"].toArray();

            for (const QJsonValue &userValue : usersArray) {
                if (userValue.isObject()) {
                    QJsonObject userObj = userValue.toObject();

                    QPixmap avatar;
                    QString tempFilePath;
                    if (userObj.contains("avatar")) {
                        QString base64Avatar = userObj["avatar"].toString();
                        QByteArray avatarData = QByteArray::fromBase64(base64Avatar.toUtf8());
                        avatar.loadFromData(avatarData);
                        tempFilePath = QDir::tempPath() + "/tmp_from_quick_chat_image_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + ".png";
                        // 如果加载失败,使用默认头像
                        if (avatar.isNull()) {
                            avatar = QPixmap(":/Resources/main/header.png");
                        }
                    } else {
                        // 没有头像字段,使用默认头像
                        avatar = QPixmap(":/Resources/main/header.png");
                    }

                    QString id = userObj["uid"].toString();
                    QString email = userObj["email"].toString();
                    QString name = userObj["name"].toString();
                    QString status = userObj["status"].toString();
                    QString avatar_path = tempFilePath;
                    auto user_info = std::make_shared<UserInfo>(id,email,name,avatar_path,status);

                    userList.append(user_info);
                }
            }

        }
        if(userList.count() == 0){
            return;
        }
        emit on_users_searched(userList);
    };

前面提到过,搜索的用户结果放在了"users"字段,是一个数组,因此我们需要分别解析出来。

当解析没有问题,内容无误之后,我们触发on_users_searched信号,通知我们的搜索框,有新的内容,进行展示。执行do_users_searched函数。

connect(TcpManager::GetInstance().get(),&TcpManager::on_users_searched,this,&AnimatedSearchBox::do_users_searched);

这个函数中我们根据传送回来的用户信息列表,更新展示框(ListWidget)的内容,然后让listWidget展示(show)。

void AnimatedSearchBox::do_users_searched(QList<std::shared_ptr<UserInfo>>list)noexcept
{
    this->usersList = std::move(list);
    updateResults();
    showResults();
}

先看updateResults函数,简言之,就是提取出列表的信息,构建对应的item,插入listWidget.

但是这里需要提一下的是,为了美观和交互,我们使用了自定义的控件。我们当然可以选择使用listView然后重写model和delegate自定义渲染,但是为了方便和快洁,我们选择这种形式。

void AnimatedSearchBox::updateResults(){
    for (const std::shared_ptr<UserInfo> &user : this->usersList) {
        QListWidgetItem *item = new QListWidgetItem;
        item->setSizeHint(QSize(300,50));
        // 提取用户ID - 实际项目中从数据结构获取
        FriendsItem *friendItem = new FriendsItem(user->id,user->avatar,user->name,user->status);
        resultList->addItem(item);
        resultList->setItemWidget(item,friendItem);
    }

    // 如果没有结果
    if (resultList->count() == 0) {
        QListWidgetItem *item = new QListWidgetItem("未找到相关用户");
        item->setFlags(item->flags() & ~Qt::ItemIsSelectable);
        item->setForeground(Qt::gray);
        item->setSizeHint(QSize(300,50));
        resultList->addItem(item);
    }
}

这个FriendItem就是一个QWidget,和普通的窗口一样有布局有控件,我们可以在内部自定义展示。

class FriendsItem :public QWidget
{
    Q_OBJECT
public:
    explicit FriendsItem(const QString&uid,const QString&avatar_path = "",const QString&name = "",const QString&status = "在线",QWidget*parent=nullptr);
    void setupUI();
    void setupConnections();
signals:
    void on_apply_clicked(const QString&uid);
private:
    QString _uid;
    QString _avatar_path;
    QString _name;
    QPushButton *_applyFriend;
    StatusLabel *_statusLabel;
    QLabel * _avatar;
    QString _status;
};


FriendsItem::FriendsItem(const QString &uid, const QString &avatar_path, const QString &name, const QString &status,QWidget*parent)
    : QWidget(parent)
    , _uid(uid)
    , _avatar_path(avatar_path)
    , _name(name)
    , _status(status)
{
    setupUI();
    setupConnections();
}

void FriendsItem::setupUI()
{
    QPixmap avatar = SourceManager::GetInstance()->getPixmap(_avatar_path);
    QHBoxLayout*main_hlay = new QHBoxLayout(this);
    main_hlay->setContentsMargins(0,0,10,0);
    main_hlay->setSpacing(5);

    _avatar = new QLabel;
    _avatar->setFixedSize(50, 50);
    _avatar->setStyleSheet(R"(
        QLabel {
            border-radius: 25px;
            background-color: #fdf5fe;
            border: 1px solid #CCCCCC;
        }
    )");
    _avatar->setAlignment(Qt::AlignCenter);  // 关键:内容居中

    // 设置头像,确保缩放并居中
    if (!avatar.isNull()) {
        QPixmap scaledAvatar = avatar.scaled(45, 45, Qt::KeepAspectRatio, Qt::SmoothTransformation);
        _avatar->setPixmap(scaledAvatar);
    } else {
        // 默认头像
        _avatar->setText("👤");
    }

    QLabel*name = new QLabel;
    name->setText(_name);
    name->setStyleSheet("font-weight:bold;color:#333333;font-size:15px;");

    _statusLabel = new StatusLabel;
    _statusLabel->setStatus(_status);
    _statusLabel->setEnabled(false);
    _statusLabel->setFixedSize({60,30});

    _applyFriend = new QPushButton;
    _applyFriend->setText("添加");
    _applyFriend->setFixedSize({60,35});
    _applyFriend->setStyleSheet(R"(
        QPushButton {
            background-color: #79fcf7;
            color: #ffffff;
            border: none;
            border-radius: 10px;
            font-size: 12px;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #3fd9d4;
        }
    )");

    main_hlay->addWidget(_avatar);
    main_hlay->addWidget(name);
    main_hlay->addStretch();
    main_hlay->addWidget(_statusLabel);
    main_hlay->addWidget(_applyFriend);
}

void FriendsItem::setupConnections()
{
    connect(_applyFriend,&QPushButton::clicked,this,[this](bool){
        QJsonObject obj;
        obj["fromUid"] = QString::number(UserManager::GetInstance()->GetUid());
        obj["toUid"] = this->_uid;

        QJsonDocument doc;
        doc.setObject(obj);
        QByteArray array = doc.toJson(QJsonDocument::Compact);

        emit TcpManager::GetInstance()->on_send_data(RequestType::ID_ADD_FRIEND_REQ,array);
        this->_applyFriend->setEnabled(false);
        showToolTip(_applyFriend,"已发送好友请求");
    });
}

最后回到showResults函数中,我们设置了展示框的大小和位置,然后show出来。

void AnimatedSearchBox::showResults()
{
    if (resultList->count() == 0) {
        return;
    }

    if (!resultList->parent()) {
        resultList->setParent(window());
    }

    QRect r = searchEdit->rect();
    QPoint bottomLeft = searchEdit->mapToGlobal(r.bottomLeft());
    bottomLeft.setX(bottomLeft.x()-50);

    // 先设置大小,再移动
    resultList->setFixedSize(310, 300);  // 使用 setFixedSize
    resultList->move(bottomLeft);

    resultList->show();
    resultList->raise();

    // 强制更新
    // resultList->update();
    // resultList->repaint();
}

这里提示,要是的这个listwidget可以在窗口内部show,同时而非独立窗口,但是又不会进入布局影响,我们这里有一个延迟的技巧,先不设置父窗,调用QTimer::singleShot在空闲的时候(因为实际上会有一个事件队列),设置父窗口,这时候不会作为独立窗口,也不受到父窗口布局的影响。

resultList = new QListWidget(window());
resultList->setObjectName("resultList");
resultList->setFixedHeight(0);  // 初始高度为0
resultList->hide();
QTimer::singleShot(0, this, [this] {
    QWidget *central = window();           // 普通 QWidget 场景
    resultList->setParent(central);
    resultList->setWindowFlags(Qt::Popup);         // 变回普通子控件
    resultList->setFocusPolicy(Qt::StrongFocus);
});

联调测试

我们打开StatusServer,GateWayServer,ChatServer,打开qt前端,在搜索框输入uid为1,回车查询。

可以看到,服务器能够接受请求并处理,前端也收到回包并展示。

image-20251111172256054

image-20251111172308879

image-20251111172544493

posted @ 2025-12-24 23:17  大胖熊哈  阅读(1)  评论(0)    收藏  举报