概要

タブコントロールは、下図のようにタブにより画面(ページ)を切り替えて使用するものです。

これらの表示は、メインダイアログにタブと連動する子ダイアログを貼り付けてそのうち1つのダイアログのみ表示されるというトリッキーな実装が標準的です。
本ページでは、タブを動的に作成・削除・順番の変更・名称変更等について説明します。
同じタブのページを追加削除等を行うため、区別ができるようにエディットボックスのテキストをページごとに変更しています。
全ページ同じダイアログプロシャージャーを使用しているので、タブの管理のためにウィンドウハンドルを管理する動的配列hPages(STLのvector)を使用しています。
エディットボックスには、足利幕府の歴代将軍を表示しています。

使い方

タブ上で左クリック

クリックした場所のタブを選択し表示する。

タブ上で右クリック

クリックした場所のタブを選択し表示しポップアップメニューを表示する。

挿入

クリックしたタブの左側に新たなタブを挿入します。挿入後は新たなタブが表示状態になります。

名称変更

クリックしたタブ上にエディットボックスが表示され名称変更が可能となります。

他のタブをクリックするとエディットボックスが終了します。

削除

クリックしたタブを削除します。

順番変更

クリックしたタブと右側のタブの順番を入れ替えます。

タブコントロールで使用できるマクロ等

TabCtrl_InsertItem

タブの挿入。指定番号のタブがすでに存在する場合はそのタブの左側にタブが挿入される。

TabCtrl_GetCurSel

選択されているタブ番号の取得

TabCtrl_HitTest

タブコントロールのクライアント座標からタブ番号を取得。タブが存在しない場合は-1を返す。

TabCtrl_GetItemRect

指定されたタブ番号のタブの左上と右下の座標を取得。取得された座標はタブコントロールのクライアント座標である。

TabCtrl_DeleteItem

指定されたタブ番号のタブを削除

TabCtrl_GetItem

指定されたタブ番号のタブの情報を取得

TabCtrl_SetItem

指定されたタブ番号のタブの情報を設定

TabCtrl_GetItemCount

タブの個数を取得

テスト環境

コンパイラ

Visual C++ 2008 Standard 32/64bit
Visual C++ 2013 Express 32/64bit

プロジェクトの作成

Win32プロジェクト Windowsアプリケーション

実行環境

Windows 8.1 Enterprise 64bit
Windows 7 EnterPrise Service Pack 1 64bit
Windows Vista Ultimate Service Pack 2 32bit
Windows XP Professional Service Pack 3 32bit

プログラムソースの概要

_tWinMain関数

Windowsから最初に_tWinMain関数が呼び出されます。 DialogBox APIよりメインダイアログボックスを作成します。

DlgProc1ダイアログボックスのプロシージャー

メインダイアログボックスのプロシージャーです。

case WM_INITDIALOG:

ダイアログボックスの初期化時に呼び出されます。
GetDlgItem APIによりタブコントロールのウィンドウハンドルを取得します。
取得したウィンドウハンドルは後で頻繁に使用するのでスタティック変数に保存しておきます。
タブは初期状態で3個作成するのでforステートメントを使用しています。
タブ名は最初から何個目に作成されたを示す数字としています。1個目は0としています。
TabCtrl_InsertItemマクロによりタブを追加します。
CreateDialogParam APIにより子ダイアログを作成します。
ダイアログプロシージャーへ渡す引数は作成されたタブの個数を与えます。
子ダイアログのウィンドウハンドルはvectorのpush_backを使用して::hPages配列に追加します。
作成した子ダイアログはとりあえずShowWindow APIにより全部非表示にしておきます。
子ダイアログの最初の1個目をShowWindows APIにより表示します。
子ダイアログの最初の1個目を選択状態にするためにTabCtrl_SetCurSelマクロを使用します。
子ダイアログの大きさをタブコントロールに合わせるためGetClientRect APIによりメインダイアログボックスのクライアントサイズを取得し、SendMessage APIによりWM_SIZEメッセージを作成します。

case WM_SIZE:

