Qter 发表于 2020-3-10 18:45:15

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 "PublicGui/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赋值接口
   * @paramconst 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::onHoverHandle);

      // 自定义点击事件
      connect(this, QOverload<const QModelIndex&, int>::of(&TreeView::signalClicked), this, &FriendTree::onClickedHandle);
    }

    /****************************************!
   * @briefhover事件处理
   * @paramconst QModelIndex & index 索引项
   * @paramint role 角色
   * @return void
   ****************************************/
    void FriendTree::onHoverHandle(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::PortraitRole) : // 视频通话
            {
                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点击事件角色处理
   * @paramconst QModelIndex & index
   * @paramint role
   * @return void
   ****************************************/
    void FriendTree::onClickedHandle(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 "PublicGui/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 "PublicGui/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代理绘制
   * @paramQPainter * painter
   * @paramconst QStyleOptionViewItem & option
   * @paramconst 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
   * @paramQPainter * painter
   * @paramconst QStyleOptionViewItem & option
   * @paramconst 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::DisplayRole, Qt::AlignLeft, QColor("black"), nameRect, 13);
    }

    /****************************************!
   * @brief绘制联系人信息
   * @paramQPainter * painter
   * @paramconst QStyleOptionViewItem & option
   * @paramconst 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::DisplayRole,
                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::DisplayRole,
                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::Leave:
            {
                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不同调整高度
   * @paramconst QStyleOptionViewItem & option
   * @paramconst 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是哪个 并返回
   * @paramconst QPoint & pos 鼠标位置
   * @paramconst QStyleOptionViewItem & option
   * @paramconst 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::PortraitRole);
      }
      return -1;
    }

    /****************************************!
   * @brief返回点击的Item的角色
   * @paramconst QPoint & pos
   * @paramconst QStyleOptionViewItem & option
   * @paramconst 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


页: [1]
查看完整版本: QQ好友列表的实现(QQ9.0版本样式) -- 使用QTreeView