Skip to content

树形视图控件 (STreeView)

Warning

The current page still doesn't have a translation for this language.

You can read it through google translate.

树形视图是一个高性能的 MVC 模式控件,用于展示具有层级结构的数据。它支持展开/折叠操作,支持节点选择,并且可以通过自定义模板来实现复杂的节点显示。

基本信息

  • 类名STreeView
  • 控件标签treeview
  • 基类SScrollView
  • 功能:提供树形结构数据的展示和交互

属性说明

基本属性

属性名 类型 默认值 说明
indent int 16 子节点缩进宽度
itemHeight int 20 节点项高度
toggleSkin string - 展开/折叠按钮皮肤
checkSkin string - 复选框皮肤
enableCheck bool 0 是否启用节点复选功能
multiSelect bool 0 是否允许多选
lines bool 1 是否显示连接线
rootToggle bool 0 根节点是否显示展开/折叠按钮

外观属性

属性名 类型 默认值 说明
colorText color 0 文本颜色
colorSelText color 0 选中项文本颜色
colorItemBkgnd color RGBA(255,255,255,255) 项目背景色
colorSelBkgnd color RGBA(0,0,255,255) 选中项背景色
hasLines bool 1 是否显示连接线
lineColor color RGBA(0,0,0,255) 连接线颜色

使用示例

基本树形视图

<treeview name="tvBasic" 
          pos="10,10,210,310" 
          indent="20"
          itemHeight="25">
    <template>
        <window class="treeitem" 
                layout="hbox" 
                padding="4,0,4,0">
            <toggle name="switch"/>
            <text name="text"/>
        </window>
    </template>
</treeview>

带复选框的树形视图

<treeview name="tvCheck" 
          pos="220,10,420,310" 
          enableCheck="1"
          checkSkin="checkbox">
    <template>
        <window class="treeitem" 
                layout="hbox" 
                padding="4,0,4,0">
            <toggle name="switch"/>
            <check name="check"/>
            <text name="text"/>
        </window>
    </template>
</treeview>

自定义模板树形视图

<treeview name="tvCustom" 
          pos="430,10,630,310">
    <template>
        <window class="treeitem" 
                layout="hbox" 
                padding="4,0,4,0">
            <toggle name="switch"/>
            <img name="icon" 
                 size="16,16" 
                 margin="0,0,4,0"/>
            <window size="0,-2" 
                    weight="1" 
                    layout="vbox" 
                    gravity="center">
                <text name="title" 
                      font="bold:1"/>
                <text name="desc" 
                      font="size:12"
                      colorText="#666666"/>
            </window>
        </window>
    </template>
</treeview>

带图标和操作按钮的树形视图

<treeview name="tvAction" 
          pos="10,320,-10,-10"
          indent="24"
          itemHeight="30">
    <template>
        <window class="treeitem" 
                layout="hbox" 
                padding="4,0,4,0"
                gravity="center">
            <toggle name="switch" 
                    size="16,16"/>
            <img name="icon" 
                 size="20,20" 
                 margin="4,0"/>
            <text name="title" 
                  size="0,0" 
                  flex="1"/>
            <button name="btn_action"
                    size="60,24"
                    text="操作"
                    margin="4,0"/>
        </window>
    </template>
</treeview>

数据模型

实现数据适配器

#include <helper/SAdapterBase.h>

class CTreeViewAdapter : public STreeViewAdapter
{
protected:
    // 定义节点数据结构
    struct ItemData {
        SStringT title;
        SStringT desc;
        SStringT iconSkin;
        bool hasChildren;
        bool bEnabled;
    };
    SArray<ItemData> m_items;

public:
    // 获取子节点数量,注意 WINAPI 调用约定
    virtual int WINAPI getChildCount(HSTREEITEM hItem) {
        return hItem ? 5 : 3; // 示例:每个节点有5个子节点
    }

    // 判断是否有子节点,注意 WINAPI 调用约定
    virtual bool WINAPI hasChildren(HSTREEITEM hItem) {
        return true; // 示例:所有节点都可以有子节点
    }

    // 获取显示文本,注意 WINAPI 调用约定
    virtual void WINAPI getText(HSTREEITEM hItem,
                        int subIndex,
                        SStringT& strText) {
        strText.Format(_T("Item %d"), subIndex);
    }