ダイアログボックスの大きさが変更されたときに呼び出されます。
ダイアログボックスの大きさは、ダイアログボックスのスタイルで変更できないようにしていますが、前項のとおり、子ウィンドウの大きさを調整するために、WM_SIZEメッセージの処理を行います。
リソース側でうまく大きさを調整しても環境が変わるとウィンドウ枠の大きさが変わったりするのでプログラムで処理するのが常道です。
通常は、メインダイアログのクライアント領域一杯にタブコントロールは配置されますが、今回は小さくしています。
通常であれば、タブコントロールのクライアント領域の大きさを取得し、TabCtrl_AdjustRectマクロにより子ウィンドウの大きさを算出し、::hPages配列からウィンドウハンドルを取り出しMoveWindow APIにより大きさを変更します。
今回は、タブコントロールのメインウィンドウ上でのクライアント領域の大きさを求めるためにGetClientRect APIを使用しますが、このAPIは左上座標が0,0で右下座標がウィンドウサイズとなります。
今回のケースでは、タブコントロールの左上のクライアント座標が0,0ではないので、メインダイアログの左上座標(0,0)とタブコントロール上での左上座標(0,0)をスクリーン座標に変換し、その差分をメインダイアログボックス上でのタブコントロールのクライアント領域の左上座標としています。
あとは、通常通りTabCtrl_AdjustRectマクロにより子ウィンドウの大きさを算出し差分を加算しています。

case WM_NOTIFY:

case TCN_SELCHANGE:
タブを左クリックした場合に発生します。
タブのタイトルが編集状態でないか確認(hEdit変数が0の場合は非編集状態)し非編集状態の場合、 TabCtrl_GetCurSelマクロによりクリックされたタブ番号を取得します。
TabSelect関数により取得されたタブ番号の子ダイアログボックスの表示を有効にします。
タブのタイトルが編集状態の場合、編集中のタブ以外が選択されたみなし、エディットボックスのテキストをGetWindowText APIで取得し、TabCtrl_SetItemマクロによりテキストをタブに設定します。エディットボックスをDestroyWindow APIにより閉じ、タイトルが編集中でないことを示すためにhEditに0を設定します。
case NM_RCLICK:
タブを右クリックした場合に発生します。
GetCursorPos APIによりマウスのカーソル位置の座標を取得します。
取得された座標はスクリーン座標なので、ScreenToClient APIによりタブコントロールに対するクライアント座標に変換します。
TabCtrl_HitTest マクロによりクライアント座標から該当するタブのページ番号を取得します。-1が返された場合は与えた座標が無効であることを示します。後から選択されたページ番号がわかるようにスタティック変数nIndexに保存しておきます。
TabSelect関数により右クリックしたページを選択状態にし表示を有効にします。
LoadMenu APIによりリソースからメニューを読み込みます。
メニューはresouce.rcで定義されています。
GetSubMenu APIによりドロップダウンメニューを取得します。
TrackPopupMenu APIによりポップアップメニューを表示します。メニュー項目が選択されるとこの関数から制御が戻ります。
DestroyMenu APIによりメニューを終了します。
メニューで選択された個目はWM_COMMANDメッセージで知ることができます。

case WM_COMMAND:

case IDM_INS:
タブを右クリックしポンプアップメニューで挿入を選択するとこのメッセージが発生します。
選択されているページをShowWindow APIにより非表示にする。
TabCtrl_InsertItemマクロにより新しいページを挿入します。新しいページは指定しているページ番号の左側に挿入されます。あとはcase WM_INITDIALOG:で説明したとおりの処理を行います。
最後にTabSelect関数により挿入したページを選択状態にし表示を有効にします。
case IDM_RENAME:
タブを右クリックしポンプアップメニューで名称変更を選択するとこのメッセージが発生します。
選択されているタブのテキストをTabCtrl_GetItemマクロのより取得します。
TabCtrl_GetItemRectマクロにより選択されているタブの矩形の左上と右上の座標を取得します。
取得された座標はタブコントロールに対するクライアント座標になります。
取得された座標をもとにCreateWindow APIによりテキストボックスを作成します。
SetFocus APIによりカーソルをテキストボックスに変更します。
case IDM_DEL:
タブを右クリックしポンプアップメニューで削除を選択するとこのメッセージが発生します。
タブの個数をチェックし1個の場合は削除をやめます。
タブを削除する目に削除後に選択状態にするタブを選択状態にしておきます。
選択したタブを削除すると右側のタブが現位置になりますので選択状態にするタブの番号は同じになります。。
最後尾のタブを削除する場合は、左側のタブを削除後の選択状態にするタブにします。
一番左側のタブを削除した場合は、右側のタブが先頭に来ますので、右側のタブを選択状態にしておきます。
TabCtrl_DeleteItemマクロによりタブを削除します。
DestroyWindow APIによりタブに関連している子ダイアログを閉じます。
eraseにより配列に登録されている子ダイアログを削除します。
最後にTabSelect関数により削除後に有効にするページを有効にします。
順番変更
タブを右クリックしポンプアップメニューで順番変更を選択するとこのメッセージが発生します。
選択状態のタブとその右側のタブの順番を入れ替えます。
TabCtrl_GetItemマクロでタブのテキストを取得しTabCtrl_SetItemマクロで隣のタブのテキストを設定します。
あわせて配列の方もウィンドウハンドルを入れ替えます。
最後にTabSelect関数によりページを選択状態にし表示を有効にします。
UpdateWindowによりタブコントロールの表示を更新します。
case IDOK:
OKプッシュボタンのID番号がIDOKです。 OKプッシュボタンをクリックするとこのメッセージが発生します。 GetDlgItemText APIを呼び出し、エディットボックスの文字列を取得します。 MessageBox APIを呼び出し、取得した文字列を表示します。 EndDialog APIを呼び出し、ダイアログボックスを終了させます。 このAPIの第2引数は、ダイアログボックスが終了し、DialogBox APIが返す値となります。
IDM_RENAME:
case IDCANCEL:
キャンセルプッシュボタンのID番号がIDCANCELです。 キャンセルプッシュボタンをクリックするとこのメッセージが発生します。 EndDialog APIを呼び出し、ダイアログボックスを終了させます。 このAPIの第2引数は、ダイアログボックスが終了し、DialogBox APIが返す値となります。

