Discuz! Board

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 7092|回复: 0
打印 上一主题 下一主题

QQ好友列表的实现(QQ9.0版本样式) -- 使用QTreeView

[复制链接]

1272

主题

2067

帖子

7958

积分

认证用户组

Rank: 5Rank: 5

积分
7958
跳转到指定楼层
楼主
发表于 2020-3-10 18:45:15 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|firemail ( 粤ICP备15085507号-1 )

GMT+8, 2024-11-23 03:41 , Processed in 0.062101 second(s), 19 queries .

Powered by Discuz! X3

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表