EternalWindows
ツリービュー / ツリービューの作成

ツリービューとは、選択可能なアイテムを階層で表示するコントロールのことです。

アイテムの横にある印を選択することで、下位に存在するアイテムを表示できるようになります。 アイテムの左横にはアイコンを表示することも可能です。

ツリービューにアイテムを追加するには、TreeView_InsertItemを呼び出します。

HTREEITEM TreeView_InsertItem(
  HWND hwndTV,
  LPTVINSERTSTRUCT lpis
);

hwndTVは、ツリービューのハンドルを指定します。 lpisは、TVINSERTSTRUCT構造体のアドレスを指定します。

TVINSERTSTRUCT構造体は、次のように定義されています。

typedef struct tagTVINSERTSTRUCT {
  HTREEITEM hParent;
  HTREEITEM hInsertAfter;
#if (_WIN32_IE >= 0x0400)
  union
  {
      TVITEMEX itemex;
      TVITEM item;
  } DUMMYUNIONNAME;
#else
  TVITEM item;
#endif
} TVINSERTSTRUCT, *LPTVINSERTSTRUCT;

hParentは、親アイテムのハンドルを指定します。 hInsertAfterは、追加するアイテムの前のアイテムのハンドルを指定します。 itemeは、TVITEMEEX構造体を指定します。

TVITEMEEX構造体は、次のように定義されています。

typedef struct tagTVITEMEX {
  UINT      mask;
  HTREEITEM hItem;
  UINT      state;
  UINT      stateMask;
  LPTSTR    pszText;
  int       cchTextMax;
  int       iImage;
  int       iSelectedImage;
  int       cChildren;
  LPARAM    lParam;
  int       iIntegral;
#if (_WIN32_IE >= 0x0600)
  UINT      uStateEx;
  HWND      hwnd;
  int       iExpandedImage;
#endif
#if (NTDDI_VERSION >= NTDDI_WIN7)
  int       iReserved;
#endif
} TVITEMEX, *LPTVITEMEX;

maskは、初期化するメンバを表す定数を指定します。 hItemは、アイテムのハンドルを指定します。 stateは、アイテムの状態を表す定数を指定します。 stateMaskは、アイテムの状態を表す定数を指定します。 pszTextは、アイテムのテキストを指定します。 cchTextMaxは、pszTextのサイズを指定します。 iImageは、イメージリストにおけるアイコンのインデックスを指定します。 iSelectedImageは、イメージリストにおける選択されたアイコンのインデックスを指定します。 cChildrenは、このアイテムに関連付けられる子アイテムの数を指定します。 lParamは、アイテムに関連付けたいデータを指定します。 iIntegralは、アイテムの高さを指定します。

ツリービューに追加されたアイテムの情報を取得したい場合は、TreeView_GetItemを呼び出します。

BOOL TreeView_GetItem(
 HWND hwndTV,
 LPTVITEMEX pitem
);

hwndTVは、ツリービューのハンドルを指定します。 pitemは、TVITEMEX構造体のアドレスを指定します。

今回のプログラムは、ツリービューを作成して表示します。 アイテムが選択された場合は、アイテムの名前をウインドウタイトルに表示します。

#include <windows.h>
#include <commctrl.h>

#pragma comment (lib, "comctl32.lib")

