MFC 自定义树控件:树节点的样式与交互

在本教程中,将介绍如何在 MFC 应用程序中使用树控件 (CTreeCtrl) 进行高级定制,包括设置字体、颜色、徽章、图标、节点的高度等。通过这些自定义设置,可以显著提升用户界面的交互性和视觉效果。

1. 树控件基本设置

首先,我们需要初始化树控件并进行一些基础的设置。以下代码展示了如何设置树控件的字体、背景色、选中项、悬停项等。

cpp 复制代码
// 设置树控件的背景色、选中项、悬停项及文本颜色
m_treeCtrl.UpdateFont(FontType::FONT_SEGOE_UI, 16); // 设置字体
m_treeCtrl.SetDefaultBkColor(RGB(240, 240, 240));   // 浅灰色背景
m_treeCtrl.SetSelectedBkColor(RGB(0, 0, 255));      // 蓝色选中项
m_treeCtrl.SetHoverBkColor(RGB(255, 255, 0));       // 黄色悬停项
m_treeCtrl.SetDefaultTextColor(RGB(0, 0, 0));       // 默认黑色文本
m_treeCtrl.SetSelectedTextColor(RGB(255, 0, 255));  // 白色选中文本
m_treeCtrl.SetHoverTextColor(RGB(255, 0, 0));       // 红色悬停项文本

在这里,我们设置了树控件的字体为 Segoe UI,字号为 16。然后定义了不同状态下的背景色和文本颜色,包括默认状态、选中状态和悬停状态的颜色。

2. 设置树项的高度

通过调用 SetItemHeight 方法,我们可以设置树控件中每个树项的高度。此方法允许我们对树项进行自定义布局,使界面更符合设计要求。

cpp 复制代码
// 设置树项高度
m_treeCtrl.SetItemHeight(50);

这段代码将树项的高度设置为 50,使得每个节点的显示区域更大,适合显示更多内容。

3. 插入节点并设置样式

树控件允许我们动态插入节点,并为每个节点设置不同的样式。以下是如何插入根节点和子节点,并为其设置背景色、图标、徽章等。

插入根节点

cpp 复制代码
// 插入根节点及其子节点
HTREEITEM hRoot = m_treeCtrl.InsertItem(_T("Root Node"));
m_treeCtrl.SetNodeBkColor(hRoot, RGB(100, 100, 255));    // 蓝色背景
m_treeCtrl.SetItemIcon(hRoot, m_hIcon);                  // 设置图标

在此,我们插入了一个名为"Root Node"的根节点,并设置了它的背景色为蓝色,以及图标。

插入子节点并设置徽章

cpp 复制代码
// 插入子节点及设置徽章
HTREEITEM hChild = m_treeCtrl.InsertItem(_T("Child Node"), hRoot);
m_treeCtrl.SetItemBadge(hChild, 20, 20, RGB(255, 0, 0), RGB(255, 255, 255)); // 红色徽章背景,白色徽章前景
m_treeCtrl.ShowItemBadgeNumber(hChild, 5);  // 显示数字 5
m_treeCtrl.SetNodeTextColor(hChild, RGB(255, 0, 0));

此段代码展示了如何插入一个子节点并为其设置一个徽章,徽章的背景色为红色,前景色为白色,并且显示了数字 5。此外,还修改了子节点的文本颜色为红色。

插入圆点徽章节点

cpp 复制代码
// 插入圆点徽章节点
HTREEITEM hDotNode = m_treeCtrl.InsertItem(_T("Dot Node"), hRoot);
m_treeCtrl.SetItemBadge(hDotNode, 20, 20, RGB(255, 0, 0), RGB(255, 255, 255)); // 设置徽章背景色和前景色
m_treeCtrl.ShowItemBadgeDotMode(hDotNode, 100, RGB(255, 0, 0), RGB(255, 255, 255)); // 显示圆点徽章

在这里,我们插入了一个带圆点徽章的节点,并为其设置了一个红色的圆点徽章,背景色为红色,前景色为白色。

插入加粗字体节点

cpp 复制代码
// 插入加粗字体节点
HTREEITEM hBoldNode = m_treeCtrl.InsertItem(_T("Bold Node"), hRoot);
m_treeCtrl.SetItemBold(hBoldNode);  // 设置加粗字体

我们还展示了如何将某个节点的字体设置为加粗样式,突出显示该节点。

4. 展开根节点

插入完所有节点后,我们可以使用 Expand 方法展开根节点,显示所有子节点。

cpp 复制代码
// 展开根节点
m_treeCtrl.Expand(hRoot, TVE_EXPAND);

这会将根节点展开,显示其子节点,使得树结构更加清晰可见。

5. 头文件和源文件

以下是一些需要添加的代码结构,用于实现上述功能:

头文件代码

cpp 复制代码
#if !defined(AFX_APREDTREECTRL_H__A4EABEC5_2E8C_11D1_B79F_00805F9ECE10__INCLUDED_)
#define AFX_APREDTREECTRL_H__A4EABEC5_2E8C_11D1_B79F_00805F9ECE10__INCLUDED_

#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000

#include <afxcmn.h>
#include <map>
#include <vector>

/**
 * @brief 消息宏,用于向父窗口发送树项目单击的通知。
 * 
 * 当用户单击树控件的项目时,父窗口会收到该消息。
 */
#define ID_MSG_TREE_CLICK_ITEM WM_USER + 2080

 /**
  * @brief 字体类型枚举。
  */
enum class FontType {
    FONT_TAHOMA,
    FONT_SEGOE_UI,
    FONT_MS_SANS_SERIF,
    FONT_ARIAL,
    FONT_WINGDINGS
    // 可以继续扩展其他字体
};

