简介
本文介绍如何实现用户查找和好友申请功能。查找和申请好友会涉及前后端通信和rpc服务间调用。所以目前先从客户端入手,搜索用户后发送查找好友申请请求给服务器,服务器收到后判断是否存在,如果不存在则显示未找到,如果存在则显示查找到的结果
点击查询
客户端点击搜索列表的添加好友item后,先弹出一个模态对话框,上面有loading动作表示加载,直到服务器返回结果
void SearchList::slot_item_clicked(QListWidgetItem *item)
{
QWidget *widget = this->itemWidget(item); //获取自定义widget对象
if(!widget){
qDebug()<< "slot item clicked widget is nullptr";
return;
}
// 对自定义widget进行操作, 将item 转化为基类ListItemBase
ListItemBase *customItem = qobject_cast<ListItemBase*>(widget);
if(!customItem){
qDebug()<< "slot item clicked widget is nullptr";
return;
}
auto itemType = customItem->GetItemType();
if(itemType == ListItemType::INVALID_ITEM){
qDebug()<< "slot invalid item clicked ";
return;
}
if(itemType == ListItemType::ADD_USER_TIP_ITEM){
if(_send_pending){
return;
}
if (!_search_edit) {
return;
}
waitPending(true);
auto search_edit = dynamic_cast<CustomizeEdit*>(_search_edit);
auto uid_str = search_edit->text();
QJsonObject jsonObj;
jsonObj["uid"] = uid_str;
QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_SEARCH_USER_REQ,
jsonData);
return;
}
//清楚弹出框
CloseFindDlg();
}
_send_pending为新增的成员变量,如果为true则表示发送阻塞.构造函数中将其设置为false。
waitPending函数为根据pending状态展示加载框
void SearchList::waitPending(bool pending)
{
if(pending){
_loadingDialog = new LoadingDlg(this);
_loadingDialog->setModal(true);
_loadingDialog->show();
_send_pending = pending;
}else{
_loadingDialog->hide();
_loadingDialog->deleteLater();
_send_pending = pending;
}
}
当我们发送数据后服务器会处理,返回ID_SEARCH_USER_RSP包,所以客户端要实现对ID_SEARCH_USER_RSP包的处理
_handlers.insert(ID_SEARCH_USER_RSP, [this](ReqId id, int len, QByteArray data){
Q_UNUSED(len);
qDebug()<< "handle id is "<< id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
// 检查转换是否成功
if(jsonDoc.isNull()){
qDebug() << "Failed to create QJsonDocument.";
return;
}
QJsonObject jsonObj = jsonDoc.object();
if(!jsonObj.contains("error")){
int err = ErrorCodes::ERR_JSON;
qDebug() << "Login Failed, err is Json Parse Err" << err ;
emit sig_login_failed(err);
return;
}
int err = jsonObj["error"].toInt();
if(err != ErrorCodes::SUCCESS){
qDebug() << "Login Failed, err is " << err ;
emit sig_login_failed(err);
return;
}
auto search_info = std::make_shared<SearchInfo>(jsonObj["uid"].toInt(),
jsonObj["name"].toString(), jsonObj["nick"].toString(),
jsonObj["desc"].toString(), jsonObj["sex"].toInt(), jsonObj["icon"].toString());
emit sig_user_search(search_info);
});
将搜索到的结果封装为search_info发送给SearchList类做展示, search_list中连接信号和槽
//连接搜索条目
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_user_search, this, &SearchList::slot_user_search);
slot_user_search槽函数弹出搜索结果
void SearchList::slot_user_search(std::shared_ptr<SearchInfo> si)
{
waitPending(false);
if(si == nullptr){
_find_dlg = std::make_shared<FindFailDlg>(this);
}else{
//此处分两种情况,一种是搜多到已经是自己的朋友了,一种是未添加好友
//查找是否已经是好友 todo...
_find_dlg = std::make_shared<FindSuccessDlg>(this);
std::dynamic_pointer_cast<FindSuccessDlg>(_find_dlg)->SetSearchInfo(si);
}
_find_dlg->show();
}
FindSuccessDlg是找到的结果展示,FindFailDlg是未找到结果展示。以下为FindSuccessDlg的ui布局
具体声明如下
class FindSuccessDlg : public QDialog
{
Q_OBJECT
public:
explicit FindSuccessDlg(QWidget *parent = nullptr);
~FindSuccessDlg();
void SetSearchInfo(std::shared_ptr<SearchInfo> si);
private:
Ui::FindSuccessDlg *ui;
std::shared_ptr<SearchInfo> _si;
QWidget * _parent;
private slots:
void on_add_friend_btn_clicked();
};
具体实现如下
FindSuccessDlg::FindSuccessDlg(QWidget *parent) :
QDialog(parent), _parent(parent),
ui(new Ui::FindSuccessDlg)
{
ui->setupUi(this);
// 设置对话框标题
setWindowTitle("添加");
// 隐藏对话框标题栏
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
// 获取当前应用程序的路径
QString app_path = QCoreApplication::applicationDirPath();
QString pix_path = QDir::toNativeSeparators(app_path +
QDir::separator() + "static"+QDir::separator()+"head_1.jpg");
QPixmap head_pix(pix_path);
head_pix = head_pix.scaled(ui->head_lb->size(),
Qt::KeepAspectRatio, Qt::SmoothTransformation);
ui->head_lb->setPixmap(head_pix);
ui->add_friend_btn->SetState("normal","hover","press");
this->setModal(true);
}
FindSuccessDlg::~FindSuccessDlg()
{
qDebug()<<"FindSuccessDlg destruct";
delete ui;
}
void FindSuccessDlg::SetSearchInfo(std::shared_ptr<SearchInfo> si)
{
ui->name_lb->setText(si->_name);
_si = si;
}
void FindSuccessDlg::on_add_friend_btn_clicked()
{
//todo... 添加好友界面弹出
this->hide();
//弹出加好友界面
auto applyFriend = new ApplyFriend(_parent);
applyFriend->SetSearchInfo(_si);
applyFriend->setModal(true);
applyFriend->show();
}
类似的FindFailDlg也是这种思路,大家自己实现即可。
服务器查询逻辑
chatserver服务器要根据客户端发送过来的用户id进行查找,chatserver服务器需先注册ID_SEARCH_USER_REQ和回调函数
void LogicSystem::RegisterCallBacks() {
_fun_callbacks[MSG_CHAT_LOGIN] = std::bind(&LogicSystem::LoginHandler, this,
placeholders::_1, placeholders::_2, placeholders::_3);
_fun_callbacks[ID_SEARCH_USER_REQ] = std::bind(&LogicSystem::SearchInfo, this,
placeholders::_1, placeholders::_2, placeholders::_3);
}
SearchInfo根据用户uid查询具体信息
void LogicSystem::SearchInfo(std::shared_ptr<CSession> session, const short& msg_id, const string& msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
auto uid_str = root["uid"].asString();
std::cout << "user SearchInfo uid is " << uid_str << endl;
Json::Value rtvalue;
Defer deder([this, &rtvalue, session]() {
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_SEARCH_USER_RSP);
});
bool b_digit = isPureDigit(uid_str);
if (b_digit) {
GetUserByUid(uid_str, rtvalue);
}
else {
GetUserByName(uid_str, rtvalue);
}
}
到此客户端和服务器搜索查询的联调功能已经解决了。
客户端添加好友
当Client1搜索到好友后,点击添加弹出信息界面,然后点击确定即可向对方Client2申请添加好友,这个请求要先发送到Client1所在的服务器Server1,服务器收到后判断Client2所在服务器,如果Client2在Server1则直接在Server1中查找Client2的连接信息,没找到说明Client2未在内存中,找到了则通过Session发送tcp给对方。如果Client2不在Server1而在Server2上,则需要让Server1通过grpc接口通知Server2,Server2收到后继续判断Client2是否在线,如果在线则通知。
如下图,Client1想和Client2以及Client3分别通信,需要先将请求发给Client1所在的Server1,再考虑是否rpc调用。
客户端在ApplySure槽函数中添加好友请求
void ApplyFriend::SlotApplySure()
{
qDebug() << "Slot Apply Sure called" ;
QJsonObject jsonObj;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj["uid"] = uid;
auto name = ui->name_ed->text();
if(name.isEmpty()){
name = ui->name_ed->placeholderText();
}
jsonObj["applyname"] = name;
auto bakname = ui->back_ed->text();
if(bakname.isEmpty()){
bakname = ui->back_ed->placeholderText();
}
jsonObj["bakname"] = bakname;
jsonObj["touid"] = _si->_uid;
QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_ADD_FRIEND_REQ, jsonData);
this->hide();
deleteLater();
}
另一个客户端会收到服务器通知添加好友的请求,所以在TcpMgr里监听这个请求
_handlers.insert(ID_NOTIFY_ADD_FRIEND_REQ, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}
QJsonObject jsonObj = jsonDoc.object();
if (!jsonObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "Login Failed, err is Json Parse Err" << err;
emit sig_user_search(nullptr);
return;
}
int err = jsonObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "Login Failed, err is " << err;
emit sig_user_search(nullptr);
return;
}
int from_uid = jsonObj["applyuid"].toInt();
QString name = jsonObj["name"].toString();
QString desc = jsonObj["desc"].toString();
QString icon = jsonObj["icon"].toString();
QString nick = jsonObj["nick"].toString();
int sex = jsonObj["sex"].toInt();
auto apply_info = std::make_shared<AddFriendApply>(
from_uid, name, desc,
icon, nick, sex);
emit sig_friend_apply(apply_info);
});
服务调用
服务器要处理客户端发过来的添加好友的请求,并决定是否调用rpc通知其他服务。
先将AddFriendApply函数注册到回调map里
void LogicSystem::RegisterCallBacks() {
_fun_callbacks[MSG_CHAT_LOGIN] = std::bind(&LogicSystem::LoginHandler, this,
placeholders::_1, placeholders::_2, placeholders::_3);
_fun_callbacks[ID_SEARCH_USER_REQ] = std::bind(&LogicSystem::SearchInfo, this,
placeholders::_1, placeholders::_2, placeholders::_3);
_fun_callbacks[ID_ADD_FRIEND_REQ] = std::bind(&LogicSystem::AddFriendApply, this,
placeholders::_1, placeholders::_2, placeholders::_3);
}
接下来实现AddFriendApply
void LogicSystem::AddFriendApply(std::shared_ptr<CSession> session, const short& msg_id, const string& msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
auto uid = root["uid"].asInt();
auto applyname = root["applyname"].asString();
auto bakname = root["bakname"].asString();
auto touid = root["touid"].asInt();
std::cout << "user login uid is " << uid << " applyname is "
<< applyname << " bakname is " << bakname << " touid is " << touid << endl;
Json::Value rtvalue;
rtvalue["error"] = ErrorCodes::Success;
Defer defer([this, &rtvalue, session]() {
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_ADD_FRIEND_RSP);
});
//先更新数据库
MysqlMgr::GetInstance()->AddFriendApply(uid, touid);
//查询redis 查找touid对应的server ip
auto to_str = std::to_string(touid);
auto to_ip_key = USERIPPREFIX + to_str;
std::string to_ip_value = "";
bool b_ip = RedisMgr::GetInstance()->Get(to_ip_key, to_ip_value);
if (!b_ip) {
return;
}
auto& cfg = ConfigMgr::Inst();
auto self_name = cfg["SelfServer"]["Name"];
//直接通知对方有申请消息
if (to_ip_value == self_name) {
auto session = UserMgr::GetInstance()->GetSession(touid);
if (session) {
//在内存中则直接发送通知对方
Json::Value notify;
notify["error"] = ErrorCodes::Success;
notify["applyuid"] = uid;
notify["name"] = applyname;
notify["desc"] = "";
std::string return_str = notify.toStyledString();
session->Send(return_str, ID_NOTIFY_ADD_FRIEND_REQ);
}
return;
}
std::string base_key = USER_BASE_INFO + std::to_string(uid);
auto apply_info = std::make_shared<UserInfo>();
bool b_info = GetBaseInfo(base_key, uid, apply_info);
AddFriendReq add_req;
add_req.set_applyuid(uid);
add_req.set_touid(touid);
add_req.set_name(applyname);
add_req.set_desc("");
if (b_info) {
add_req.set_icon(apply_info->icon);
add_req.set_sex(apply_info->sex);
add_req.set_nick(apply_info->nick);
}
//发送通知
ChatGrpcClient::GetInstance()->NotifyAddFriend(to_ip_value, add_req);
}
上面的函数中先更新数据库将申请写入数据库中
bool MysqlMgr::AddFriendApply(const int& from, const int& to) {
return _dao.AddFriendApply(from, to);
}
内部调用dao层面的添加好友请求
bool MysqlDao::AddFriendApply(const int& from, const int& to) {
auto con = pool_->getConnection();
if (con == nullptr) {
return false;
}
Defer defer([this, &con]() {
pool_->returnConnection(std::move(con));
});
try {
std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT INTO friend_apply (from_uid, to_uid) values (?,?) "
"ON DUPLICATE KEY UPDATE from_uid = from_uid, to_uid = to_uid "));
pstmt->setInt(1, from);
pstmt->setInt(2, to);
//执行更新
int rowAffected = pstmt->executeUpdate();
if (rowAffected < 0) {
return false;
}
return true;
}
catch (sql::SQLException& e) {
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
return true;
}
添加完成后判断要通知的对端是否在本服务器,如果在本服务器则直接通过uid查找session,判断用户是否在线,如果在线则直接通知对端。
如果不在本服务器,则需要通过rpc通知对端服务器。rpc的客户端这么写即可。
AddFriendRsp ChatGrpcClient::NotifyAddFriend(std::string server_ip, const AddFriendReq& req) {
AddFriendRsp rsp;
Defer defer([&rsp, &req]() {
rsp.set_error(ErrorCodes::Success);
rsp.set_applyuid(req.applyuid());
rsp.set_touid(req.touid());
});
auto find_iter = _pools.find(server_ip);
if (find_iter == _pools.end()) {
return rsp;
}
auto& pool = find_iter->second;
ClientContext context;
auto stub = pool->getConnection();
Status status = stub->NotifyAddFriend(&context, req, &rsp);
Defer defercon([&stub, this, &pool]() {
pool->returnConnection(std::move(stub));
});
if (!status.ok()) {
rsp.set_error(ErrorCodes::RPCFailed);
return rsp;
}
return rsp;
}
同样rpc的服务端也要实现,我们先将rpc客户端和服务端的逻辑都在ChatServer1写好,然后复制给ChatServer2即可。 rpc的服务实现如下
Status ChatServiceImpl::NotifyAddFriend(ServerContext* context, const AddFriendReq* request,
AddFriendRsp* reply) {
//查找用户是否在本服务器
auto touid = request->touid();
auto session = UserMgr::GetInstance()->GetSession(touid);
Defer defer([request, reply]() {
reply->set_error(ErrorCodes::Success);
reply->set_applyuid(request->applyuid());
reply->set_touid(request->touid());
});
//用户不在内存中则直接返回
if (session == nullptr) {
return Status::OK;
}
//在内存中则直接发送通知对方
Json::Value rtvalue;
rtvalue["error"] = ErrorCodes::Success;
rtvalue["applyuid"] = request->applyuid();
rtvalue["name"] = request->name();
rtvalue["desc"] = request->desc();
rtvalue["icon"] = request->icon();
rtvalue["sex"] = request->sex();
rtvalue["nick"] = request->nick();
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_NOTIFY_ADD_FRIEND_REQ);
return Status::OK;
}
上面的代码也是判断要通知的客户端是否在内存中,如果在就通过session发送tcp请求。
将ChatServer1的代码拷贝给ChatServer2,重启两个服务,再启动两个客户端,一个客户端申请另一个客户端,通过查看客户端日志是能看到申请信息的。
申请显示
接下来被通知申请的客户端要做界面显示,我们实现被通知的客户端收到sig_friend_apply信号的处理逻辑。在ChatDialog的构造函数中连接信号和槽
//连接申请添加好友信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_friend_apply, this, &ChatDialog::slot_apply_friend);
实现申请好友的槽函数
void ChatDialog::slot_apply_friend(std::shared_ptr<AddFriendApply> apply)
{
qDebug() << "receive apply friend slot, applyuid is " << apply->_from_uid << " name is "
<< apply->_name << " desc is " << apply->_desc;
bool b_already = UserMgr::GetInstance()->AlreadyApply(apply->_from_uid);
if(b_already){
return;
}
UserMgr::GetInstance()->AddApplyList(std::make_shared<ApplyInfo>(apply));
ui->side_contact_lb->ShowRedPoint(true);
ui->con_user_list->ShowRedPoint(true);
ui->friend_apply_page->AddNewApply(apply);
}
这样就能显示新的申请消息和红点了。具体添加一个新的申请条目到申请好友页面的逻辑如下:
void ApplyFriendPage::AddNewApply(std::shared_ptr<AddFriendApply> apply)
{
//先模拟头像随机,以后头像资源增加资源服务器后再显示
int randomValue = QRandomGenerator::global()->bounded(100); // 生成0到99之间的随机整数
int head_i = randomValue % heads.size();
auto* apply_item = new ApplyFriendItem();
auto apply_info = std::make_shared<ApplyInfo>(apply->_from_uid,
apply->_name, apply->_desc,heads[head_i], apply->_name, 0, 0);
apply_item->SetInfo( apply_info);
QListWidgetItem* item = new QListWidgetItem;
//qDebug()<<"chat_user_wid sizeHint is " << chat_user_wid->sizeHint();
item->setSizeHint(apply_item->sizeHint());
item->setFlags(item->flags() & ~Qt::ItemIsEnabled & ~Qt::ItemIsSelectable);
ui->apply_friend_list->insertItem(0,item);
ui->apply_friend_list->setItemWidget(item, apply_item);
apply_item->ShowAddBtn(true);
//收到审核好友信号
connect(apply_item, &ApplyFriendItem::sig_auth_friend, [this](std::shared_ptr<ApplyInfo> apply_info) {
auto* authFriend = new AuthenFriend(this);
authFriend->setModal(true);
authFriend->SetApplyInfo(apply_info);
authFriend->show();
});
}
测试效果, 收到对方请求后如下图
登录加载申请
当用户登录后,服务器需要将申请列表同步给客户端, 写在登录逻辑里。
//从数据库获取申请列表
std::vector<std::shared_ptr<ApplyInfo>> apply_list;
auto b_apply = GetFriendApplyInfo(uid,apply_list);
if (b_apply) {
for (auto & apply : apply_list) {
Json::Value obj;
obj["name"] = apply->_name;
obj["uid"] = apply->_uid;
obj["icon"] = apply->_icon;
obj["nick"] = apply->_nick;
obj["sex"] = apply->_sex;
obj["desc"] = apply->_desc;
obj["status"] = apply->_status;
rtvalue["apply_list"].append(obj);
}
}
获取好友申请信息函数
bool LogicSystem::GetFriendApplyInfo(int to_uid, std::vector<std::shared_ptr<ApplyInfo>> &list) {
//从mysql获取好友申请列表
return MysqlMgr::GetInstance()->GetApplyList(to_uid, list, 0, 10);
}
dao层面实现获取申请列表
bool MysqlMgr::GetApplyList(int touid,
std::vector<std::shared_ptr<ApplyInfo>>& applyList, int begin, int limit) {
return _dao.GetApplyList(touid, applyList, begin, limit);
}
bool MysqlDao::GetApplyList(int touid, std::vector<std::shared_ptr<ApplyInfo>>& applyList, int begin, int limit) {
auto con = pool_->getConnection();
if (con == nullptr) {
return false;
}
Defer defer([this, &con]() {
pool_->returnConnection(std::move(con));
});
try {
// 准备SQL语句, 根据起始id和限制条数返回列表
std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("select apply.from_uid, apply.status, user.name, "
"user.nick, user.sex from friend_apply as apply join user on apply.from_uid = user.uid where apply.to_uid = ? "
"and apply.id > ? order by apply.id ASC LIMIT ? "));
pstmt->setInt(1, touid); // 将uid替换为你要查询的uid
pstmt->setInt(2, begin); // 起始id
pstmt->setInt(3, limit); //偏移量
// 执行查询
std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());
// 遍历结果集
while (res->next()) {
auto name = res->getString("name");
auto uid = res->getInt("from_uid");
auto status = res->getInt("status");
auto nick = res->getString("nick");
auto sex = res->getInt("sex");
auto apply_ptr = std::make_shared<ApplyInfo>(uid, name, "", "", nick, sex, status);
applyList.push_back(apply_ptr);
}
return true;
}
catch (sql::SQLException& e) {
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}
好友认证界面
客户端需要实现好友认证界面,当点击同意对方好友申请后,弹出认证信息,点击确定后将认证同意的请求发给服务器,服务器再通知申请方,告知对方被申请人已经同意加好友了。认证界面和申请界面类似, 这个大家自己实现即可。
认证界面的函数和逻辑可以照抄申请好友的逻辑。
AuthenFriend::AuthenFriend(QWidget *parent) :
QDialog(parent),
ui(new Ui::AuthenFriend),_label_point(2,6)
{
ui->setupUi(this);
// 隐藏对话框标题栏
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
this->setObjectName("AuthenFriend");
this->setModal(true);
ui->lb_ed->setPlaceholderText("搜索、添加标签");
ui->back_ed->setPlaceholderText("燃烧的胸毛");
ui->lb_ed->SetMaxLength(21);
ui->lb_ed->move(2, 2);
ui->lb_ed->setFixedHeight(20);
ui->lb_ed->setMaxLength(10);
ui->input_tip_wid->hide();
_tip_cur_point = QPoint(5, 5);
_tip_data = { "同学","家人","菜鸟教程","C++ Primer","Rust 程序设计",
"父与子学Python","nodejs开发指南","go 语言开发指南",
"游戏伙伴","金融投资","微信读书","拼多多拼友" };
connect(ui->more_lb, &ClickedOnceLabel::clicked, this, &AuthenFriend::ShowMoreLabel);
InitTipLbs();
//链接输入标签回车事件
connect(ui->lb_ed, &CustomizeEdit::returnPressed, this, &AuthenFriend::SlotLabelEnter);
connect(ui->lb_ed, &CustomizeEdit::textChanged, this, &AuthenFriend::SlotLabelTextChange);
connect(ui->lb_ed, &CustomizeEdit::editingFinished, this, &AuthenFriend::SlotLabelEditFinished);
connect(ui->tip_lb, &ClickedOnceLabel::clicked, this, &AuthenFriend::SlotAddFirendLabelByClickTip);
ui->scrollArea->horizontalScrollBar()->setHidden(true);
ui->scrollArea->verticalScrollBar()->setHidden(true);
ui->scrollArea->installEventFilter(this);
ui->sure_btn->SetState("normal","hover","press");
ui->cancel_btn->SetState("normal","hover","press");
//连接确认和取消按钮的槽函数
connect(ui->cancel_btn, &QPushButton::clicked, this, &AuthenFriend::SlotApplyCancel);
connect(ui->sure_btn, &QPushButton::clicked, this, &AuthenFriend::SlotApplySure);
}
AuthenFriend::~AuthenFriend()
{
qDebug()<< "AuthenFriend destruct";
delete ui;
}
void AuthenFriend::InitTipLbs()
{
int lines = 1;
for(int i = 0; i < _tip_data.size(); i++){
auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(_tip_data[i]);
connect(lb, &ClickedLabel::clicked, this, &AuthenFriend::SlotChangeFriendLabelByTip);
QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度
if (_tip_cur_point.x() + textWidth + tip_offset > ui->lb_list->width()) {
lines++;
if (lines > 2) {
delete lb;
return;
}
_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);
}
auto next_point = _tip_cur_point;
AddTipLbs(lb, _tip_cur_point,next_point, textWidth, textHeight);
_tip_cur_point = next_point;
}
}
void AuthenFriend::AddTipLbs(ClickedLabel* lb, QPoint cur_point, QPoint& next_point, int text_width, int text_height)
{
lb->move(cur_point);
lb->show();
_add_labels.insert(lb->text(), lb);
_add_label_keys.push_back(lb->text());
next_point.setX(lb->pos().x() + text_width + 15);
next_point.setY(lb->pos().y());
}
bool AuthenFriend::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->scrollArea && event->type() == QEvent::Enter)
{
ui->scrollArea->verticalScrollBar()->setHidden(false);
}
else if (obj == ui->scrollArea && event->type() == QEvent::Leave)
{
ui->scrollArea->verticalScrollBar()->setHidden(true);
}
return QObject::eventFilter(obj, event);
}
void AuthenFriend::SetApplyInfo(std::shared_ptr<ApplyInfo> apply_info)
{
_apply_info = apply_info;
ui->back_ed->setPlaceholderText(apply_info->_name);
}
void AuthenFriend::ShowMoreLabel()
{
qDebug()<< "receive more label clicked";
ui->more_lb_wid->hide();
ui->lb_list->setFixedWidth(325);
_tip_cur_point = QPoint(5, 5);
auto next_point = _tip_cur_point;
int textWidth;
int textHeight;
//重拍现有的label
for(auto & added_key : _add_label_keys){
auto added_lb = _add_labels[added_key];
QFontMetrics fontMetrics(added_lb->font()); // 获取QLabel控件的字体信息
textWidth = fontMetrics.width(added_lb->text()); // 获取文本的宽度
textHeight = fontMetrics.height(); // 获取文本的高度
if(_tip_cur_point.x() +textWidth + tip_offset > ui->lb_list->width()){
_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y()+textHeight+15);
}
added_lb->move(_tip_cur_point);
next_point.setX(added_lb->pos().x() + textWidth + 15);
next_point.setY(_tip_cur_point.y());
_tip_cur_point = next_point;
}
//添加未添加的
for(int i = 0; i < _tip_data.size(); i++){
auto iter = _add_labels.find(_tip_data[i]);
if(iter != _add_labels.end()){
continue;
}
auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(_tip_data[i]);
connect(lb, &ClickedLabel::clicked, this, &AuthenFriend::SlotChangeFriendLabelByTip);
QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度
if (_tip_cur_point.x() + textWidth + tip_offset > ui->lb_list->width()) {
_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);
}
next_point = _tip_cur_point;
AddTipLbs(lb, _tip_cur_point, next_point, textWidth, textHeight);
_tip_cur_point = next_point;
}
int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);
//qDebug()<<"after resize ui->lb_list size is " << ui->lb_list->size();
ui->scrollcontent->setFixedHeight(ui->scrollcontent->height()+diff_height);
}
void AuthenFriend::resetLabels()
{
auto max_width = ui->gridWidget->width();
auto label_height = 0;
for(auto iter = _friend_labels.begin(); iter != _friend_labels.end(); iter++){
//todo... 添加宽度统计
if( _label_point.x() + iter.value()->width() > max_width) {
_label_point.setY(_label_point.y()+iter.value()->height()+6);
_label_point.setX(2);
}
iter.value()->move(_label_point);
iter.value()->show();
_label_point.setX(_label_point.x()+iter.value()->width()+2);
_label_point.setY(_label_point.y());
label_height = iter.value()->height();
}
if(_friend_labels.isEmpty()){
ui->lb_ed->move(_label_point);
return;
}
if(_label_point.x() + MIN_APPLY_LABEL_ED_LEN > ui->gridWidget->width()){
ui->lb_ed->move(2,_label_point.y()+label_height+6);
}else{
ui->lb_ed->move(_label_point);
}
}
void AuthenFriend::addLabel(QString name)
{
if (_friend_labels.find(name) != _friend_labels.end()) {
return;
}
auto tmplabel = new FriendLabel(ui->gridWidget);
tmplabel->SetText(name);
tmplabel->setObjectName("FriendLabel");
auto max_width = ui->gridWidget->width();
//todo... 添加宽度统计
if (_label_point.x() + tmplabel->width() > max_width) {
_label_point.setY(_label_point.y() + tmplabel->height() + 6);
_label_point.setX(2);
}
else {
}
tmplabel->move(_label_point);
tmplabel->show();
_friend_labels[tmplabel->Text()] = tmplabel;
_friend_label_keys.push_back(tmplabel->Text());
connect(tmplabel, &FriendLabel::sig_close, this, &AuthenFriend::SlotRemoveFriendLabel);
_label_point.setX(_label_point.x() + tmplabel->width() + 2);
if (_label_point.x() + MIN_APPLY_LABEL_ED_LEN > ui->gridWidget->width()) {
ui->lb_ed->move(2, _label_point.y() + tmplabel->height() + 2);
}
else {
ui->lb_ed->move(_label_point);
}
ui->lb_ed->clear();
if (ui->gridWidget->height() < _label_point.y() + tmplabel->height() + 2) {
ui->gridWidget->setFixedHeight(_label_point.y() + tmplabel->height() * 2 + 2);
}
}
void AuthenFriend::SlotLabelEnter()
{
if(ui->lb_ed->text().isEmpty()){
return;
}
addLabel(ui->lb_ed->text());
ui->input_tip_wid->hide();
}
void AuthenFriend::SlotRemoveFriendLabel(QString name)
{
qDebug() << "receive close signal";
_label_point.setX(2);
_label_point.setY(6);
auto find_iter = _friend_labels.find(name);
if(find_iter == _friend_labels.end()){
return;
}
auto find_key = _friend_label_keys.end();
for(auto iter = _friend_label_keys.begin(); iter != _friend_label_keys.end();
iter++){
if(*iter == name){
find_key = iter;
break;
}
}
if(find_key != _friend_label_keys.end()){
_friend_label_keys.erase(find_key);
}
delete find_iter.value();
_friend_labels.erase(find_iter);
resetLabels();
auto find_add = _add_labels.find(name);
if(find_add == _add_labels.end()){
return;
}
find_add.value()->ResetNormalState();
}
//点击标已有签添加或删除新联系人的标签
void AuthenFriend::SlotChangeFriendLabelByTip(QString lbtext, ClickLbState state)
{
auto find_iter = _add_labels.find(lbtext);
if(find_iter == _add_labels.end()){
return;
}
if(state == ClickLbState::Selected){
//编写添加逻辑
addLabel(lbtext);
return;
}
if(state == ClickLbState::Normal){
//编写删除逻辑
SlotRemoveFriendLabel(lbtext);
return;
}
}
void AuthenFriend::SlotLabelTextChange(const QString& text)
{
if (text.isEmpty()) {
ui->tip_lb->setText("");
ui->input_tip_wid->hide();
return;
}
auto iter = std::find(_tip_data.begin(), _tip_data.end(), text);
if (iter == _tip_data.end()) {
auto new_text = add_prefix + text;
ui->tip_lb->setText(new_text);
ui->input_tip_wid->show();
return;
}
ui->tip_lb->setText(text);
ui->input_tip_wid->show();
}
void AuthenFriend::SlotLabelEditFinished()
{
ui->input_tip_wid->hide();
}
void AuthenFriend::SlotAddFirendLabelByClickTip(QString text)
{
int index = text.indexOf(add_prefix);
if (index != -1) {
text = text.mid(index + add_prefix.length());
}
addLabel(text);
//标签展示栏也增加一个标签, 并设置绿色选中
if (index != -1) {
_tip_data.push_back(text);
}
auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(text);
connect(lb, &ClickedLabel::clicked, this, &AuthenFriend::SlotChangeFriendLabelByTip);
qDebug() << "ui->lb_list->width() is " << ui->lb_list->width();
qDebug() << "_tip_cur_point.x() is " << _tip_cur_point.x();
QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度
qDebug() << "textWidth is " << textWidth;
if (_tip_cur_point.x() + textWidth+ tip_offset+3 > ui->lb_list->width()) {
_tip_cur_point.setX(5);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);
}
auto next_point = _tip_cur_point;
AddTipLbs(lb, _tip_cur_point, next_point, textWidth,textHeight);
_tip_cur_point = next_point;
int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);
lb->SetCurState(ClickLbState::Selected);
ui->scrollcontent->setFixedHeight(ui->scrollcontent->height()+ diff_height );
}
void AuthenFriend::SlotApplySure()
{
qDebug() << "Slot Apply Sure ";
//添加发送逻辑
QJsonObject jsonObj;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj["fromuid"] = uid;
jsonObj["touid"] = _apply_info->_uid;
QString back_name = "";
if(ui->back_ed->text().isEmpty()){
back_name = ui->back_ed->placeholderText();
}else{
back_name = ui->back_ed->text();
}
jsonObj["back"] = back_name;
QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_AUTH_FRIEND_REQ, jsonData);
this->hide();
deleteLater();
}
void AuthenFriend::SlotApplyCancel()
{
this->hide();
deleteLater();
}
源码连接
https://gitee.com/secondtonone1/llfcchat