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,回车查询。
可以看到,服务器能够接受请求并处理,前端也收到回包并展示。




浙公网安备 33010602011771号