/**
 * @brief CApredTreeCtrl 类
 * 
 * CApredTreeCtrl 是一个继承自 MFC 的 CTreeCtrl 的自定义控件,扩展了以下功能:
 * - 为树控件的每个项目添加徽章(数字徽章、小圆点等)
 * - 支持项目的加粗显示
 * - 支持设置项目图标
 * - 自定义项目的背景颜色、文本颜色和悬停效果
 */
class CApredTreeCtrl : public CTreeCtrl {
public:
    /**
     * @brief 树项目徽章的定义结构体。
     * 
     * 描述了项目上的徽章类型、颜色及显示时间。
     */
    typedef struct tagBADGE {
        HTREEITEM hTreeItem;         ///< 树项目的句柄
        COLORREF badgeBackground;   ///< 徽章背景颜色
        COLORREF badgeForeground;   ///< 徽章前景(文字)颜色
        int width;                  ///< 徽章宽度
        int height;                 ///< 徽章高度
        int type;                   ///< 徽章类型:0(隐藏),1(小圆点),2(数字)
        int number;                 ///< 数字徽章显示的数字(仅当类型为数字时有效)
        int showTime;               ///< 徽章的显示时间,单位为秒(0 表示永久显示)
    } BADGE;

    /**
     * @brief 动态声明宏,用于支持动态创建和 RTTI。
     */
    DECLARE_DYNAMIC(CApredTreeCtrl)

    /**
     * @brief 构造函数:初始化控件的默认样式和资源。
     */
    CApredTreeCtrl();

    /**
     * @brief 析构函数:释放分配的资源。
     */
    virtual ~CApredTreeCtrl();

    /**
     * @brief 设置树项目的徽章。
     * 
     * @param hItem 树项目句柄
	 * @param width 徽章的宽度
	 * @param height 徽章的高度
     * @param badgeBackground 徽章的背景颜色
     * @param badgeForeground 徽章的前景(文字)颜色
     */
    void SetItemBadge(HTREEITEM hItem, int width = 20, int height = 20, COLORREF badgeBackground = RGB(255, 255, 255), COLORREF badgeForeground = RGB(0, 0, 0));

    /**
     * @brief 显示树项目的数字徽章。
     * 
     * @param hItem 树项目句柄
     * @param number 徽章中显示的数字
     * @param badgeBackground 徽章的背景颜色,默认为白色
     * @param badgeForeground 徽章的前景(字体)颜色,默认为黑色
     */
    void ShowItemBadgeNumber(HTREEITEM hItem, int number, COLORREF badgeBackground = RGB(255, 255, 255), COLORREF badgeForeground = RGB(0, 0, 0));

    /**
     * @brief 显示树项目的小圆点徽章。
     * 
     * @param hItem 树项目句柄
     * @param nSecond 徽章显示的持续时间,单位为秒,0 表示永久显示 
     * @param badgeBackground 徽章的背景颜色,默认为白色
     * @param badgeForeground 徽章的前景(字体)颜色,默认为黑色
     */
    void ShowItemBadgeDotMode(HTREEITEM hItem, int nSecond = 0, COLORREF badgeBackground = RGB(255, 255, 255), COLORREF badgeForeground = RGB(0, 0, 0));

    /**
     * @brief 隐藏树项目的徽章。
     * 
     * @param hItem 树项目句柄
     */
    void HideItemBadge(HTREEITEM hItem);

    /**
     * @brief 将树项目设置为加粗显示。
     * 
     * @param item 树项目句柄
     */
    void SetItemBold(HTREEITEM item);

    /**
     * @brief 取消树项目的加粗显示。
     * 
     * @param item 树项目句柄
     */
    void CancelItemBold(HTREEITEM item);

    /**
     * @brief 检查树项目是否为加粗状态。
     * 
     * @param item 树项目句柄
     * @return BOOL 如果项目是加粗的,返回 TRUE;否则返回 FALSE。
     */
    BOOL FindBoldItem(HTREEITEM item);

    /**
     * @brief 设置树项目的图标。
     * 
     * @param hItem 树项目句柄
     * @param hIcon 图标句柄
     */
    void SetItemIcon(HTREEITEM hItem, HICON hIcon);

    /**
     * @brief 设置默认背景色。
     * 
     * @param color 背景色值
     */
    void SetDefaultBkColor(COLORREF color);

    /**
     * @brief 设置选中项的背景色。
     * 
     * @param color 选中项的背景色值
     */
    void SetSelectedBkColor(COLORREF color);

    /**
     * @brief 设置悬停项的背景色。
     * 
     * @param color 悬停项的背景色值
     */
    void SetHoverBkColor(COLORREF color);

    /**
     * @brief 设置默认文本颜色。
     * 
     * @param color 文本颜色值
     */
    void SetDefaultTextColor(COLORREF color);

    /**
     * @brief 设置选中文本的颜色。
     * 
     * @param color 选中文本的颜色值
     */
    void SetSelectedTextColor(COLORREF color);

    /**
     * @brief 设置悬停项的文本颜色。
     * 
     * @param color 悬停项文本的颜色值
     */
    void SetHoverTextColor(COLORREF color);

    /**
     * @brief 为特定节点设置背景色。
     * 
     * @param hItem 树项目句柄
     * @param color 节点的背景色
     */
    void SetNodeBkColor(HTREEITEM hItem, COLORREF color);

    /**
     * @brief 为特定节点设置文本颜色。
     *
     * @param hItem 树项目句柄
     * @param color 节点的文本颜色
     */
    void CApredTreeCtrl::SetNodeTextColor(HTREEITEM hItem, COLORREF color);