    // 获取节点状态,注意 WINAPI 调用约定
    virtual int WINAPI getItemState(HSTREEITEM hItem) {
        return WndState_Normal;
    }

    // 获取项目视图,注意 WINAPI 调用约定
    virtual void WINAPI getView(HSTREEITEM hItem, 
                        SItemPanel* pItem, 
                        SXmlNode xmlTemplate)
    {
        if(pItem && pItem->GetChildrenCount() == 0)
        {
            pItem->InitFromXml(&xmlTemplate);

            // 订阅操作按钮事件
            SWindow* pBtn = pItem->FindChildByName(L"btn_action");
            if(pBtn)
            {
                pBtn->GetEventSet()->subscribeEvent(
                    EventCmd::EventID,
                    Subscriber(&CTreeViewAdapter::OnActionClick, this));
            }
        }

        // 更新项目内容
        UpdateItemView(hItem, pItem);
    }

    // 更新项目视图
    void UpdateItemView(HSTREEITEM hItem, SWindow* pItem)
    {
        if(!pItem) return;

        int nIndex = GetItemIndex(hItem);
        if(nIndex < 0 || nIndex >= m_items.GetCount()) return;

        const ItemData& item = m_items[nIndex];

        // 设置图标
        SWindow* pIcon = pItem->FindChildByName(L"icon");
        if(pIcon)
        {
            pIcon->SetAttribute(L"skin", item.iconSkin);
        }

        // 设置标题
        SWindow* pTitle = pItem->FindChildByName(L"title");
        if(pTitle)
        {
            pTitle->SetWindowText(item.title);
        }

        // 设置描述
        SWindow* pDesc = pItem->FindChildByName(L"desc");
        if(pDesc)
        {
            pDesc->SetWindowText(item.desc);
        }

        // 设置启用状态
        pItem->EnableWindow(item.bEnabled);
    }

    // 处理操作按钮点击
    bool OnActionClick(EventArgs* pEvt)
    {
        SWindow* pBtn = sobj_cast<SWindow>(pEvt->sender);
        if(pBtn)
        {
            SItemPanel* pItem = sobj_cast<SItemPanel>(pBtn->GetParent());
            if(pItem)
            {
                HSTREEITEM hItem = (HSTREEITEM)pItem->GetItemIndex();
                // 处理操作按钮点击
                HandleActionClick(hItem);
            }
        }
        return true;
    }

    // 添加根节点
    HSTREEITEM AddRootItem(const ItemData& item)
    {
        m_items.Add(item);
        HSTREEITEM hItem = InsertItem(item, STVI_ROOT);
        return hItem;
    }

    // 添加子节点
    HSTREEITEM AddChildItem(HSTREEITEM hParent, const ItemData& item)
    {
        m_items.Add(item);
        HSTREEITEM hItem = InsertItem(item, hParent);
        return hItem;
    }
};

绑定数据适配器

void BindTreeViewData()
{
    STreeView *pTreeView = 
        FindChildByName2<STreeView>(L"tvDemo");
    if(pTreeView) {
        CTreeViewAdapter *pAdapter = new CTreeViewAdapter;

        // 添加根节点
        CTreeViewAdapter::ItemData rootItem;
        rootItem.title = L"根节点";
        rootItem.desc = L"这是根节点";
        rootItem.iconSkin = L"skin_folder";
        rootItem.hasChildren = true;
        rootItem.bEnabled = true;
        pAdapter->AddRootItem(rootItem);

        // 添加子节点
        CTreeViewAdapter::ItemData childItem;
        childItem.title = L"子节点";
        childItem.desc = L"这是子节点";
        childItem.iconSkin = L"skin_file";
        childItem.hasChildren = false;
        childItem.bEnabled = true;
        pAdapter->AddChildItem(/*hParent*/, childItem);

        pTreeView->SetAdapter(pAdapter);
        pAdapter->Release();
    }
}

事件处理

树形视图控件支持以下事件:

事件名 EventID 说明
EVT_TV_SELCHANGING EventTVSelChanging::EventID 选择改变前事件
EVT_TV_SELCHANGED EventTVSelChanged::EventID 选择改变事件
// 事件处理示例
EVENT_MAP_BEGIN()
    EVENT_NAME_HANDLER(L"tvBasic", EventTVSelChanged::EventID, OnTVSelChanged)
EVENT_MAP_END()