HTREEITEM InsertTreeItem(HWND hwndTreeView, HTREEITEM hitemParent, LPTSTR lpszName);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndTreeView = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE: {
		HTREEITEM            hitemRoot, hitemParent;
		INITCOMMONCONTROLSEX ic;
		
		ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
		ic.dwICC  = ICC_TREEVIEW_CLASSES;
		InitCommonControlsEx(&ic);
		
		hwndTreeView = CreateWindowEx(0, WC_TREEVIEW, TEXT(""), WS_CHILD | WS_VISIBLE | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		hitemRoot = InsertTreeItem(hwndTreeView, TVI_ROOT, TEXT("A"));
		hitemParent = InsertTreeItem(hwndTreeView, hitemRoot, TEXT("B"));
		InsertTreeItem(hwndTreeView, hitemParent, TEXT("C"));
		InsertTreeItem(hwndTreeView, hitemParent, TEXT("D"));

		return 0;
	}
	
	case WM_NOTIFY:
		if (((LPNMHDR)lParam)->code == TVN_SELCHANGED) {
			TCHAR    szBuf[256];
			TVITEMEX item;

			item.mask       = TVIF_HANDLE | TVIF_TEXT;
			item.hItem      = ((LPNMTREEVIEW)lParam)->itemNew.hItem;
			item.pszText    = szBuf;
			item.cchTextMax = sizeof(szBuf) / sizeof(TCHAR);
			TreeView_GetItem(hwndTreeView, &item);
			
			SetWindowText(hwnd, szBuf);
		}
		return 0;
	
	case WM_SIZE:
		MoveWindow(hwndTreeView, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

HTREEITEM InsertTreeItem(HWND hwndTreeView, HTREEITEM hitemParent, LPTSTR lpszName)
{
	TVINSERTSTRUCT is;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT;
	is.item.pszText = lpszName;
	
	return TreeView_InsertItem(hwndTreeView, &is);
}

ツリービューを作成するには、ICC_TREEVIEW_CLASSESを指定してInitCommonControlsExを呼び出します。 また、ウインドウクラスにWC_TREEVIEWを指定し、ウインドウスタイルにTVS_HASBUTTONS、TVS_HASLINES、TVS_LINESATROOTを指定します。 TVS_HASBUTTONSは展開を示す+-ボタンの表示、TVS_HASLINESはアイテム同士をつなぐ線の表示、 TVS_LINESATROOTはルート同士をつなぐ線の表示になります。 InsertTreeItemは、TreeView_InsertItemの呼び出しをラッピングしたもので次のように実装されています。

HTREEITEM InsertTreeItem(HWND hwndTreeView, HTREEITEM hitemParent, LPTSTR lpszName)
{
	TVINSERTSTRUCT is;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT;
	is.item.pszText = lpszName;
	
	return TreeView_InsertItem(hwndTreeView, &is);
}

hParentは、これから追加するアイテムの親となるアイテムのハンドルを指定します。 item.maskは、初期化するメンバを表す定数を指定します。 ここではTVIF_TEXTを指定しているので、pszTextメンバが考慮されるようになります。 pszTextは、アイテムのテキストを指定します。 TreeView_InsertItemが成功すれば、 hParentの子として新しいアイテムが追加されます。

今回はInsertTreeItemを4回呼び出していますが、 これによりどのようなツリーが形成されるかを確認してみましょう。

hitemRoot = InsertTreeItem(hwndTreeView, TVI_ROOT, TEXT("A"));
hitemParent = InsertTreeItem(hwndTreeView, hitemRoot, TEXT("B"));
InsertTreeItem(hwndTreeView, hitemParent, TEXT("C"));
InsertTreeItem(hwndTreeView, hitemParent, TEXT("D"));

1回目の呼び出しでは、まだ親となるアイテムが作成されていないため、 第2引数にはTVI_ROOTを指定します。 これを指定して作成されたアイテムは、一番上位に存在するルートアイテムになります。 2回目の呼び出しでは、このルートアイテムを第2引数に指定しているため、 作成されるアイテムはルートアイテムの子となります。 つまり、展開ボタンを押さなければ確認することはできません。 3回目の4回目と呼び出しでは、第2引数にhitemParentを指定していることから、 これらのアイテムはhitemParentの子となります。 以上の事から、最上位にAが存在し、その下にBが、そしてBの下にCとDが存在します。

今回は、ツリービューのアイテムを選択した場合にメッセージボックスが表示されますが、 これはWM_NOTIFYの処理によるものです。

case WM_NOTIFY:
	if (((LPNMHDR)lParam)->code == TVN_SELCHANGED) {
		TCHAR    szBuf[256];
		TVITEMEX item;

		item.mask       = TVIF_HANDLE | TVIF_TEXT;
		item.hItem      = ((LPNMTREEVIEW)lParam)->itemNew.hItem;
		item.pszText    = szBuf;
		item.cchTextMax = sizeof(szBuf) / sizeof(TCHAR);
		TreeView_GetItem(hwndTreeView, &item);
		
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	}
	return 0;

ツリービューのアイテムを選択した場合は、通知コードがTVN_SELCHANGEDであるWM_NOTIFYが送られます。 LPARAMにはNMTREEVIEW構造体が格納されており、itemNew.hItemから選択されたアイテムのハンドルを参照することができます。 後はこれをhItemに指定し、hItemを初期化することを示すTVIF_HANDLEをmaskに指定しておけば、 TreeView_GetItemの呼び出しは成功することになります。 この例ではmaskにTVIF_TEXTを指定しているため、pszTextにアイテムのテキストが返ることになります。

アイテムを動的に追加する

階層構造を持つツリービューには多くのアイテムを追加することができるため、 アイテムの数が多い場合などは表示に時間が掛かることが懸念されます。 この問題を回避するためには、最初に親アイテムだけを追加しておき、 親アイテムが展開された段階で子アイテムを追加するようにします。 次に例を示します。

#include <windows.h>
#include <commctrl.h>

#pragma comment (lib, "comctl32.lib")

HTREEITEM InsertTreeItem(HWND hwndTreeView, HTREEITEM hitemParent, LPTSTR lpszName, BOOL bChild);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndTreeView = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE: {
		INITCOMMONCONTROLSEX ic;
		
		ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
		ic.dwICC  = ICC_TREEVIEW_CLASSES;
		InitCommonControlsEx(&ic);
		
		hwndTreeView = CreateWindowEx(0, WC_TREEVIEW, TEXT(""), WS_CHILD | WS_VISIBLE | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		InsertTreeItem(hwndTreeView, TVI_ROOT, TEXT("A"), TRUE);

		return 0;
	}
	
	case WM_NOTIFY:
		if (((LPNMHDR)lParam)->code == TVN_ITEMEXPANDING) {
			LPTVITEM lpItem = &((LPNMTREEVIEW)lParam)->itemNew;
			
			if (!(lpItem->state & TVIS_EXPANDEDONCE)) {
				InsertTreeItem(hwndTreeView, lpItem->hItem, TEXT("B"), FALSE);
				InsertTreeItem(hwndTreeView, lpItem->hItem, TEXT("C"), FALSE);
			}
		}
		return 0;
	
	case WM_SIZE:
		MoveWindow(hwndTreeView, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

HTREEITEM InsertTreeItem(HWND hwndTreeView, HTREEITEM hitemParent, LPTSTR lpszName, BOOL bChild)
{
	TVINSERTSTRUCT is;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT;
	is.item.pszText = lpszName;

	if (bChild) {
		is.item.mask     |= TVIF_CHILDREN;
		is.item.cChildren = 1;
	}
	
	return TreeView_InsertItem(hwndTreeView, &is);
}

ツリービューの構造は、Aを親としてその下にBとCが存在するものとします。 WM_CREATEではAのアイテムのみしか追加していませんが、 これは子アイテムを動的に追加するのが目的であるからです。 ただし、単純にアイテムを追加するだけでは、子を持つことを示す印が表示されませんから、 InsertTreeItemではcChildrenに1を指定するようにします。 これにより、実際に子アイテムを持っていなくても印が表示されるようになります。 cChildrenを初期化する場合は、maskにTVIF_CHILDRENを指定します。

アイテムが展開された場合は、通知コードがTVN_ITEMEXPANDINGであるWM_NOTIFYが送られます。 lParamにはTVITEM構造体のアドレスが格納されており、stateにTVIS_EXPANDEDONCEが格納されていない場合は、 まだ一度も展開が行われていないことを意味します。 よって、このときに子アイテムを追加することになります。 親アイテムのハンドルはhItemから参照できます。



戻る