    /**
     * @brief 刷新树控件,更新颜色显示。
     */
    void Refresh();

	/**
	 * @brief 刷新指定的树项目,更新颜色显示。
	 *
	 * @param hItem 树项目句柄
	 */
    void RefreshItem(HTREEITEM hItem);

	/**
	 * @brief 设置字体。
	 *
	 * @param eFont 字体类型
	 * @param nSize 字体大小
	 */
    void UpdateFont(FontType eFont, int nSize);

protected:
    /**
     * @brief 绘制树项目右侧的展开/收起按钮。
     * 
     * @param hItem 树项目句柄
     * @param hDC 设备上下文句柄
     * @param pRect 按钮的绘制区域
     */
    void DrawItemButton(HTREEITEM hItem, HDC hDC, CRect* pRect);

    /**
     * @brief 绘制树控件中的所有可见项目。
     * 
     * @param hDC 设备上下文句柄
     */
    void DrawItems(HDC hDC);

	/**
	 * @brief 绘制树项目的背景。
	 *
	 * @param hDC 设备上下文句柄
	 * @param hItem 树项目句柄
	 * @param pRect 绘制区域
	 */
    void DrawBadge(HTREEITEM hItem, HDC hDC, CRect* pRect, const BADGE& badge);

    // 私有数据成员
	std::map<HTREEITEM, COLORREF> m_itemTextColors;///< 节点文本颜色存储
    std::map<HTREEITEM, COLORREF> m_itemBkColors;  ///< 节点背景色存储
    std::map<HTREEITEM, BADGE> m_badges;           ///< 存储每个树项目的徽章信息
    std::map<HTREEITEM, HICON> m_icons;            ///< 存储每个树项目的图标信息
    std::vector<HTREEITEM> m_itemBolds;            ///< 存储需要加粗显示的树项目
    HBRUSH m_hBrushItem[3];                        ///< 树项目背景刷子(默认、选中、悬停)
    COLORREF m_crItemBk[3];                        ///< 树项目背景颜色(默认、选中、悬停)
    COLORREF m_crText[3];                          ///< 树项目文本颜色(默认、选中、悬停)
    HBRUSH m_hBrushBtn[3];                         ///< 按钮的背景刷子
    HPEN m_hPenItem[3];                            ///< 树项目边框画笔(默认、选中、悬停)
    HTREEITEM m_hHoverItem;                        ///< 当前鼠标悬停的树项目句柄
    BOOL m_bTracking;                              ///< 鼠标是否正在跟踪树项目(用于悬停效果)
	CFont m_font;								   ///< 树控件的字体

public:
    // MFC 消息映射
    DECLARE_MESSAGE_MAP()

    /**
     * @brief 重载的 WM_PAINT 消息处理函数,用于自定义绘制树控件。
     */
    afx_msg void OnPaint();

    /**
     * @brief 重载的 WM_MOUSEMOVE 消息处理函数,用于处理鼠标移动事件。
     */
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);

    /**
     * @brief 重载的 WM_MOUSEHOVER 消息处理函数,用于处理鼠标悬停事件。
     */
    afx_msg void OnMouseHover(UINT nFlags, CPoint point);

    /**
     * @brief 重载的 WM_MOUSELEAVE 消息处理函数,用于处理鼠标移出控件的事件。
     */
    afx_msg void OnMouseLeave();

    /**
     * @brief 重载的 WM_TIMER 消息处理函数,用于徽章的显示时间控制。
     */
    afx_msg void OnTimer(UINT_PTR nIDEvent);

    /**
     * @brief 重载的 WM_LBUTTONDOWN 消息处理函数,用于处理鼠标左键单击事件。
     */
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

    /**
     * @brief 重载的 WM_CREATE 消息处理函数,用于初始化定时器。
     */
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

    /**
     * @brief 重载的 PreSubclassWindow 函数,用于初始化定时器和其他资源。
     */
    virtual void PreSubclassWindow();

	/**
	 * @brief 重载的 WM_DESTROY 消息处理函数,用于释放资源。
	 */
    afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point);
};

#endif // !defined(AFX_APREDTREECTRL_H__A4EABEC5_2E8C_11D1_B79F_00805F9ECE10__INCLUDED_)

源文件代码

cpp 复制代码
#include "pch.h"
#include "ApredTreeCtrl.h"

#define ROFFSET 7                // 按钮距离右侧的偏移量
#define WIDE 10                  // 按钮宽度
#define WIDE2 5                  // 按钮宽度的一半
#define EXPANDED_WIDE 8          // 按钮展开时的宽度

// 徽章类型定义
#define BADGE_HIDE 0             // 不显示徽章
#define BADGE_DOT 1              // 小圆点徽章
#define BADGE_NUMBER 2           // 数字徽章
#define BADGE_RECTANGLE 3        // 矩形徽章
#define BADGE_TRIANGLE 4         // 三角形徽章

IMPLEMENT_DYNAMIC(CApredTreeCtrl, CTreeCtrl)

/**
 * @brief 构造函数:初始化控件默认样式和资源。
 */
CApredTreeCtrl::CApredTreeCtrl() {
    // 初始化背景刷子、边框画笔和颜色
    for (int i = 0; i < 3; ++i) {
        m_hBrushItem[i] = nullptr;
        m_hBrushBtn[i] = nullptr;
        m_hPenItem[i] = nullptr;
    }

    // 默认、选中、悬停的背景和文本颜色
    m_crItemBk[0] = RGB(255, 255, 255);
    m_crItemBk[1] = RGB(228, 229, 234);
    m_crItemBk[2] = RGB(236, 237, 241);

    m_crText[0] = RGB(28, 28, 28);
    m_crText[1] = RGB(28, 28, 28);
    m_crText[2] = RGB(28, 28, 28);

    m_hHoverItem = nullptr;      // 当前没有悬停的项目
    m_bTracking = FALSE;         // 鼠标未开始跟踪
}

