[C#]WinForm TreeView CheckBoxes 기능 완벽 사용하기

개요

윈폼 트리뷰는 각 노드에 체크박스를 사용할 수 있는 속성이 있습니다. 하지만 각각의 노드를 체크할 수 있지, 부모 노드를 체크하면 자식 노드도 같이 모두 선택되거나(또는 반대거나) 자식 노드를 모두 체크하면 부모 노드도 체크되는 등의 UX는 직접 구현해야 합니다. 이번 포스팅에서는 해당 부분을 구현하는 코드를 안내합니다.


TreeView의 체크 기능은 반쪽이다.

저는 지금 생산설비의 각 상황과 그 추세, 작업량등을 파악하는 종합 모니터링 프로그램을 만들고 있습니다. 물론 자세한건 설명드리긴 그렇고, 전체적인 UI는 왼쪽 베너에서 A라는 항목을 선택하면 오른쪽 빈 공간에 그와 관련된 여러가지 차트를 그려주는 프로그램이라고 생각하면 쉽습니다.

어느정도 사용이 가능한 상태로 만들었더니 UX측면에서의 요구사항이 들어왔는데, 바로 특정 항목을 선택하면 해당 항목들만 순환하여 보여달라는 것이였죠. 처음부터 베너 구조가 트리뷰로 되어 있었기 때문에 체크표시를 구현하는건 트리뷰의 속성 하나(CheckBoxes)를 true로 설정해주면 되기 때문에 문제가 없었죠.


근데, 개요와 마찬가지로, 이는 어디까지나 트리뷰의 각 노드별로 체크만 만들어주지 일반적으로 아는 부모노드, 자식노드간의 체크여부와 그에대한 동작은 전혀 구현되지 않아 있습니다. WPF는 어떨지 모르겠는데, 차트를 그려야 하니 그냥 다루기 쉬운 WinForm으로 만들고 있었으니 엎지는 못하고, 그냥 이대로 작업해야 해서 일단 만들었습니다. 만들면서 인터넷에 찾아보니 완벽하게 정리된 방법은 없는것 같아 포스팅 해 봅니다.


구현 방법

두가지 경우를 생각해야 합니다.


부모노드를 체크하거나 체크를 풀 때

자, 우선 부모노드의 체크상태에 따라 그에 해당하는 자식노드의 체크상태를 바꿔줘야 합니다. 이는 어렵지 않아요. 간단하게 생각해보면.

1. 현재 체크상태가 바뀐 노드를 가져온다.
2. 해당 노드의 자식 노드를 가져온다.
3. 자식 노드들을 현재 부모노드의 체크상태에 맞춰준다.

라고 할 수 있습니다.


자식노드를 체크하거나 체크를 풀 때

이때도 간단하게 한번 상상해 봅시다. 어떻게 해야 할지.

1. 현재 선택된 노드의 부모 노드를 가져온다.
2. 해당 노드의 자식 노드들의 체크상태를 확인한다.
3. 자식 노드들이 모두 true이면 부모 노드는 true, 하나라도 false라면 부모 노드는 false로 맞춰준다.

이정도만 하면 흔히 아는 트리뷰의 UX를 구현할 수 있습니다.


이 두가지 경우를 한번에 처리해야 한다

위의 두가지 경우를 확인하면 이제 이벤트에 어떻게 등록 할 지를 찾아야 합니다. 단순히 해당 노드가 부모노드일때, 자식노드일때만을 구분해서 처리한다면 레벨이 3 이상일 경우에 레벨 2의 노드의 체크값이 올바르게 처리되지 않게 될테니까요.

사실 이 UX를 구현하는데 있어서 이부분을 처리하는것이 가장 중요한데, Check를 bool 타입으로 한다면 그렇게 어렵지 않습니다. bool? 타입일 경우에는 조금 어려울 것이고 제가 만드는 프로그램은 레벨 2 까지만 표시되도록 했기 때문에 굳이 이부분을 구현할 필요는 없었는데, 완벽하게 구현하기 위해서는 이정도 처리까지는 해야할 테니 대충 bool 타입으로 표시하는 방법으로 하도록 하죠.


코드 구현

선택한 노드의 자식노드들을 처리

우선 처리하는 코드부터 먼저 작성해봅니다. 첫번째로 현재 체크상태를 변경한 노드의 자식노드를 모두 해당 노드의 체크상태로 바꿔줘야 합니다. 신경써야 할 부분은 자식노드는 또다른 자식노드를 가지고 있을 수 있기 때문에 해당 노드가 또다른 자식노드를 가지고 있는지 확인하고 그 노드들도 같은 값으로 바꿔줘야 겠죠.

private void ChildNodeChecking(TreeNode selectNode)
{
    foreach (TreeNode tn in selectNode.Nodes)
    {
        tn.Checked = selectNode.Checked;
        ChildNodeChecking(tn);
    }
    return;
}

현재 선택된 노드를 가져와서 그의 자식 노드들에게 선택된 노드의 상태를 적용시켜 줍니다. 그리고 재귀함수를 통해서 해당 노드를 부모노드라고 가정하고 다시 해당하는 자식 노드들에게 그의 부모노드의 체크값을 적용시켜 줍니다. 그리고 다시 재귀함수를…

이런식으로 재귀함수를 통해 반복하면서 모든 자식노드들에게 체크값을 적용시켜 줍니다.


선택한 노드의 부모노드를 처리

부모노드를 처리하려면 먼저 자식노드들의 체크상태가 어느 하나라도 false라면 부모노드도 false로 처리하면 됩니다. 굳이 자식노드 전체를 볼 필요는 없고 직속(?) 자식노드들만 확인하면 되겠죠. 단, 해당 노드의 부모노드가 또다른 노드의 자식노드라고 한다면 이를 반복해서 처리해야 할 겁니다. 앞의 경우는 노드의 하뒤로 내려간다면 이 경우는 노드의 상위로 올라가면서 처리하는 식이죠.

public void ParentNodeChecking(TreeNode selectNode)
{
    TreeNode t = selectNode.Parent;
    if (t != null)
    {
        t.Checked = true;
        foreach (TreeNode tn in t.Nodes)
        {
            if (!tn.Checked)
            {
                t.Checked = false;
                break;
            }
        }
        ParentNodeChecking(t);
    }
}

현재 선택된 노드의 부모노드를 확인하고 있다면 먼저 해당 노드를 체크한 다음, 해당 노드의 자식노드중 어느 하나라도 false라면 해당 노드를 false로 처리, 그리고 다시 해당 노드의 부모노드를 확인하고…

역시 마찬가지로 재귀함수를 통해 반복하도록 하면서 자식노드들을 검사하고 체크값을 결정해 주도록 하는거죠.


이벤트로 처리되도록

이렇게 코드로 구현을 했으니, 체크를 빼든 하던 해당 동작이 발생하면 이 코드들을 작동시킬 수 있도록 이벤트에 달아야 합니다. TreeView의 AfterCheck 라는 이벤트가 있는데 여기에 등록해줘야 합니다. 근데 단순히 함수를 붙혀주는것으로 끝낸다면 프로그램은 스텍오버플로우가 되면서 뻗어버릴겁니다.

이유는 위의 작업을 통해 트리뷰의 체크값을 바꿀경우 또다시 해당 이벤트가 발생될 것이고 그게 무한반복 될 테니까요. 그러니까 정확히 해당 작업들을 하기 전에 먼저 이벤트 함수를 없애주고, 작업이 끝나게 되면 다시 이벤트를 붙혀 주는 것이 중요합니다.

private void treeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    treeView1.AfterCheck -= treeView1_AfterCheck;
    ChildNodeChecking(e.Node);
    ParentNodeChecking(e.Node);
    treeView1.AfterCheck += treeView1_AfterCheck;
}

이런식으로 말이죠.


확인

잘 작동되는지 제가 작업중인 프로그램에 테스트 노드를 붙혀서 확인해 보았습니다. 잘 되네요.


bool? 타입으로 구현할 경우에는 ParentNodeChecking 함수에서 자식노드의 값을 확인하는 부분을 단순히 false 하나 만나면 끝내는 것이 아니라 각각 노드의 상태를 확인하여 모든게 false라면 false로 모든게 true라면 true로 하나 이상의 false가 나온다면 ? 로 처리해서 부모노드에 적용하면 될 것 같은데, 트리뷰의 체크박스가 bool? 타입을 지원하는지는 잘 모르겠습니다. 여튼 그건 직접 해보시는것 추천.

Minny_

,