firemail
标题:
QQ好友列表的实现(QQ9.0版本样式) -- 使用QTreeView
[打印本页]
作者:
Qter
时间:
2020-3-10 18:45
标题:
QQ好友列表的实现(QQ9.0版本样式) -- 使用QTreeView
本帖最后由 Qter 于 2020-3-24 17:03 编辑
文章结构
最终实现效果
基本功能
代码主要结构
FriendTree类主要工作解析
ItemDelegate类主要工作解析
工程源码路径/下载地址
最终实现效果
以上是实现的最终样式,自己电脑上安装的QQ9.0版本,就按这个版本来了。
基本功能
实现的一些基本功能总结:
分组展示好友列表 ,一个组下多个好友;
Item上绘制头像、在线状态、个性签名、用户名+昵称(依据是否VIP设置成不同颜色)、视频通话图标;
头像、在线状态、视频通话图标采用svg图标格式
hover效果,鼠标移至Item不同位置,ToolTip显示不同的信息,如鼠标移动至头像时提示“鼠标移到头像上啦!”,鼠标移到视频通话按钮上显示"视频通话"等,默认显示用户名+昵称+QQ号。
当鼠标移动到某个好友Item上时,对应Item显示视频通话图标。
双击Item事件,打开聊天(仅演示捕获事件,进行弹窗提示事件处理结果);
点击视频通话图标,进行视频通话(仅演示捕获事件,进行弹窗提示事件处理结果)
代码主要结构
说明:公共UI库主要是一些通用的处理,比如DelegatePainter类,专门用来绘制文本、图片等,TreeView增加了一些自定义的事件信号,在此处不一一赘述,若有需要,可直接拿过去复用即可,也可以自己定义其他信号等。我们的好友列表TreeView是继承此类的。源码路径见文章最后。
FriendTree类主要工作解析
前提:使用上面提到的公共Ui库。
下面讲解下FriendTree主要做的事情,类头文件如下:
#pragma once
#include <QTreeView>
#include "
ublicGui/TreeView/TreeView.h"
#include "GlobalDefines.h"
using namespace publicgui;
namespace qqfriendlist
{
class ItemDelegate;
class FriendTree : public TreeView
{
Q_OBJECT
public:
FriendTree(QWidget *parent = Q_NULLPTR);
~FriendTree();
// 赋值 传入分组/好友结构数据
void setValues(const std::vector<Group>& groups);
private:
void initUi();
void initConnection();
// 自定义的hover处理
void onHoverHandle(const QModelIndex& index, int role);
// 自定义的点击事件处理
void onClickedHandle(const QModelIndex& index, int role);
private:
QStandardItemModel* m_model{ nullptr }; // model
ItemDelegate* m_delegate{ nullptr };
};
}
以下四个成员函数
#include "FriendTree.h"
#include "ItemDelegate.h"
#include <QHeaderView>
#include <QTime>
#include <QMessageBox>
#include "GlobalDefines.h"
namespace qqfriendlist
{
FriendTree::FriendTree(QWidget *parent)
: TreeView(parent)
{
initUi();
initConnection();
}
FriendTree::~FriendTree()
{
}
/****************************************!
* @brief 赋值接口
* @param [in] const std::vector<Group> & groups
* @return void
****************************************/
void FriendTree::setValues(const std::vector<Group>& groups)
{
m_model->clear();
for (const auto& group : groups)
{
// 添加分组
QStandardItem* item = new QStandardItem(group.groupName);
item->setEditable(false);
item->setData(group.groupName, Qt::ToolTipRole);
item->setData(true, static_cast<int>(CustomRole::IsGroupRole));
m_model->appendRow(item);
for (const auto& contact : group.contactList)
{
// 分组下的联系人
QStandardItem* contactItem = new QStandardItem(contact.name);
contactItem->setEditable(false);
contactItem->setData(contact.name, Qt::ToolTipRole);
QVariant value{};
value.setValue(contact);
contactItem->setData(value, static_cast<int>(CustomRole::ContactRole));
item->appendRow(contactItem);
}
}
}
/****************************************!
* @brief 初始化界面
* @return void
****************************************/
void FriendTree::initUi()
{
setWindowTitle(QStringLiteral("QQ好友列表"));
// basic init
header()->hide(); // 隐藏表头
setIndentation(0); // 左边距设置为0
setAnimated(true); // 展开时动画
m_model = new QStandardItemModel(this);
setModel(m_model);
m_delegate = new ItemDelegate(this);
setItemDelegate(m_delegate);
}
/****************************************!
* @brief 初始化信号槽链接
* @return void
****************************************/
void FriendTree::initConnection()
{
// 点击事件
connect(this, &QTreeView::clicked, [&](const QModelIndex& index)
{
if (index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool())
{
setExpanded(index, !isExpanded(index)); // 单击展开/收缩列表
}
});
// 双击打开聊天
connect(this, &QTreeView::doubleClicked, [&](const QModelIndex& index)
{
if (!index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool())
{
// 不是分组Item才去处理双击事件
auto info = index.data(static_cast<int>(CustomRole::ContactRole)).value<Contact>();
QMessageBox msgBox;
msgBox.setWindowTitle(QStringLiteral("双击打开聊天"));
msgBox.setText(QStringLiteral("你好,") + info.name + QStringLiteral("。在不?"));
msgBox.exec();
}
});
// 展开时更换左侧的展开图标
connect(this, &QTreeView::expanded, [&](const QModelIndex& index)
{
m_model->itemFromIndex(index)->setData(true, static_cast<int>(CustomRole::IsExpandedRole));
});
// 收起时更换左侧的展开图标
connect(this, &QTreeView::collapsed, [&](const QModelIndex& index)
{
m_model->itemFromIndex(index)->setData(false, static_cast<int>(CustomRole::IsExpandedRole));
});
// 自定义hover事件
connect(this, &TreeView::signalHover, this, &FriendTree:
nHoverHandle);
// 自定义点击事件
connect(this, QOverload<const QModelIndex&, int>:
f(&TreeView::signalClicked), this, &FriendTree:
nClickedHandle);
}
/****************************************!
* @brief hover事件处理
* @param [in] const QModelIndex & index 索引项
* @param [in] int role 角色
* @return void
****************************************/
void FriendTree:
nHoverHandle(const QModelIndex& index, int role)
{
if (index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool())
{
return; // 群组的hover事件 退出
}
else
{
// 不同区域显示不同tooltip
auto info = index.data(static_cast<int>(CustomRole::ContactRole)).value<Contact>();
QString displayName{};
switch (role)
{
case static_cast<int>(CustomRole:
ortraitRole) : // 视频通话
{
displayName = QStringLiteral("鼠标移到头像上啦!");
break;
}
case static_cast<int>(CustomRole::VideoRole) : // 视频通话
{
displayName = QStringLiteral("视频通话");
break;
}
case static_cast<int>(CustomRole::SignatureRole) : // 个性签名
{
displayName = info.signature;
break;
}
default:
{
// 默认tooltip显示用户名称+QQ号
displayName = info.name + "(" + info.nickName + ")" + "(" + info.id + ")";
break;
}
}
m_model->itemFromIndex(index)->setData(displayName, Qt::ToolTipRole);
}
}
/****************************************!
* @brief 点击事件角色处理
* @param [in] const QModelIndex & index
* @param [in] int role
* @return void
****************************************/
void FriendTree:
nClickedHandle(const QModelIndex& index, int role)
{
// 不同区域显示不同tooltip
auto info = index.data(static_cast<int>(CustomRole::ContactRole)).value<Contact>();
switch (role)
{
case static_cast<int>(CustomRole::VideoRole) : // 视频通话
{
QMessageBox msgBox;
msgBox.setWindowTitle(QStringLiteral("视频通话"));
msgBox.setText(QTime::currentTime().toString("hh:mm:ss")
+ QStringLiteral(",向") + info.name + QStringLiteral("发起视频通话。"));
msgBox.exec();
break;
}
default:
{
break;
}
}
}
}
FriendTree类仅需要初始化model delegate,以及相关的点击事件,hover事件等。
ItemDelegate类主要工作解析
此类是TreeView列表样式绘制部分,样式基本全部在这个类中完成
#pragma once
#include "
ublicGui/TreeView/StyledDelegate.h"
using namespace publicgui;
namespace qqfriendlist
{
class ItemDelegate : public StyledDelegate
{
Q_OBJECT
public:
ItemDelegate(QObject *parent = Q_NULLPTR);
~ItemDelegate();
// 完成Item具体内容的绘制
virtual void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
// 绘制群组
virtual void paintGroup(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
// 绘制联系人
virtual void paintContact(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
protected:
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const Q_DECL_OVERRIDE;
// hover的role
virtual int getHoverEventRole(const QPoint& pos, const QStyleOptionViewItem& option, const QModelIndex &index) const;
// 点击的role
virtual int getMouseEventRole(const QPoint& pos, const QStyleOptionViewItem& option, const QModelIndex &index) const;
};
}
cpp文件
#include "ItemDelegate.h"
#include "
ublicGui/TreeView/DelegatePainter.h"
#include "GlobalDefines.h"
namespace qqfriendlist
{
namespace
{
const int kGroupItemHeight{ 35 };
const int kContactItemHeight{ 60 };
const QRect kGroupPullIconRect{ 10,12,11,11 }; // 群组下拉图标
const QRect kGroupNameRect{ 30,0,200,35 }; // 群组名称
const QRect kContactPortraitRect{ 10,10,40,40 }; // 联系人头像
const QRect kContactNameRect{ 60,10,200,20 }; // 联系人名字
const QRect kSignatureRect{ 60,30,160,20 }; // 联系人个性签名
const QRect kVipIconRect{ 60,30,30,12 }; // 联系人VIP图标
const QRect kOnlineStateIconRect{ 40,35,14,14 }; // 在线状态图标
const QRect kVideoIconRect{ 0,25,20,13 }; // 视频通话图标
}
ItemDelegate::ItemDelegate(QObject *parent)
: StyledDelegate(parent)
{
}
ItemDelegate::~ItemDelegate()
{
}
/****************************************!
* @brief 代理绘制
* @param [in] QPainter * painter
* @param [in] const QStyleOptionViewItem & option
* @param [in] const QModelIndex & index
* @return void
****************************************/
void ItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
DelegatePainter delegatePainter;
OperateActions operateActions = getOperateActions(option, index);
QColor color;
color = (operateActions.isHovered) ? QColor("#f0f0f0") //背景色选中
: (!operateActions.isSelected && operateActions.isHovered) ? QColor("lightblue") // hover
: QColor("#ffffff");
if (index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool())
{
paintGroup(painter, option, index);
return;
}
else
{
paintContact(painter, option, index); // 绘制联系人
}
}
/****************************************!
* @brief 绘制群组Item
* @param [in] QPainter * painter
* @param [in] const QStyleOptionViewItem & option
* @param [in] const QModelIndex & index
* @return void
****************************************/
void ItemDelegate::paintGroup(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
DelegatePainter delegatePainter;
OperateActions operateActions = getOperateActions(option, index);
QColor color;
color = (operateActions.isHovered) ? QColor("#f0f0f0") : QColor("#ffffff"); // hover时变灰
// 背景色
painter->setPen(Qt::NoPen);
painter->setBrush(color);
painter->drawRect(option.rect);
// 下拉列表图标
QRect pullIconRect(option.rect.left() + kGroupPullIconRect.x(), option.rect.top() + kGroupPullIconRect.y(),
kGroupPullIconRect.width(), kGroupPullIconRect.height());
QString pullIconPath{ ":/QQFriendList/Resources/images/expand_down.svg" };
if (!index.data(static_cast<int>(CustomRole::IsExpandedRole)).toBool())
{
pullIconPath = ":/QQFriendList/Resources/images/expand_right.svg";
}
delegatePainter.paintSvgImage(painter, pullIconPath, pullIconRect);
// 群组名称
QRect nameRect(option.rect.left() + kGroupNameRect.x(), option.rect.top() + kGroupNameRect.y(), kGroupNameRect.width(), kGroupNameRect.height());
delegatePainter.paintText(painter, option, index, Qt:
isplayRole, Qt::AlignLeft, QColor("black"), nameRect, 13);
}
/****************************************!
* @brief 绘制联系人信息
* @param [in] QPainter * painter
* @param [in] const QStyleOptionViewItem & option
* @param [in] const QModelIndex & index
* @return void
****************************************/
void ItemDelegate::paintContact(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
DelegatePainter delegatePainter;
OperateActions operateActions = getOperateActions(option, index);
QColor backgroundColor;
if (operateActions.isHovered && !operateActions.isSelected)
{
backgroundColor = QColor("#f2f2f2");
}
else if (operateActions.isSelected)
{
backgroundColor = QColor("#ebebeb");
}
else
{
backgroundColor = QColor("#ffffff");
}
// 背景色
painter->setPen(Qt::NoPen);
painter->setBrush(backgroundColor);
painter->drawRect(option.rect);
// 联系人信息
auto info = index.data(static_cast<int>(CustomRole::ContactRole)).value<Contact>();
// 联系人头像
{
QRect contactHeadPortraitRect(option.rect.left() + kContactPortraitRect.x(), option.rect.top() + kContactPortraitRect.y(),
kContactPortraitRect.width(), kContactPortraitRect.height());
QString contactHeadPortraitPath{ ":/QQFriendList/Resources/images/portrait_boy.svg" };
if (!info.sex)
{
contactHeadPortraitPath = ":/QQFriendList/Resources/images/portrait_girl.svg";
}
delegatePainter.paintSvgImage(painter, contactHeadPortraitPath, contactHeadPortraitRect);
}
// 联系人名称
{
QRect nameRect(option.rect.left() + kContactNameRect.x(), option.rect.top() + kContactNameRect.y(),
kContactNameRect.width(), kContactNameRect.height());
QColor nameColor{ "black" };
if (info.isVip) // 是vip
{
nameColor = QColor("#ff0000");
}
delegatePainter.paintText(painter, option, index, Qt:
isplayRole,
Qt::AlignLeft, nameColor, nameRect, 13, info.name + "(" + info.nickName + ")");
}
// 个性签名
{
QRect signatureRect(option.rect.left() + kSignatureRect.x(), option.rect.top() + kSignatureRect.y(),
kSignatureRect.width(), kSignatureRect.height());
delegatePainter.paintText(painter, option, index, Qt:
isplayRole,
Qt::AlignLeft, QColor("black"), signatureRect, 13, info.signature);
}
// 在线状态图标
{
QRect onlineStateIconRect(option.rect.left() + kOnlineStateIconRect.x(), option.rect.top() + kOnlineStateIconRect.y(),
kOnlineStateIconRect.width(), kOnlineStateIconRect.height());
QString onlineStateIconPath{ ":/QQFriendList/Resources/images/online-im.svg" };
switch (info.onlineState)
{
case OnlineState::Busy:
{
onlineStateIconPath = ":/QQFriendList/Resources/images/busy-im.svg";
break;
}
case OnlineState:
eave:
{
onlineStateIconPath = ":/QQFriendList/Resources/images/leave-im.svg";
break;
}
case OnlineState::Online:
{
break;
}
default:break;
}
// 防背景透明 先把背景处理了 用背景色backgroundColor画一个圆形区域
painter->setPen(Qt::NoPen);
painter->setBrush(backgroundColor);
painter->drawRoundedRect(onlineStateIconRect, onlineStateIconRect.width() / 2, onlineStateIconRect.height() / 2);
delegatePainter.paintSvgImage(painter, onlineStateIconPath, onlineStateIconRect);
}
// 视频通话
{
// 只有hover状态才会显示视频通话图标
if (operateActions.isHovered)
{
QRect videoRect(option.rect.left() + option.rect.width() - kVideoIconRect.width() - 16, option.rect.top() + kVideoIconRect.y(),
kVideoIconRect.width(), kVideoIconRect.height());
delegatePainter.paintSvgImage(painter, ":/QQFriendList/Resources/images/video.svg", videoRect);
}
}
}
/****************************************!
* @brief 根据Item不同调整高度
* @param [in] const QStyleOptionViewItem & option
* @param [in] const QModelIndex & index
* @return QSize
****************************************/
QSize ItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QSize size = QStyledItemDelegate::sizeHint(option, index);
if (index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool())
{
return QSize(size.width(), kGroupItemHeight); // 群组Item高度
}
return QSize(size.width(), kContactItemHeight); // 联系人Item高度
}
/****************************************!
* @brief 根据鼠标位置 判断hover的role是哪个 并返回
* @param [in] const QPoint & pos 鼠标位置
* @param [in] const QStyleOptionViewItem & option
* @param [in] const QModelIndex & index 索引位置
* @return int
****************************************/
int ItemDelegate::getHoverEventRole(const QPoint& pos, const QStyleOptionViewItem& option, const QModelIndex &index) const
{
// 视频通话图标位置
QRect videoRect(option.rect.left() + option.rect.width() - kVideoIconRect.width() - 16, option.rect.top() + kVideoIconRect.y(),
kVideoIconRect.width(), kVideoIconRect.height());
if (!index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool() && videoRect.contains(pos))
{
return static_cast<int>(CustomRole::VideoRole);
}
// 个性签名
QRect signatureRect(option.rect.left() + kSignatureRect.x(), option.rect.top() + kSignatureRect.y(),
kSignatureRect.width(), kSignatureRect.height());
if (!index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool() && signatureRect.contains(pos))
{
return static_cast<int>(CustomRole::SignatureRole);
}
// 头像
QRect contactHeadPortraitRect(option.rect.left() + kContactPortraitRect.x(), option.rect.top() + kContactPortraitRect.y(),
kContactPortraitRect.width(), kContactPortraitRect.height());
if (!index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool() && contactHeadPortraitRect.contains(pos))
{
return static_cast<int>(CustomRole:
ortraitRole);
}
return -1;
}
/****************************************!
* @brief 返回点击的Item的角色
* @param [in] const QPoint & pos
* @param [in] const QStyleOptionViewItem & option
* @param [in] const QModelIndex & index
* @return int
****************************************/
int ItemDelegate::getMouseEventRole(const QPoint& pos, const QStyleOptionViewItem& option, const QModelIndex &index) const
{
// 视频通话图标位置
QRect videoRect(option.rect.left() + option.rect.width() - kVideoIconRect.width() - 16, option.rect.top() + kVideoIconRect.y(),
kVideoIconRect.width(), kVideoIconRect.height());
if (!index.data(static_cast<int>(CustomRole::IsGroupRole)).toBool() && videoRect.contains(pos))
{
return static_cast<int>(CustomRole::VideoRole);
}
return -1;
}
}
工程源码路径/下载地址
开发环境:
vs2015+Qt5.9.6+ qt-vsaddin-msvc2015-2.2.2.vsix
所有源码路径:
https://github.com/lesliefish/Qt/tree/master/UI/QQFriendList
————————————————
版权声明:本文为CSDN博主「lesliefish」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/y396397735/article/details/86799470
欢迎光临 firemail (http://firemail.wang:8088/)
Powered by Discuz! X3