/**
 * @brief 析构函数:释放资源。
 */
CApredTreeCtrl::~CApredTreeCtrl() {
    for (int i = 0; i < 3; ++i) {
        if (m_hBrushItem[i] != nullptr) ::DeleteObject(m_hBrushItem[i]);
        if (m_hBrushBtn[i] != nullptr) ::DeleteObject(m_hBrushBtn[i]);
        if (m_hPenItem[i] != nullptr) ::DeleteObject(m_hPenItem[i]);
    }
}

BEGIN_MESSAGE_MAP(CApredTreeCtrl, CTreeCtrl)
    ON_WM_PAINT()
    ON_WM_MOUSEMOVE()
    ON_WM_MOUSEHOVER()
    ON_WM_MOUSELEAVE()
    ON_WM_TIMER()
    ON_WM_LBUTTONDOWN()
    ON_WM_CREATE()
    ON_WM_LBUTTONDBLCLK()
END_MESSAGE_MAP()

/**
 * @brief 绘制展开/收起按钮。
 */
void CApredTreeCtrl::DrawItemButton(HTREEITEM hItem, HDC hDC, CRect* pRect) {
    POINT pt[3];  // 三角形顶点

    if ((GetItemState(hItem, TVIS_EXPANDED) & TVIS_EXPANDED) == TVIS_EXPANDED) {
        // 项目已展开,绘制向下的三角形
        int nBottomOffset = (pRect->Height() - EXPANDED_WIDE) / 2;
        pt[0].x = pRect->right - ROFFSET - EXPANDED_WIDE;
        pt[0].y = pRect->bottom - nBottomOffset;
        pt[1].x = pRect->right - ROFFSET;
        pt[1].y = pRect->bottom - nBottomOffset;
        pt[2].x = pRect->right - ROFFSET;
        pt[2].y = pRect->bottom - nBottomOffset - EXPANDED_WIDE;
    } else {
        // 项目未展开,绘制向右的三角形
        int nBottomOffset = (pRect->Height() - WIDE) / 2;
        pt[0].x = pRect->right - ROFFSET - WIDE2;
        pt[0].y = pRect->bottom - nBottomOffset - WIDE;
        pt[1].x = pRect->right - ROFFSET - WIDE2;
        pt[1].y = pRect->bottom - nBottomOffset;
        pt[2].x = pRect->right - ROFFSET;
        pt[2].y = pRect->bottom - nBottomOffset - WIDE2;
    }

    ::Polygon(hDC, pt, 3);  // 绘制三角形
}

/**
 * @brief 自定义绘制树控件。
 */
void CApredTreeCtrl::OnPaint() {
    HDC hDC, hMemDC;
    HBITMAP hBitmap;
    RECT rcClient;

    for (int i = 0; i < 3; i++) {
        m_hBrushItem[i] = CreateSolidBrush(m_crItemBk[i]);
        m_hBrushBtn[i] = CreateSolidBrush(m_crText[i]);
        m_hPenItem[i] = ::CreatePen(PS_SOLID, 1, m_crText[i]);
    }

    PAINTSTRUCT ps;
    hDC = ::BeginPaint(m_hWnd, &ps);  // 开始绘制
    GetClientRect(&rcClient);

    // 创建内存设备上下文和兼容位图
    hMemDC = ::CreateCompatibleDC(hDC);
    hBitmap = ::CreateCompatibleBitmap(hDC, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top);
    ::SelectObject(hMemDC, hBitmap);

    // 设置背景色
    HBRUSH hBrushBK = CreateSolidBrush(m_crItemBk[0]);
    ::FillRect(hMemDC, &rcClient, hBrushBK);
    DeleteObject(hBrushBK);

    // 绘制子项
    DrawItems(hMemDC);

    // 将内存 DC 中的内容复制到窗口 DC
    ::BitBlt(hDC, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, hMemDC, 0, 0, SRCCOPY);

    // 清理资源
    ::EndPaint(m_hWnd, &ps);
    ::DeleteObject(hBitmap);
    ::DeleteDC(hMemDC);
}

/**
 * @brief 绘制树控件的所有可见子项。
 */