PageProc関数

子ダイアログのプロシージャーです。各タブでプロシージャーを共有しています。

case WM_INITDIALOG:

エディットボックスの初期値を設定しています。
CreateDialogParamで渡された番号を取得しそれをstr配列の要素としてエディットボックスにページごとに異なるユニークなテキストを設定します。

プログラムソース

tabctl2.cpp

//	タブコントロールサンプル(タブの挿入・削除・名称変更)
//	Visual C++ 2013 32/64bit

#include <windows.h>
#include <commctrl.h> 
#include <stdio.h>
#include <tchar.h>
#include <vector>
#include "resource.h"

//	ダイアログボックスプロシージャー
LRESULT CALLBACK DlgProc1(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
//	タブコントロールの0ページ目
LRESULT CALLBACK PageProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);

//	nIndexで示すタブを表示状態にする
void TabSelect(HWND hTab,int nIndex);

using namespace std;

vector<HWND> hPages;

HINSTANCE hInst;

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPreInst,
                   TCHAR* lpszCmdLine, int nCmdShow){
	::hInst = hInstance;
	DialogBox(hInstance, TEXT("TAB_DLG"), 0, (DLGPROC)DlgProc1);
    return (int)0;
}


//	ダイアログボックスプロシージャー

LRESULT CALLBACK DlgProc1(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam){
	static HWND hTab;
	static int max_num = 0;
	static HWND hEdit = 0;
	static int hEditPage = 0;
	static int nIndex = 0;
	TCHAR buf[32];
	DWORD n;
	RECT rc;
	TCITEM tc_item;
    switch (msg) {
        case WM_INITDIALOG:
			hTab = GetDlgItem(hDlg, IDC_TABCNTRL);
			tc_item.mask = TCIF_TEXT;
			for (n = 0; n < 3; n++){
				HWND h = CreateDialogParam(::hInst, _TEXT("DLG1"), hDlg, (DLGPROC)PageProc, (LPARAM)n);
				_stprintf_s(buf, sizeof(buf) / sizeof(TCHAR), _TEXT("%d"), max_num++);
				tc_item.mask = TCIF_TEXT;
				tc_item.pszText = buf;
				TabCtrl_InsertItem(hTab, n, &tc_item);
				::hPages.push_back(h);
				ShowWindow(::hPages[n], SW_HIDE);
			}
			ShowWindow(::hPages[0], SW_SHOW);
			TabCtrl_SetCurSel(hTab, 0);
			GetClientRect(hDlg, &rc);
			SendMessage(hDlg, WM_SIZE, 0, MAKELPARAM(rc.right, rc.bottom));
			return TRUE;
		case WM_SIZE:{
			POINT dlg_pt;
			POINT tab_pt;
			dlg_pt.x = dlg_pt.y = 0;
			ClientToScreen(hDlg, &dlg_pt);
			tab_pt.x = tab_pt.y = 0;
			ClientToScreen(hTab, &tab_pt);

			GetClientRect(hTab, &rc);
			TabCtrl_AdjustRect(hTab, FALSE, &rc);

			int dx = tab_pt.x - dlg_pt.x;
			int dy = tab_pt.y - dlg_pt.y;
			for (n = 0; n < ::hPages.size(); n++)
				MoveWindow(::hPages[n], rc.left + dx, rc.top + dy, rc.right - rc.left, rc.bottom - rc.top, TRUE);
			break;
		}
		case WM_NOTIFY:
			switch (((NMHDR *)lParam)->code){
				case TCN_SELCHANGE:{	//	タブの切り替え
					if (hEdit){	//	タブの文字列編集が有効である。
						GetWindowText(hEdit, buf, sizeof(buf) / sizeof(TCHAR));
						tc_item.mask = TCIF_TEXT;
						tc_item.pszText = buf;
						TabCtrl_SetItem(hTab, hEditPage, &tc_item);
						DestroyWindow(hEdit);
						hEdit=0;
					}
					nIndex=TabCtrl_GetCurSel(hTab);
					TabSelect(hTab,nIndex);
					break;
				}
				case NM_RCLICK:{	//	タブ上で右クリック
					TCHITTESTINFO info;
					POINT p;
					GetCursorPos(&info.pt);
					p = info.pt;
					ScreenToClient(hTab, &info.pt);

					nIndex = TabCtrl_HitTest(hTab, &info);
					if (nIndex != -1){
						TabSelect(hTab,nIndex);
						HMENU hMenu = LoadMenu(::hInst, _TEXT("TAB_POPUP"));
						HMENU hSubmenu = GetSubMenu(hMenu, 0);
						TrackPopupMenu(hSubmenu, TPM_LEFTALIGN, p.x, p.y, 0, hDlg, NULL);
						DestroyMenu(hMenu);
					}
					break;
				}
			}
			return FALSE;
		case WM_COMMAND:
            switch (LOWORD(wParam)) {
				case IDM_INS:{
					ShowWindow(::hPages[nIndex], SW_HIDE);
					_stprintf_s(buf, sizeof(buf) / sizeof(TCHAR), _TEXT("%d"), max_num);
					tc_item.mask = TCIF_TEXT | TCIF_PARAM;
					tc_item.pszText = buf;
					tc_item.lParam = max_num;
					TabCtrl_InsertItem(hTab, nIndex, &tc_item);
					HWND h = CreateDialogParam(::hInst, _TEXT("DLG1"), hDlg, (DLGPROC)PageProc,(LPARAM)max_num);
					++max_num;
					vector<HWND>::iterator start;
					start = ::hPages.begin();
					::hPages.insert(start+nIndex,h);

					GetClientRect(hDlg, &rc);
					SendMessage(hDlg, WM_SIZE, 0, MAKELPARAM(rc.right, rc.bottom));

					TabSelect(hTab,nIndex);
					break;
				}
				case IDM_RENAME:{
					hEditPage = nIndex;
					RECT rect;
					tc_item.mask = TCIF_TEXT;
					tc_item.pszText = buf;
					tc_item.cchTextMax = sizeof(buf) / sizeof(TCHAR);
					TabCtrl_GetItem(hTab, nIndex, &tc_item);

					TabCtrl_GetItemRect(hTab, nIndex, &rect);	//	タブの領域を取得
					POINT p1;
					p1.x = rect.left; p1.y = rect.top;
					int width=rect.right-rect.left;
					int height=rect.bottom-rect.top;
					hEdit = CreateWindow(
						TEXT("EDIT"), buf,
						WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT,
						p1.x, p1.y, width, height, hTab, (HMENU)1,
						hInst, NULL
						);
					SetFocus(hEdit);
					break;
				}
				case IDM_DEL:{
					if(1<::hPages.size()){	//	タブが1個の時は削除しない
						int i;
						if (nIndex){
							if (nIndex==::hPages.size()-1)	//	最後のタブを削除するとき
								i = nIndex-1;
							else
								i = nIndex;
							TabCtrl_SetCurSel(hTab, 0);
						}else{
							i=0;
							TabCtrl_SetCurSel(hTab, 1);
						}
						TabCtrl_DeleteItem(hTab, nIndex);
						DestroyWindow(::hPages[nIndex]);
						vector<HWND>::iterator start;
						start = ::hPages.begin();
						:: hPages.erase(start + nIndex);
						TabSelect(hTab,i);
					}
					break;
				}
 				case IDM_RENUM:{	//	選択されたタブと右のタブとの順番を入れ替え
					if (1 < ::hPages.size()){
						if (unsigned(nIndex) < ::hPages.size() - 1){
							TCHAR src[32], dtc[32];
							tc_item.mask = TCIF_TEXT;
							tc_item.pszText = src;
							tc_item.cchTextMax = sizeof(src) / sizeof(TCHAR);
							TabCtrl_GetItem(hTab, nIndex, &tc_item);
							tc_item.pszText = dtc;
							tc_item.cchTextMax = sizeof(dtc) / sizeof(TCHAR);
							TabCtrl_GetItem(hTab, nIndex + 1, &tc_item);
							tc_item.pszText = dtc;
							tc_item.cchTextMax = sizeof(dtc) / sizeof(TCHAR);
							TabCtrl_SetItem(hTab, nIndex, &tc_item);
							tc_item.pszText = src;
							tc_item.cchTextMax = sizeof(src) / sizeof(TCHAR);
							TabCtrl_SetItem(hTab, nIndex + 1, &tc_item);
							HWND temp = ::hPages[nIndex];
							::hPages[nIndex] = ::hPages[nIndex + 1];
							::hPages[nIndex + 1] = temp;
							TabSelect(hTab, nIndex);
							UpdateWindow(hTab);
						}
					}
					break;
				}
				case IDOK:{
					EndDialog(hDlg, TRUE);
					return TRUE;
				}
				case IDCANCEL:
					EndDialog(hDlg,FALSE);
					return FALSE;
				default:
					return FALSE;
			}
			default:
				return FALSE;
	}
	return TRUE;
}

