[DirectX] IMGUI - Content, Tree
in Study on WinAPI&DirectX
Imgui를 이용해서 콘텐츠를 표시하는 UI를 설계해보자.
ContentUI 클래스의 구성
우선 ContentUI의 클래스를 아래와 같이 구성해준다.
// ContentUI.h 파일
#pragma once
#include "UI.h"
class TreeUI;
class ContentUI :
public UI
{
private:
TreeUI* m_Tree;
public:
virtual int render_update() override;
public:
void ResetContent();
public:
ContentUI();
~ContentUI();
};
// ContentUI.cpp 파일
#include "pch.h"
#include "ContentUI.h"
#include <Engine\CResMgr.h>
#include "TreeUI.h"
ContentUI::ContentUI()
: UI("##Content")
{
SetName("ContentUI");
}
ContentUI::~ContentUI()
{
}
int ContentUI::render_update()
{
return 0;
}
void ContentUI::ResetContent()
{
// Tree Clear
m_Tree->Clear();
// 리소스 매니저에서 현재 모든 리소스 목록 받아옴
// m_Tree 에 현재 리소스 목록을 AddItem
}
그리고 ContentUI.cpp에서 컨텐츠를 표시하기 위해서 Imgui의 트리구조(TreeNodeEX)를 활용할 것이다.
if (ImGui::TreeNodeEx("Parent1", flag))
{
if (ImGui::TreeNodeEX("Child", flag))
{
// ...
// 코드 입력
// ...
ImGui::TreePop();
}
// ...
// 코드 입력
// ...
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Parent2", flag))
{
// ...
// 코드 입력
// ...
ImGui::TreePop();
}
트리노드 구조는 위의 구조를 통해서 생성을 할 수 있다.
하지만 트리의 개수는 계속해서 변할 수 있다.
따라서 Tree구조를 만들어주는 클래스를 새로 생성할 것이다.
이는 후술할 것이다.
그리고 ContentUI의 활성화를 위해서 객체를 생성해준다.
// ImGuiMgr.cpp의 CreateUI 함수 중에서
// ContentUI
pUI = new ContentUI;
pUI->SetActive(true);
m_mapUI.insert(make_pair(pUI->GetID(), pUI));
TreeUI 클래스의 구성
// TreeUI.h
#pragma once
#include "UI.h"
class TreeUI :
public UI
{
private:
TreeNode* m_RootNode; // 트리가 소유하고 있는 노드 중 루트 노드
UINT g_NextId; // 생성되는 노드뒤에 붙여줄 고유 숫자
bool m_bShowRoot;
public:
virtual int render_update() override;
public:
void Clear();
TreeNode* AddItem(const string& _strNodeName, DWORD_PTR _Data, TreeNode* _pParent = nullptr);
void ShowRoot(bool _Show) { m_bShowRoot = _Show; }
public:
TreeUI();
~TreeUI();
};
// TreeUI.cpp
#include "pch.h"
#include "TreeUI.h"
TreeUI::TreeUI()
: UI("##Tree")
, m_RootNode(nullptr)
, g_NextId(0)
, m_bShowRoot(true)
{
}
TreeUI::~TreeUI()
{
if (nullptr != m_RootNode)
delete m_RootNode;
}
int TreeUI::render_update()
{
if (nullptr != m_RootNode)
{
if (m_bShowRoot)
{
m_RootNode->render_update();
}
else
{
for (size_t i = 0; i < m_RootNode->m_vecChildNode.size(); ++i)
{
m_RootNode->m_vecChildNode[i]->render_update();
}
}
}
return 0;
}
void TreeUI::Clear()
{
if (nullptr != m_RootNode)
{
delete m_RootNode;
m_RootNode = nullptr;
}
}
TreeNode* TreeUI::AddItem(const string& _strNodeName, DWORD_PTR _Data, TreeNode* _pParent)
{
TreeNode* pNewNode = new TreeNode;
pNewNode->m_Owner = this;
pNewNode->m_strName = _strNodeName;
pNewNode->m_Data = _Data;
pNewNode->m_ID = g_NextId++;
// 루트가 NULL 이다 ==> 트리에 들어온 최초의 데이터
if (nullptr == m_RootNode)
{
// 최초 데이터 입력인데, 부모를 지정한 경우
assert(!_pParent);
m_RootNode = pNewNode;
}
// 트리에 들어온 데이터가 최초가 아니다.
else
{
if (_pParent)
{
// 노드의 부모로 지정된 노드가 해당 트리 소속이 아니다.
if (_pParent->m_Owner != this)
assert(nullptr);
// 지정된 부모의 자식으로 연결
_pParent->m_vecChildNode.push_back(pNewNode);
pNewNode->m_ParentNode = _pParent;
}
// 부모로 지정된 노드가 없는경우, 루트 밑으로 넣는다
else
{
// 새로 생성한 노드를 루트노드의 자식으로 연결
m_RootNode->m_vecChildNode.push_back(pNewNode);
pNewNode->m_ParentNode = m_RootNode;
}
}
return pNewNode;
}
AddItem을 할 때 트리노드의 고유한 ID값과 데이터의 주소값, 그리고 목적지에 해당하는 Parent Node의 주소(_pParent)를 같이 줄 것이다.
Parent Node는 기본적으로 nullptr값을 줄 것이다. 이는 Root노드의 역할을 하도록 한다.
이렇게 하는 이유는 같은 이름을 가진 오브젝트가 있을 수 있고, 또한 선택을 하면 어떤 오브젝트를 클릭했는지를 주소값을 통해서 알 수 있기 때문이다.
그리고 잘 만들어진 노드(TreeNode)에 대한 주소를 반환할 수 있도록 만든다.
주의사항 (assert 지정)
- Tree가 여러개일 경우 원하는 트리가 아니라 다른 트리에 넣고 있지 않은지?
- Root가 지정이 되지 않은, 최초의 데이터 입력인데 부모를 지정한 경우인지?
ShowRoot는 루트 노드를 보여줄지 말지를 결정하는 함수이다.
- 아래와 같은 형식으로 보이고 싶을 수도 있고,
- 루트
- 차일드1
- 차일드2
- 아래와 같이 보이고 싶을 때도 있을 것이다.
- 차일드1
- 차일드2
이때, ShowRoot를 켜면 1번처럼, 끄면 2번처럼 출력되게 만들 수 있다.
TreeNode 클래스
TreeUI는 friend class로 TreeNode 클래스를 가진다.
// 헤더
// =========
// TreeNode
// =========
class TreeUI;
class TreeNode
{
private:
TreeUI* m_Owner; // 노드를 소유하고 있는 트리
TreeNode* m_ParentNode; // 부모노드
vector<TreeNode*> m_vecChildNode; // 노드의 자식 노드
string m_strName; // 노드의 출력 이름
UINT m_ID; // 노드의 고유 ID
DWORD_PTR m_Data; // 노드에 저장된 데이터
private:
void render_update();
public:
TreeNode();
~TreeNode();
friend class TreeUI;
};
// cpp
// ========
// TreeNode
// ========
TreeNode::TreeNode()
: m_Owner(nullptr)
, m_ParentNode(nullptr)
{
}
TreeNode::~TreeNode()
{
Safe_Del_Vec(m_vecChildNode);
}
void TreeNode::render_update()
{
// FinalName 만들기
string strFinalName = m_strName;
strFinalName += "##";
char szBuff[100] = {};
itoa(m_ID, szBuff, 10);
strFinalName += szBuff;
ImGuiTreeNodeFlags_ flag = ImGuiTreeNodeFlags_::ImGuiTreeNodeFlags_None;
if (ImGui::TreeNodeEx(strFinalName.c_str(), flag))
{
for (size_t i = 0; i < m_vecChildNode.size(); ++i)
{
m_vecChildNode[i]->render_update();
}
ImGui::TreePop();
}
}
트리를 구성하는 노드 하나를 TreeNode라는 클래스로 지정하자.
그리고 트리가 알아야하는 정보인 RootNode를 변수로 지정해준다.
루트노드가 render update를 호출하면 자식의 render update를 호출할 수 있도록 설계를 해주는 것이다.
void TreeNode::render_update()
{
// FinalName 만들기
string strFinalName = m_strName;
strFinalName += "##";
char szBuff[100] = {};
itoa(m_ID, szBuff, 10);
strFinalName += szBuff;
ImGuiTreeNodeFlags_ flag = ImGuiTreeNodeFlags_::ImGuiTreeNodeFlags_None;
if (ImGui::TreeNodeEx(strFinalName.c_str(), flag))
{
for (size_t i = 0; i < m_vecChildNode.size(); ++i)
{
m_vecChildNode[i]->render_update();
}
ImGui::TreePop();
}
}
또한, 트리노드가 어떤 트리에 속해있는지를 알 수 있어야 하므로 변수를 설정해준다.
private:
TreeUI* m_Owner; // 노드를 소유하고 있는 트리
그리고 트리노드는 자식 트리노드를 가지고 있으므로 이를 저장하는 변수를 설정해준다.
private:
vector<TreeNode*> m_vecChildNode;
Tree 생성하기
ID는 ##Tree로 넣되, Tree 별 Name은 사용을 해주는 Content에서 설정해주어야 한다.
// ContentUI.cpp 중에서 생성자 함수
TreeUI* pTreeUI = new TreeUI;
pTreeUI->SetName("ContentTree");
pTreeUI->SetActive(true);
AddChildUI(pTreeUI);
// 루트 생성
pTreeUI->AddItem("Root Node", 0);
// 루트 밑에 자식 2개 추가
TreeNode* pChild1Node = pTreeUI->AddItem("Child1", 0);
pTreeUI->AddItem("Child2", 0);
// Child1 밑에 자식 2개 추가
pTreeUI->AddItem("ChildChild1" , 0, pChild1Node);
pTreeUI->AddItem("ChildChild2" , 0, pChild1Node);
Content에 리소스 구성하기
define.h 에서 리소스 타입에 따라서 enum class 값을 지정하였다.
하지만 트리UI에서 사용하려면 이름을 문자열로 바꿔줘야할 것 이다.
define.h와 extern.cpp를 통해 작업을 해주자.
// define.h 중에서
enum class RES_TYPE
{
MESHDATA,
MATERIAL,
PREFAB,
MESH, // 형태
TEXTURE, // 이미지
SOUND,
GRAPHICS_SHADER,
COMPUTE_SHADER,
END,
};
extern const char* RES_TYPE_STR[(UINT)RES_TYPE::END];
extern const wchar_t* RES_TYPE_WSTR[(UINT)RES_TYPE::END];
그리고 실제 문자열의 구현은 extern.cpp에서 해준다.
// extern.cpp 중에서
extern const char* RES_TYPE_STR[(UINT)RES_TYPE::END] =
{
"MESHDATA",
"MATERIAL",
"PREFAB",
"MESH",
"TEXTURE",
"SOUND",
"GRAPHICS_SHADER",
"COMPUTE_SHADER"
};
extern const wchar_t* RES_TYPE_WSTR[(UINT)RES_TYPE::END] =
{
L"MESHDATA",
L"MATERIAL",
L"PREFAB",
L"MESH",
L"TEXTURE",
L"SOUND",
L"GRAPHICS_SHADER",
L"COMPUTE_SHADER"
};
이렇게 하면 해당 enum 값에 해당하는 문자열을 매칭시킬 수 있다.
// func.h 중에서
const char* ToString(RES_TYPE);
const wchar_t* ToWString(RES_TYPE);
// func.cpp 중에서
const char* ToString(RES_TYPE type)
{
return RES_TYPE_STR[type];
}
const wchar_t* ToWString(RES_TYPE type)
{
return RES_TYPE_WSTR[type];
}
그리고 func 파일에 담아서 함수호출을 통해서 문자열로 변환할 수 있도록 만든다.
계속해서, ContentUI.cpp에서 리소스를 받아오는 작업을 해보자.
// ContentUI.cpp 중에서
void ContentUI::ResetContent()
{
// Tree Clear
m_Tree->Clear();
m_Tree->AddItem("Root", 0);
for (size_t i = 0; i < (UINT)RES_TYPE::END; ++i)
{
const map<wstring, Ptr<CRes>>& mapRes = CResMgr::GetInst()->GetResources((RES_TYPE)i);
// m_Tree 에 현재 리소스 목록을 AddItem
TreeNode* pCategory = m_Tree->AddItem(ToString((RES_TYPE)i), 0);
for (const auto& pair : mapRes)
{
m_Tree->AddItem(string(pair.first.begin(), pair.first.end()), (DWORD_PTR)pair.second.Get(), pCategory);
}
}
}
기타 꾸미기 기능
자신이 리프노드일경우 (자식이 없을 경우) 펼치기, 접기 기능을 하는 화살표가 표시되지 않도록 만들어보자.
// TreeUI.cpp 중에서
UINT flag = ImGuiTreeNodeFlags_::ImGuiTreeNodeFlags_None;
if (m_vecChildNode.empty())
{
flag |= ImGuiTreeNodeFlags_Leaf;
}
ImGuiTreeNodeFlags_Leaf 를 flag에 달게 된다면 펼치기, 접기 표시를 하는 화살표가 활성화되지 않는다.
자식노드가 없다는 것은 m_vecChildNode의 길이가 0이거나 비어있다는 것이므로 해당 조건이 맞을 경우 옵션을 추가해준다.
카테고리 노드에만 하이라이트를 넣어보자.
// TreeUI.h 중에서
bool m_CategoryNode; // 항목 대표 노드
bool m_Hilight; // 노드 하이라이트 처리
public:
void SetCategoryNode(bool _category)
{
m_CategoryNode = _category;
}
자신이 카테고리노드인지 여부를 저장하는 멤버변수와 하이라이트를 쳐줘야 하는지 여부를 저장하는 멤버변수 그리고 자신이 카테고리노드인지 여부를 세팅 해주는 함수를 추가한다.
// ContentUI.cpp 중에서
void ContentUI::ResetContent()
{
// Tree Clear
m_Tree->Clear();
m_Tree->AddItem("Root", 0);
for (size_t i = 0; i < (UINT)RES_TYPE::END; ++i)
{
const map<wstring, Ptr<CRes>>& mapRes = CResMgr::GetInst()->GetResources((RES_TYPE)i);
// m_Tree 에 현재 리소스 목록을 AddItem
TreeNode* pCategory = m_Tree->AddItem(ToString((RES_TYPE)i), 0);
pCategory->SetCategoryNode(true);
for (const auto& pair : mapRes)
{
m_Tree->AddItem(string(pair.first.begin(), pair.first.end()), (DWORD_PTR)pair.second.Get(), pCategory);
}
}
}
// TreeUI.cpp 중에서
if (m_Hilight || m_CategoryNode)
flag |= ImGuiTreeNodeFlags_Selected;
여기서, m_Hilight는 자신이 선택되었는지 여부를 나타내는 bool 멤버변수이고,
m_CategoryNode는 자신이 카테고리 노드인지 여부를 나타내는 bool 멤버변수이다.