void CApredTreeCtrl::DrawItems(HDC hDC) {
    HTREEITEM hCurrentItem;
    int itemState;
    CRect rcClient, rcItem, rcText;
    GetClientRect(&rcClient);
    //Gdiplus::Graphics graphics(hDC);
    //graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);

    // 统一设置透明背景
    ::SetBkMode(hDC, TRANSPARENT);

    // 获取第一个可见项
    hCurrentItem = GetFirstVisibleItem();
    do {
        if (GetItemRect(hCurrentItem, &rcItem, FALSE) && GetItemRect(hCurrentItem, &rcText, TRUE)) {
            rcText.right = rcItem.right - 16;
            rcText.left += 16;
            CRect fillRect(0, rcItem.top, rcClient.right, rcItem.bottom);
            if (rcItem.top > rcClient.bottom) {
                break;
            }

            // 背景颜色
            COLORREF nodeBackgroundColor = m_crItemBk[0]; // 默认背景色
            if (m_itemBkColors.find(hCurrentItem) != m_itemBkColors.end()) {
                nodeBackgroundColor = m_itemBkColors[hCurrentItem]; // 特定节点背景色
            }

            // 绘制背景:根据选中、悬停或特定节点背景色填充
            itemState = GetItemState(hCurrentItem, TVIF_STATE);
            COLORREF crText = m_crText[0];
            if (m_itemTextColors.find(hCurrentItem) != m_itemTextColors.end()) {
                crText = m_itemTextColors[hCurrentItem]; // 特定节点背景色
            }

            if (itemState & TVIS_SELECTED) {
                crText = m_crText[1];
                HRGN hRgn = CreateRoundRectRgn(rcItem.left, rcItem.top, rcItem.right, rcItem.bottom, 4, 4);
                ::FillRgn(hDC, hRgn, m_hBrushItem[1]);
                ::DeleteObject(hRgn);
            }
            else if (hCurrentItem == m_hHoverItem) {
                crText = m_crText[2];
                HRGN hRgn = CreateRoundRectRgn(rcItem.left, rcItem.top, rcItem.right, rcItem.bottom, 4, 4);
                ::FillRgn(hDC, hRgn, m_hBrushItem[2]);
                ::DeleteObject(hRgn);
            }
            else if (nodeBackgroundColor != m_crItemBk[0]) {
                // 为特定节点设置背景色
                HRGN hRgn = CreateRoundRectRgn(rcItem.left, rcItem.top, rcItem.right, rcItem.bottom, 4, 4);
                ::FillRgn(hDC, hRgn, ::CreateSolidBrush(nodeBackgroundColor));
                ::DeleteObject(hRgn);
            }

            // 处理根节点的三角形按钮和文本
            if (hCurrentItem == GetRootItem()) {
                ::SetBkMode(hDC, TRANSPARENT);  // 确保背景透明
                CRect rcBtn = rcText;
                rcBtn.right = rcText.left - 2;
                rcBtn.left = rcBtn.right - 30;
                rcBtn.left -= 8;
                DrawItemButton(hCurrentItem, hDC, &rcBtn);  // 绘制展开/收起按钮
            }
            // 处理其他节点的展开按钮
            else if (ItemHasChildren(hCurrentItem)) {
                CRect rcBtn = rcText;
                rcBtn.right = rcText.left - 2;
                rcBtn.left = rcBtn.right - 30;
                rcBtn.left -= 8;
                DrawItemButton(hCurrentItem, hDC, &rcBtn);  // 绘制展开/收起按钮
            }

            // 绘制图标(如果存在)
            if (m_icons.find(hCurrentItem) != m_icons.end()) {
                HICON hIcon = m_icons[hCurrentItem];
                DrawIconEx(hDC, 16, (rcItem.bottom - rcItem.top - 32) / 2, hIcon, 32, 32, 0, 0, DI_NORMAL);
                rcText.left += 32;
            }

            // 创建加粗字体
            LOGFONT lf;
            ::GetObject(m_font, sizeof(LOGFONT), &lf);
            lf.lfWeight = FW_BOLD;  // 设置加粗
            HFONT hFontBold = CreateFontIndirect(&lf);  // 创建加粗字体

            // 绘制文本
            ::SetTextColor(hDC, crText);
            CString strText = GetItemText(hCurrentItem);

            // 如果是加粗文本,选择加粗字体
            if (FindBoldItem(hCurrentItem)) {
                ::SelectObject(hDC, hFontBold);
                ::DrawText(hDC, strText, strText.GetLength(), &rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
                ::DeleteObject(hFontBold);  // 释放加粗字体资源
            }
            else {
                ::SelectObject(hDC, m_font);
                ::DrawText(hDC, strText, strText.GetLength(), &rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
            }

            // 是否有小圆点(如徽章)
            if (m_badges.find(hCurrentItem) != m_badges.end()) {
                BADGE& badge = m_badges[hCurrentItem];

                int x = rcItem.right - 18 - badge.width;
                int y = rcItem.top + (rcItem.Height() - badge.height) / 2;

                CRect rcBadge;
                rcBadge.left = x;
                rcBadge.right = rcBadge.left + badge.width;
                rcBadge.top = y;
                rcBadge.bottom = rcBadge.top + badge.height;

                if (badge.type == BADGE_NUMBER) {
                    ::SetTextColor(hDC, badge.badgeForeground);
                    char szBuffer[32];
                    sprintf_s(szBuffer, 32, "%d%s", min(badge.number, 9), badge.number > 9 ? "+" : "");
                    DrawText(hDC, CString(szBuffer), (int)strlen(szBuffer), &rcBadge, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
                }
                else {
                    DrawBadge(hCurrentItem, hDC, &rcBadge, badge);
                }
            }
        }
    } while ((hCurrentItem = GetNextVisibleItem(hCurrentItem)) != NULL);
}

void CApredTreeCtrl::DrawBadge(HTREEITEM hItem, HDC hDC, CRect* pRect, const BADGE& badge) {
    int x = pRect->left; // 徽章的 X 坐标
    int y = pRect->top;  // 徽章的 Y 坐标

    // 调试输出徽章位置
    TRACE("Badge position: x = %d, y = %d, width = %d, height = %d\n", x, y, badge.width, badge.height);

    // 启用抗锯齿(GDI中的高级图形模式)
    SetGraphicsMode(hDC, GM_ADVANCED);  // 启用高级图形模式
    SetBkMode(hDC, TRANSPARENT);  // 背景透明

    // 设置笔和画刷,创建抗锯齿效果
    HBRUSH hBrush = CreateSolidBrush(badge.badgeBackground);
    HPEN hPen = CreatePen(PS_SOLID, 1, badge.badgeForeground);  // 设置边框颜色
    SelectObject(hDC, hBrush);
    SelectObject(hDC, hPen);

    // GDI 绘制抗锯齿设置
    SetGraphicsMode(hDC, GM_ADVANCED);  // 启用高级图形模式
    SetBkMode(hDC, TRANSPARENT);  // 背景透明

    // 根据徽章的类型绘制图形
    switch (badge.type) {
    case BADGE_DOT:
    {
        // 绘制圆形徽章
        Ellipse(hDC, x, y, x + badge.width, y + badge.height);  // 绘制一个圆形

        // 使用 m_font 绘制文本
        ::SelectObject(hDC, m_font.GetSafeHandle());  // 设置字体
        ::SetTextColor(hDC, badge.badgeForeground);
        RECT rcText = { x, y, x + badge.width, y + badge.height };
        ::SetBkMode(hDC, TRANSPARENT);  // 设置透明背景

        char szBuffer[32];
        sprintf_s(szBuffer, 32, "%d", badge.showTime);  // 显示时间或其他数字
        DrawText(hDC, CString(szBuffer), -1, &rcText, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    }
    break;

    case BADGE_RECTANGLE:
    {
        // 绘制矩形徽章
        Rectangle(hDC, x, y, x + badge.width, y + badge.height);  // 绘制矩形
    }
    break;

    case BADGE_TRIANGLE:
    {
        // 绘制三角形徽章
        DrawItemButton(hItem, hDC, pRect);
    }
    break;

    default:
        // 如果没有匹配的类型,可以选择默认行为
        break;
    }

    // 清理资源
    DeleteObject(hBrush);
    DeleteObject(hPen);

    // 恢复图形模式
    SetGraphicsMode(hDC, GM_COMPATIBLE);  // 恢复为兼容模式,关闭抗锯齿
}

/**
 * @brief 鼠标移动事件处理:更新当前悬停的项目。
 */
void CApredTreeCtrl::OnMouseMove(UINT nFlags, CPoint point) {
    HTREEITEM hHoverItem = HitTest(point);  // 根据鼠标位置获取悬停的树项目

    // 检查是否需要更新悬停项
    if (m_hHoverItem != hHoverItem) {
        // 如果之前有悬停项,恢复它的状态
        if (m_hHoverItem != NULL) {
            // 恢复之前悬停项的状态(比如取消高亮)
            RefreshItem(m_hHoverItem);
        }

        m_hHoverItem = hHoverItem;  // 更新当前悬停项目
        if (m_hHoverItem != NULL) {
            // 更新当前悬停项的状态(比如高亮)
            RefreshItem(m_hHoverItem);
        }
    }

    // 追踪鼠标事件(只在第一次进入时设置)
    if (!m_bTracking) {
        TRACKMOUSEEVENT tme = {};
        tme.cbSize = sizeof(tme);
        tme.dwFlags = TME_HOVER | TME_LEAVE;  // 追踪鼠标悬停和离开事件
        tme.hwndTrack = m_hWnd;
        tme.dwHoverTime = 10;  // 鼠标停留时间阈值
        m_bTracking = _TrackMouseEvent(&tme);
    }

    CTreeCtrl::OnMouseMove(nFlags, point);  // 调用基类的默认处理
}

/**
 * @brief 鼠标悬停事件处理:可在此添加需要悬停时的逻辑。
 */
void CApredTreeCtrl::OnMouseHover(UINT nFlags, CPoint point) {
    // 此处可以添加鼠标悬停特定逻辑
    CTreeCtrl::OnMouseHover(nFlags, point);
}

/**
 * @brief 鼠标移出控件事件处理:重置悬停状态。
 */
void CApredTreeCtrl::OnMouseLeave() {
    if (m_hHoverItem != nullptr) {
        // 清空悬停项目并刷新
        HTREEITEM hPreviousHoverItem = m_hHoverItem;
        m_hHoverItem = nullptr;  // 清除悬停项

        // 刷新之前悬停项的区域
        CRect rcItem;
        if (GetItemRect(hPreviousHoverItem, &rcItem, FALSE)) {
            InvalidateRect(&rcItem, FALSE);  // 仅刷新该项的区域
        }
    }

    m_bTracking = FALSE;     // 停止鼠标事件追踪
    CTreeCtrl::OnMouseLeave(); // 调用基类的处理方法
}

/**
 * @brief 定时器事件处理:控制徽章显示时间。
 */
void CApredTreeCtrl::OnTimer(UINT_PTR nIDEvent) {
    if (nIDEvent == 1) {  // 检测定时器 ID
        for (auto& item : m_badges) {
            if (item.second.showTime > 0) {  // 如果有计时的徽章
                item.second.showTime--;      // 减少显示时间
                if (item.second.showTime == 0) {  // 时间到达时隐藏徽章
                    item.second.type = BADGE_HIDE;
                }

                // 只刷新当前徽章所在的项
                RefreshItem(item.first);
            }
        }
    }

    CTreeCtrl::OnTimer(nIDEvent);
}

/**
 * @brief 鼠标左键点击事件处理:选中项目并通知父窗口。
 */
void CApredTreeCtrl::OnLButtonDown(UINT nFlags, CPoint point) {
    HTREEITEM hItem = HitTest(point);  // 根据鼠标位置获取点击的树项目
    if (hItem != nullptr) {
        SelectItem(hItem);            // 选中点击的项目
        GetParent()->SendMessage(ID_MSG_TREE_CLICK_ITEM, (WPARAM)hItem, 0);  // 通知父窗口
    }

    CTreeCtrl::OnLButtonDown(nFlags, point);  // 调用基类的默认处理
}

/**
 * @brief 鼠标左键双击事件处理:展开或收起树项目。
 */
void CApredTreeCtrl::OnLButtonDblClk(UINT nFlags, CPoint point)
{
    HTREEITEM hItem = HitTest(point);

    if (hItem != NULL && ItemHasChildren(hItem)) {
        // 获取该节点的矩形区域
        CRect rcItem;
        GetItemRect(hItem, &rcItem, TRUE); // 获取包含文本区域的矩形

        if (!rcItem.PtInRect(point)) {
            // 检查节点是否已经展开
            UINT nState = GetItemState(hItem, TVIS_EXPANDED);  // 获取项的状态
            if (nState & TVIS_EXPANDED) {
                Expand(hItem, TVE_COLLAPSE);
            }
            else {
                Expand(hItem, TVE_EXPAND);
            }

            // 在展开或收起后,刷新该项
            RefreshItem(hItem);
        }
    }

    // 调用父类的处理方法
    CTreeCtrl::OnLButtonDblClk(nFlags, point);
}

/**
 * @brief 设置树项目的徽章(背景色和前景色)。
 */
void CApredTreeCtrl::SetItemBadge(HTREEITEM hItem, int width /*= 20*/, int height /*= 20*/, COLORREF badgeBackground /*= RGB(255, 255, 255)*/, COLORREF badgeForeground /*= RGB(255, 255, 255)*/) {
    if (m_badges.find(hItem) == m_badges.end()) {
        // 如果项目没有徽章,创建一个新的徽章记录
        BADGE badge = {
            hItem,                      // 树项目句柄
            badgeBackground,            // 背景颜色
            badgeForeground,            // 前景颜色
            width,                      // 徽章宽度
            height,                     // 徽章高度
            BADGE_DOT,                  // 徽章类型(小圆点)
            0,                          // 数字徽章的数字
            0                           // 显示时间(0 表示永久显示)
        };
        m_badges[hItem] = badge;
    } else {
        // 更新现有徽章的信息
        BADGE& badge = m_badges[hItem];
        badge.badgeBackground = badgeBackground;
        badge.badgeForeground = badgeForeground;
		badge.width = width;
		badge.height = height;
    }
}

/**
 * @brief 显示树项目的数字徽章。
 */
void CApredTreeCtrl::ShowItemBadgeNumber(HTREEITEM hItem, int number, COLORREF badgeBackground /*= RGB(255, 255, 255)*/, COLORREF badgeForeground /*= RGB(0, 0, 0)*/) {
    if (m_badges.find(hItem) != m_badges.end()) {
        BADGE& badge = m_badges[hItem];
        badge.type = BADGE_NUMBER;
        badge.number = number;
		badge.badgeBackground = badgeBackground;
		badge.badgeForeground = badgeForeground;
        InvalidateRect(NULL, TRUE);  // 刷新控件
    }
}

/**
 * @brief 显示树项目的小圆点徽章。
 */
void CApredTreeCtrl::ShowItemBadgeDotMode(HTREEITEM hItem, int nSecond /*= 0*/, COLORREF badgeBackground /*= RGB(255, 255, 255)*/, COLORREF badgeForeground /*= RGB(0, 0, 0)*/) {
    if (m_badges.find(hItem) != m_badges.end()) {
        BADGE& badge = m_badges[hItem];
        badge.type = BADGE_DOT;
        badge.showTime = nSecond;  // 设置显示时长
		badge.badgeBackground = badgeBackground;
		badge.badgeForeground = badgeForeground;
        InvalidateRect(NULL, TRUE);
    }
}

/**
 * @brief 隐藏树项目的徽章。
 */
void CApredTreeCtrl::HideItemBadge(HTREEITEM hItem) {
    if (m_badges.find(hItem) != m_badges.end()) {
        BADGE& badge = m_badges[hItem];
        badge.type = BADGE_HIDE;
        badge.showTime = 0;
        InvalidateRect(NULL, TRUE);
    }
}

/**
 * @brief 将树项目设置为加粗显示。
 */
void CApredTreeCtrl::SetItemBold(HTREEITEM item) {
    if (!FindBoldItem(item)) {
        m_itemBolds.push_back(item);  // 加入加粗列表
        InvalidateRect(NULL, TRUE);  // 刷新控件
    }
}

/**
 * @brief 取消树项目的加粗显示。
 */
void CApredTreeCtrl::CancelItemBold(HTREEITEM item) {
    for (auto iter = m_itemBolds.begin(); iter != m_itemBolds.end(); ++iter) {
        if (*iter == item) {
            m_itemBolds.erase(iter);  // 从加粗列表中移除
            break;
        }
    }

    InvalidateRect(NULL, TRUE);  // 刷新控件
}

/**
 * @brief 检查树项目是否为加粗状态。
 */
BOOL CApredTreeCtrl::FindBoldItem(HTREEITEM item) {
    return std::find(m_itemBolds.begin(), m_itemBolds.end(), item) != m_itemBolds.end();
}

/**
 * @brief 初始化时钟或其他资源。
 */
int CApredTreeCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) {
    if (CTreeCtrl::OnCreate(lpCreateStruct) == -1) {
        return -1;
    }

    SetTimer(1, 1000, nullptr);  // 设置定时器,间隔 1 秒
    return 0;
}

/**
 * @brief 设置图标到树项目。
 */
void CApredTreeCtrl::SetItemIcon(HTREEITEM hItem, HICON hIcon) {
    m_icons[hItem] = hIcon;  // 存储图标
    InvalidateRect(NULL, TRUE);  // 刷新控件
}

/**
 * @brief 子类化窗口前的初始化。
 */
void CApredTreeCtrl::PreSubclassWindow() {
    SetTimer(1, 1000, nullptr);  // 初始化定时器
    CTreeCtrl::PreSubclassWindow();
}

/**
 * @brief 设置默认背景色。
 */
void CApredTreeCtrl::SetDefaultBkColor(COLORREF color) {
    m_crItemBk[0] = color;
    if (m_hBrushItem[0]) {
        ::DeleteObject(m_hBrushItem[0]);
    }
    m_hBrushItem[0] = CreateSolidBrush(color);
    Refresh();
}

/**
 * @brief 设置选中项背景色。
 */
void CApredTreeCtrl::SetSelectedBkColor(COLORREF color) {
    m_crItemBk[1] = color;
    if (m_hBrushItem[1]) {
        ::DeleteObject(m_hBrushItem[1]);
    }
    m_hBrushItem[1] = CreateSolidBrush(color);
    Refresh();
}

/**
 * @brief 设置悬停项背景色。
 */
void CApredTreeCtrl::SetHoverBkColor(COLORREF color) {
    m_crItemBk[2] = color;
    if (m_hBrushItem[2]) {
        ::DeleteObject(m_hBrushItem[2]);
    }
    m_hBrushItem[2] = CreateSolidBrush(color);
    Refresh();
}

/**
 * @brief 设置默认文本色。
 */
void CApredTreeCtrl::SetDefaultTextColor(COLORREF color) {
    m_crText[0] = color;
    Refresh();
}

/**
 * @brief 设置选中文本色。
 */
void CApredTreeCtrl::SetSelectedTextColor(COLORREF color) {
    m_crText[1] = color;
    Refresh();
}

/**
 * @brief 设置悬停文本色。
 */
void CApredTreeCtrl::SetHoverTextColor(COLORREF color) {
    m_crText[2] = color;
    Refresh();
}

/**
 * @brief 为特定节点设置背景色。
 */
void CApredTreeCtrl::SetNodeBkColor(HTREEITEM hItem, COLORREF color) {
    m_itemBkColors[hItem] = color;
    Refresh();
}

/**
 * @brief 为特定节点设置文本颜色。
 */
void CApredTreeCtrl::SetNodeTextColor(HTREEITEM hItem, COLORREF color)
{
    m_itemTextColors[hItem] = color;  // 设置指定节点的文本颜色
    Refresh();  // 刷新树控件以应用更改
}

/**
 * @brief 刷新控件。
 */
void CApredTreeCtrl::Refresh() {
    InvalidateRect(NULL, TRUE); // 强制重绘控件
}

void CApredTreeCtrl::RefreshItem(HTREEITEM hItem) 
{
    CRect rcItem;
    if (GetItemRect(hItem, &rcItem, FALSE)) {  // 改为 FALSE,获取整个项的矩形区域
        InvalidateRect(&rcItem, FALSE);  // 刷新整个项的区域
    }
}

void CApredTreeCtrl::UpdateFont(FontType eFont, int nSize)
{
    LOGFONT logFont = { 0 };

    // 根据 FontType 枚举选择对应的字体
    switch (eFont)
    {
    case FontType::FONT_TAHOMA:
        _tcscpy_s(logFont.lfFaceName, _T("Tahoma"));
        break;
    case FontType::FONT_SEGOE_UI:
        _tcscpy_s(logFont.lfFaceName, _T("Segoe UI"));
        break;
    case FontType::FONT_MS_SANS_SERIF:
        _tcscpy_s(logFont.lfFaceName, _T("MS Sans Serif"));
        break;
    case FontType::FONT_ARIAL:
        _tcscpy_s(logFont.lfFaceName, _T("Arial"));
        break;
    case FontType::FONT_WINGDINGS:
        _tcscpy_s(logFont.lfFaceName, _T("Wingdings"));
        break;
        // 可以继续添加其他字体
    default:
        _tcscpy_s(logFont.lfFaceName, _T("Segoe UI"));  // 默认字体
        break;
    }

    // 设置字体的大小
    logFont.lfHeight = -nSize;  // 字体大小为负值表示像素大小
    logFont.lfQuality = CLEARTYPE_QUALITY;  // 清晰字体质量

    // 删除旧字体并创建新字体
    m_font.DeleteObject();
    m_font.CreateFontIndirect(&logFont);

    // 设置新的字体到控件
    SetFont(&m_font);
}

总结

通过上述代码,我们可以轻松实现树控件的个性化定制,设置节点的字体、颜色、徽章、图标等样式。这些功能不仅提升了用户体验,还能够增强界面的可操作性和美观度。你可以根据项目需求灵活调整设置,以达到最佳效果。如果 GDI 抗锯齿效果不理想,考虑使用 GDI+(Graphics)来绘制圆形和文本,它提供了更高质量的抗锯齿效果。

相关推荐
熬夜学编程的小王2 分钟前
【C++进阶篇】C++继承进阶:深入理解继承的复杂性
c++·继承·开发项目·虚拟继承·继承与友元·继承与静态函数
轩情吖35 分钟前
C++类型转换
java·开发语言·c++·多态·c++类型转换·rtti
爱吃涮毛肚的肥肥(暂时吃不了版)36 分钟前
数据结构与算法——1127—异或^&&
开发语言·数据结构·c++·算法
erxij1 小时前
【游戏引擎之路】登神长阶(十五)——DirectX12龙书:行百里者半九十(学习阶段完结)
c++·经验分享·学习·游戏·3d·游戏引擎
天赐细莲2 小时前
(C语言) 8大翻译阶段
c语言·开发语言·c++·windows·微软·编辑器
Zfox_3 小时前
【Linux】线程池设计 + 策略模式
linux·运维·c语言·c++·策略模式
翔云API4 小时前
人脸识别API解锁智能生活、C++人脸识别接口软文
开发语言·数据库·c++·python·ios·php
dongdongcoding4 小时前
c++中的函数指针
开发语言·c++
蚰蜒螟4 小时前
openjdk17 jvm 对象 内存溢出 在C++源码体现
开发语言·jvm·c++