void OnTVSelChanged(IEvtArgs *pEvt)
{
    EventTVSelChanged *pRealEvt = sobj_cast<EventTVSelChanged>(pEvt);
    int nOldSel = pRealEvt->nOldSel;
    int nNewSel = pRealEvt->nNewSel;
    // 处理选择改变事件
}

代码操作

// 查找树形视图控件
STreeView *pTreeView = FindChildByName2<STreeView>(L"tvBasic");

// 设置适配器
pTreeView->SetAdapter(pAdapter);



// 展开/折叠节点
pTreeView->Expand(hItem, TRUE);  // 展开
pTreeView->Expand(hItem, FALSE); // 折叠

// 获取选中节点
HSTREEITEM hSelectedItem = pTreeView->GetSelectedItem();

// 选中指定节点
pTreeView->SelectItem(hItem);

// 获取节点文本
SStringT strText;
pTreeView->GetItemText(hItem, strText);

// 获取父节点
HSTREEITEM hParent = pTreeView->GetParentItem(hItem);

// 获取子节点
HSTREEITEM hChild = pTreeView->GetChildItem(hItem);

// 获取下一个兄弟节点
HSTREEITEM hNext = pTreeView->GetNextSiblingItem(hItem);

实现文件浏览器示例

#include <helper/SAdapterBase.h>

// 文件树节点数据
struct FileTreeNode
{
    SStringT strName;        // 节点名称
    SStringT strFullPath;    // 完整路径
    bool bIsFolder;          // 是否为文件夹
    SArray<FileTreeNode*> children; // 子节点
};

// 文件树适配器
class CFileTreeAdapter : public STreeViewAdapter
{
private:
    FileTreeNode* m_pRootNode;

public:
    CFileTreeAdapter() : m_pRootNode(NULL)
    {
        // 初始化根节点
        m_pRootNode = new FileTreeNode();
        m_pRootNode->strName = L"计算机";
        m_pRootNode->strFullPath = L"";
        m_pRootNode->bIsFolder = true;
    }

    ~CFileTreeAdapter()
    {
        DeleteNode(m_pRootNode);
    }

    // 递归删除节点
    void DeleteNode(FileTreeNode* pNode)
    {
        if(!pNode) return;

        for(int i = 0; i < pNode->children.GetCount(); i++)
        {
            DeleteNode(pNode->children[i]);
        }
        delete pNode;
    }

    // 获取子节点数量,注意 WINAPI 调用约定
    virtual int WINAPI getChildCount(HSTREEITEM hItem)
    {
        FileTreeNode* pNode = (FileTreeNode*)GetItemData(hItem);
        if(!pNode) return 0;

        if(pNode->bIsFolder && pNode->children.GetCount() == 0)
        {
            // 动态加载子节点
            LoadChildren(pNode);
        }

        return pNode->children.GetCount();
    }

    // 判断是否有子节点,注意 WINAPI 调用约定
    virtual bool WINAPI hasChildren(HSTREEITEM hItem)
    {
        FileTreeNode* pNode = (FileTreeNode*)GetItemData(hItem);
        if(!pNode) return false;

        if(pNode->bIsFolder)
        {
            // 对于文件夹,检查是否包含子项
            if(pNode->children.GetCount() == 0)
            {
                // 检查实际文件系统
                return HasFileSystemChildren(pNode->strFullPath);
            }
            return pNode->children.GetCount() > 0;
        }
        return false;
    }

    // 获取显示文本,注意 WINAPI 调用约定
    virtual void WINAPI getText(HSTREEITEM hItem, int subIndex, SStringT& strText)
    {
        FileTreeNode* pNode = (FileTreeNode*)GetItemData(hItem);
        if(pNode)
        {
            strText = pNode->strName;
        }
    }

    // 获取项目视图,注意 WINAPI 调用约定
    virtual void WINAPI getView(HSTREEITEM hItem, SItemPanel* pItem, SXmlNode xmlTemplate)
    {
        if(pItem && pItem->GetChildrenCount() == 0)
        {
            pItem->InitFromXml(&xmlTemplate);
        }

        FileTreeNode* pNode = (FileTreeNode*)GetItemData(hItem);
        if(pNode && pItem)
        {
            // 设置图标
            SWindow* pIcon = pItem->FindChildByName(L"icon");
            if(pIcon)
            {
                pIcon->SetAttribute(L"skin", 
                    pNode->bIsFolder ? L"skin_folder" : L"skin_file");
            }

            // 设置标题
            SWindow* pTitle = pItem->FindChildByName(L"title");
            if(pTitle)
            {
                pTitle->SetWindowText(pNode->strName);
            }
        }
    }