//	タブコントロールの0ページ目

LRESULT CALLBACK PageProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam){
	int num;
	TCHAR* str[] = {
		_TEXT("00尊氏"),
		_TEXT("01義詮"),
		_TEXT("02義満"),
		_TEXT("03義持"),
		_TEXT("04義量"),
		_TEXT("05義教"),
		_TEXT("06義勝"),
		_TEXT("07義政"),
		_TEXT("08義尚"),
		_TEXT("09義材"),
		_TEXT("10義澄"),
		_TEXT("11義晴"),
		_TEXT("12義輝"),
		_TEXT("13義栄"),
		_TEXT("14義昭"),
		0
	};
	switch (msg) {
	case WM_INITDIALOG:
		num = (int)lParam;
		SetWindowText(GetDlgItem(hDlg, IDC_EDIT1), str[num]);
		return TRUE;
	case WM_COMMAND:
		switch (LOWORD(wParam)) {
		case IDC_EDIT1:
			if (HIWORD(wParam) == EN_UPDATE){	//	エディットボックスが変更された場合
			}
			break;
		default:
			return FALSE;
		}
	default:
		return FALSE;
	}
	return TRUE;
}

//	nIndexで示すタブを表示状態にする

void TabSelect(HWND hTab,int nIndex){
	unsigned n;				
	for (n = 0; n < ::hPages.size(); n++)
		ShowWindow(::hPages[n], nIndex == n ? SW_SHOW : SW_HIDE);
	TabCtrl_SetCurSel(hTab, nIndex);
}