    // 加载子节点
    void LoadChildren(FileTreeNode* pParentNode)
    {
        if(!pParentNode || !pParentNode->bIsFolder) return;

        // 这里应该实际读取文件系统目录
        // 示例代码,实际应用中需要替换为真实的文件系统操作
        /*
        WIN32_FIND_DATA findData;
        SStringT strSearchPath = pParentNode->strFullPath + L"\\*";
        HANDLE hFind = FindFirstFile(strSearchPath, &findData);

        if(hFind != INVALID_HANDLE_VALUE)
        {
            do
            {
                if(lstrcmp(findData.cFileName, L".") == 0 || 
                   lstrcmp(findData.cFileName, L"..") == 0)
                {
                    continue;
                }

                FileTreeNode* pChildNode = new FileTreeNode();
                pChildNode->strName = findData.cFileName;
                pChildNode->strFullPath = pParentNode->strFullPath + L"\\" + findData.cFileName;
                pChildNode->bIsFolder = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;

                pParentNode->children.Add(pChildNode);
            } while(FindNextFile(hFind, &findData));

            FindClose(hFind);
        }
        */
    }

    // 检查文件系统中是否有子项
    bool HasFileSystemChildren(const SStringT& strPath)
    {
        // 实际应用中实现检查目录是否包含文件或子目录
        return true; // 示例返回true
    }

    // 添加根目录
    void AddRootDirectory(const SStringT& strPath)
    {
        FileTreeNode* pNode = new FileTreeNode();
        pNode->strName = GetFileNameFromPath(strPath);
        pNode->strFullPath = strPath;
        pNode->bIsFolder = true;

        m_pRootNode->children.Add(pNode);
        InsertItem(pNode, STVI_ROOT);
    }

    // 从路径获取文件名
    SStringT GetFileNameFromPath(const SStringT& strPath)
    {
        int nPos = strPath.ReverseFind(L'\\');
        if(nPos != -1)
        {
            return strPath.Mid(nPos + 1);
        }
        return strPath;
    }
};

// 使用文件树
void InitFileTree()
{
    STreeView* pTreeView = FindChildByName2<STreeView>(L"tvFileTree");
    if(pTreeView)
    {
        CFileTreeAdapter* pAdapter = new CFileTreeAdapter();

        // 添加驱动器
        pAdapter->AddRootDirectory(L"C:");
        pAdapter->AddRootDirectory(L"D:");

        pTreeView->SetAdapter(pAdapter);
        pAdapter->Release();
    }
}

样式定制

基本样式

<style>
    <class name="treeStyle"
           colorText="#333333"
           colorSelText="#FFFFFF"
           colorItemBkgnd="#FFFFFF"
           colorSelBkgnd="#1E90FF"
           hasLines="1"
           lineColor="#CCCCCC"/>
</style>

<treeview class="treeStyle"/>

自定义连接线

<treeview hasLines="1" 
          lineColor="#1E90FF"
          indent="24">
    <template>
        <!-- 自定义模板 -->
    </template>
</treeview>

不同样式节点

<treeview name="tvStyled">
    <template>
        <window class="treeitem">
            <toggle name="switch"/>
            <img name="icon"/>
            <text name="text"/>
        </window>
    </template>
</treeview>

<style>
    <!-- 普通节点样式 -->
    <treeitem colorText="#333333" 
              colorBkgnd="#FFFFFF"/>

    <!-- 选中节点样式 -->
    <treeitem checked="1" 
              colorText="#FFFFFF" 
              colorBkgnd="#1E90FF"/>

    <!-- 禁用节点样式 -->
    <treeitem disabled="1" 
              colorText="#999999"/>
</style>

常见问题

Q: 数据修改后如何刷新视图?

A: 如果要刷新全部视图,通过调用 adapter->notifyDataSetChanged() 方法刷新;如果只需要刷新指定的项目,可以通过调用 adapter->notifyItemChanged(hItem) 方法;如果要刷新一个节点及子节点,可以通过调用 adapter->notifyBranchChanged(hItem) 方法。

相关控件