resource.h

#define IDC_TABCNTRL 100

#define IDC_EDIT1	200
#define IDC_LABEL0	210

#define IDM_INS	300
#define IDM_RENAME 310
#define IDM_DEL 320
#define IDM_RENUM 330

resource.rc

#include <windows.h>
#include "resource.h"

TAB_DLG DIALOG DISCARDABLE 0, 0, 205, 185
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_SETFONT
CAPTION "タブコントロール"
FONT 9,  "MS Shell Dlg"
{
	CONTROL "", IDC_TABCNTRL, "SYSTABCONTROL32", WS_CHILD | WS_VISIBLE | TCS_SINGLELINE | TCS_TABS | WS_CLIPSIBLINGS, 7, 7, 191, 120

	CONTROL			"" , IDC_LABEL0  ,"STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY,7,134,191,12

	DEFPUSHBUTTON   "OK", IDOK, 9, 154, 50, 14
	PUSHBUTTON      "キャンセル", IDCANCEL, 70, 154, 50, 14

}

DLG1 DIALOG DISCARDABLE  0, 0, 121, 76
STYLE WS_BORDER | WS_CHILD
FONT 9, "MS Shell Dlg"
{
	EDITTEXT        IDC_EDIT1, 7, 13, 104, 14, ES_AUTOHSCROLL
}

TAB_POPUP MENU DISCARDABLE
BEGIN
POPUP "表示されません"
BEGIN
 MENUITEM "挿入", IDM_INS
 MENUITEM "名称変更", IDM_RENAME
 MENUITEM "削除", IDM_DEL
 MENUITEM "順番変更", IDM_RENUM
 END
END

ソースファイルと実行ファイルのダウンロード

ダウンロード tabctl2.zip(41.0kByte)
ZIPファイルに含まれるファイル
tabctl2.cpp
resource.h
resource.rc
tabctl2.exe