ScrollBar(スクロールバー)の使い方
さて今回はスクロールバーについて学んでいきます。スクロールバーとは、ウィンドウの下とか右にあるアレなんですが、今回はダイアログ上に設置してリアルタイムカラー表示?をして見たいと思います。
1. ダイアログリソースの編集から始めましょう

今回作成するダイアログの概要をちょいとお見せいたしますと、左のようなダイアログになります。 これはダイアログ下部にあるRGBの三つのダイアログを動かすと、上のエディットボックスの背景色が変わるようなダイアログです。コントロールの章で「背景色」に関してのコーナーがありましたが、そのコーナーを見てくだされば、このコーナーは比較的楽に読むことができるかもしれません。
ではいつもの通りですが、ダイアログリソースの編集から初めていきましょうね。
1.AppWizardでプロジェクトを作成する
プロジェクト名をColorにしてください(このときワークスペースは新規に作成するとしておきましょう)。作成するアプリケーションの種類をダイアログにします。あとはディフォルトでいいと思います。
2.ダイアログリソースを開き編集する
配置図は上の図を参考にしてくださいね。閉じるボタンは消してしまっても構いません。
コントロール
 
ID
スクロールバー(一番上:赤色)
ID_SCR_R
スクロールバー(真ん中:緑色)
ID_SCR_G
スクロールバー(一番下:青色)
ID_SCR_B
エディットボックス
ID_EDIT

ではスクロールバーについて少し説明していきましょう。スクロールバーとは、データをある範囲内で自由に変更するためのコントロールです。ここでいうある範囲というのもプログラマー側で指定します。例えば0〜100までの範囲を指定したら、バーの左に行くと0で右側の右に行くと100に近づくように設定されます。したがって、まずスクロールバーがどの範囲の値を動くのかを指定する必要があります。
そのほか重要な点は、スクロールバーのバーが動かされるときには、ウィンドウズからアプリの方にメッセージがやってきます。このメッセージを捕まえてなんらかの処理をすればいいわけですね。またスクロールバーには水平と垂直の二種類があって、スクロールバーとしては同じものなのですが、ウィンドウズから送られてくるメッセージは異なります。今回は水平に動かすスクロールバーになります。と・・・まぁ難しい話は置いておいてどんどん先にすすんでしまいましょう。

2. ダイアログメンバを追加と初期化

 ここではヘッダファイルに色をつけるためのハケに相当するCBrushクラス変数とDWORD(非負整数型)の変数を追加します。DWORD型の変数は、RGBのそれぞれの値を保持するものです。RGB値は、それぞれの色の成分が0〜255の非負整数で表現されるためにintではなくDWORDにしておきました。別にunsigned intでもいいと思いますが、色変数であるCOLORREF構造体は(今回は使いませんが)、各成分の色をDWORD型で保持しているので、私もDWORD型の方で行こうと思います。ではヘッダファイルColorDlg.hを開いて以下のように編集してみましょう。

/////////////////////////////////////////////////////////////////////////////
// CColorDlg ダイアログ

class CColorDlg : public CDialog
{
// 構築
public:
	CBrush*		pBrush;		// 背景色ブラシ(ここを追加)
	DWORD		dwColor[3];	// RGB値(ここを追加)
	CColorDlg(CWnd* pParent = NULL);	// 標準のコンストラクタ
DWORD型の変数が三つの配列になっていますが、1番目がRの成分、2番目がGの成分、3番目がBの成分が入ります。ここまでは本当に簡単ですね。ここでコンストラクタ(ColorDlg.cpp)で変数の初期化をしておきます。
CColorDlg::CColorDlg(CWnd* pParent /*=NULL*/)
	: CDialog(CColorDlg::IDD, pParent)
{
	//{{AFX_DATA_INIT(CColorDlg)
	//}}AFX_DATA_INIT
	// メモ: LoadIcon は Win32 の DestroyIcon のサブシーケンスを要求しません。
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);

	// 初期背景色(白)
	dwColor[0] = 255;
	dwColor[1] = 255;
	dwColor[2] = 255;

	// ブラシのポインタ(NULL)
	pBrush = NULL;
}
ここでは色を白:RGB(255,255,255)にしておき、ブラシのポインタをNULLに設定しておきます。
3. WM_HSCROLLメッセージの処理を行おう

↑なんだか意味のわからない文字列WM_HSCROLLですが、水平スクロールバーが動かされた時にWindowsからアプリケーションの方に送られてくるメッセージのことです。今回はスクロールバーが動かされた時に、スクロールバーの値を読み取り、エディットボックスの背景色を変更しなければならないので、このメッセージを捕まえて実行する関数を作成する必要性があります。
このようにWindowsから送られてくるメッセージを捕まえて実行する関数を指定することを、メッセージマッピングと言います。CColorDlg.cpp内にこんな記述があります。
BEGIN_MESSAGE_MAP(CColorDlg, CDialog)
	//{{AFX_MSG_MAP(CColorDlg)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()
これがメッセージマップの正体です。例えばWM_PAINTというメッセージがWindowsから送られてきた時には、メッセージを捕まえそのメッセージに対応した関数が実行されるようになっています。とはいいつつ、Windowsからのメッセージを対応する関数なんてわかりませんよね。そこでclasswizardでメッセージマップを行います。
1.classwizardを開き、WM_HSCROLLのメッセージマッピングを行う
クラス名:ColorDlg.cpp
オブジェクトID:CColorDlg
メッセージ:WM_HSCROLL
を選択して、関数の追加を押す。するとOnHScroll関数が追加されるはずです。
2.InitDialog関数を編集する
せっかくマッピングしたのですが、先にInitDialog関数を編集しておきます。
BOOL CColorDlg::OnInitDialog()
{
	(中略)
	// TODO: 特別な初期化を行う時はこの場所に追加してください。

	// スクロールバーのポインタ取得
	CScrollBar* pRed = (CScrollBar*)GetDlgItem(IDC_SCR_R);
	CScrollBar* pGreen = (CScrollBar*)GetDlgItem(IDC_SCR_B);
	CScrollBar* pBlue = (CScrollBar*)GetDlgItem(IDC_SCR_G);
	// スクロールバーのレンジ設定
	pRed->SetScrollRange(0,255);
	pGreen->SetScrollRange(0,255);
	pBlue->SetScrollRange(0,255);
	// スクロールバーの初期位置設定
	pRed->SetScrollPos(dwColor[0]);
	pGreen->SetScrollPos(dwColor[1]);
	pBlue->SetScrollPos(dwColor[2]);
	
	(中略)
}
CScrollBar* pRed = (CScrollBar*)GetDlgItem(IDC_SCR_R);
Redのスクロールバー(IDC_SCR_R)のポインタを取得します。以下の操作を行うために必要になります。GetDlgItemは、コントロールのIDからそのコントロールのポインタを取得する関数です。
pRed->SetScrollRange(0,255);
スクロールバーの取りうる値を設定します。この設定では左端が0、右端が255になるように設定します。RGB値は各成分の値が28=256段階で表現されるためにこのレンジを設定します。
pRed->SetScrollPos(dwColor[0]);
コンストラクタで設定したRGB値のRの値をスクロールバーに設定します。ここで言う設定とは、バー自体を希望の位置にセットすることを指しています。コンストラクタでdwColor[0]=255と決めたので、IDC_SCR_Rのスクロールバーは、右端にセットされるはずです。
※この操作をRGB全てに関して行ったのが上のコードです。
2.以下のコードを記述する。
やっと先ほど作成した関数にコードを記述します。
void CColorDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
	// スクロール位置を取得
	int nNewpos = pScrollBar->GetScrollPos();
	
	switch(nSBCode){
	case SB_LINEDOWN:	// 右側ボタンを押された
		nNewpos++; break;
	case SB_LINEUP:	// 左側ボタンを押された
		nNewpos--; break;
	case SB_PAGEDOWN:	// スクロールバーの右側が押された
		nNewpos+=16; break;
	case SB_PAGEUP:	// スクロールバーの左側が押された
		nNewpos-=16; break;
	case SB_THUMBTRACK:	// マウスでつかんで動かした
		nNewpos = nPos; break;
	default:

		return;
	}
	
	// 最大・最少チェック
	if( nNewpos <  0) 		nNewpos = 0;
	else if( nNewpos > 255)	nNewpos = 255;
	else	;
	
	// 現在位置にスクロールバー更新
	pScrollBar->SetScrollPos(nNewpos);
	
	CScrollBar* pRed = (CScrollBar*)GetDlgItem(IDC_SCR_R);
	CScrollBar* pGreen = (CScrollBar*)GetDlgItem(IDC_SCR_G);
	CScrollBar* pBlue = (CScrollBar*)GetDlgItem(IDC_SCR_B);
	dwColor[0] = (DWORD)(pRed->GetScrollPos());
	dwColor[1] = (DWORD)(pGreen->GetScrollPos());
	dwColor[2] = (DWORD)(pBlue->GetScrollPos());	
	
	Invalidate(true);
}
この関数がこの章の一番重要なところなので詳しく解説していきたいと思います。まず関数の引数について説明していきましょう。
nSBCode
スクロール要求の指示です。 どのスクロールがされたが分かります。
SB_LINELEFT 左へスクロール。
SB_LINERIGHT 右へスクロール。
SB_PAGELEFT 1 ページ左へスクロール。
SB_PAGERIGHT 1 ページ右へスクロール。
SB_THUMBTRACK スクロールバーを指定位置へドラッグします。現在位置は引数 nPos で指定されます。
nPos
スクロールバーのコードが SB_THUMBTRACK の場合は、スクロール ボックスの位置を指定します。
pScrollBar
スクロールバーへのポインタを保持します。今回は三つのスクロールバーがありますが、あくまでスクロールされたスクロールバーのポインタのみです。

引数も分かったところで、コードを最初から解説していきます。
int nNewpos = pScrollBar->GetScrollPos();
変更前のスクロールの位置を取得します。
case SB_LINEDOWN:
nNewpos++; break;
右側をクリックされた時、nNewposを増加させます。
case SB_LINEUP:
nNewpos--; break;
左側をクリックされた時、nNewposを減少させます。
case SB_PAGEDOWN:
nNewpos+=16; break;
右側1ページスクロールが指定された時、nNewposを16増加させます。
case SB_PAGEUP:
nNewpos-=16; break;
左側1ページスクロールが指定された時、nNewposを16減少させます。
case SB_THUMBTRACK:
nNewpos = nPos; break;
マウスで位置を指定された時には、その位置nPosを現在位置に設定します。
if( nNewpos < 0) nNewpos = 0;
else if( nNewpos > 255) nNewpos = 255;
else ;
数値の最小と最大の判定をします。つまりnNewposがレンジを超えてはならないので、0〜255の間にあればよいことになります。
pScrollBar->SetScrollPos(nNewpos);
先ほど作成した位置をスクロールバーに反映させます。この操作を行うことで、スクロールされているように見えるようになります。
CScrollBar* pRed = (CScrollBar*)GetDlgItem(IDC_SCR_R);
CScrollBar* pGreen = (CScrollBar*)GetDlgItem(IDC_SCR_G);
CScrollBar* pBlue = (CScrollBar*)GetDlgItem(IDC_SCR_B);
それぞれのスクロールバーのポインタを取得します。以下の操作に使用するためです。
dwColor[0] = (DWORD)(pRed->GetScrollPos());
dwColor[1] = (DWORD)(pGreen->GetScrollPos());
dwColor[2] = (DWORD)(pBlue->GetScrollPos());
現在の位置をスクロールの位置を取得します。位置すなわち色成分データ変数dwColorはDWORD型なので、一応DWORD型でキャストしておいたほうが無難です。
Invalidate(true);
ダイアログを更新します。
この操作を通して理解していただきたいことは、"スクロールバーはただ設置するだけでは動作しない" ということです。まとめれば、スクロールバーがクリックされた時にWindowsからアプリに送られてくるメッセージに対応した関数OnHScrollで以下の作業を必要とします。

1.変更前のスクロールバーの値を取得
2.スクロールバーに適用される操作に基づいて、新しいスクロールバーの位置を計算する
3.新しいスクロールバーの位置を、実際にスクロールバーに反映させる


これをすればスクロールバーがはじめて動作しているように「見えます」。後はInvalidateでダイアログが更新される時に、背景色を変更するようなコードを追加してあげればいいのですね。ではラスト頑張っていきましょう。
4. WM_CTLCOLORメッセージの処理を行おう

最後は、エディットボックスの背景色変更です。これは前にも紹介したとおり、WM_CTLCOLORメッセージが深く関わっているんでしたよね。まずメッセージマッピングを行っていきます。メッセージマッピングの方法は、先ほどのWM_HSCROLLでも紹介しましたが、まったく同じ操作でできます。
WM_CTLCOLORWM_DESTROYのメッセージをマッピングしてください。マッピングできましたか?突然出てきたWM_DESTROYですが、これはダイアログが破棄されたときに送られるメッセージです。ダイアログが破棄=アプリの終了ですから、このWM_DESTROYで定義されたOnDestroy関数では、プログラムの後処理(お掃除)を行うのが適当ですが、これは以下のコードを見てもらえれば即分かりでしょう(笑)。では、マッピングによって追加された関数OnCtlColorとOnDestroyを以下のように編集してください。

HBRUSH CColorDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
	HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

	switch(nCtlColor){
	/////////// エディットボックスorメッセージボックスの場合////////////
	case CTLCOLOR_EDIT:   
	case CTLCOLOR_MSGBOX:
		// ブラシのポインタがNULLでない時メモリを解放
		if(pBrush != NULL){
			delete pBrush;
			pBrush = NULL;
		}
		// 現在の色のブラシを作成
		pBrush = new CBrush(RGB(dwColor[0],dwColor[1],dwColor[2]));
		// ブラシのハンドルを返す
		return (HBRUSH)(pBrush->GetSafeHandle());

	/////////// そのほかのコントロールは何もしない/////////////////////	
	default:
		break;
	}
	return hbr;
}

void CColorDlg::OnDestroy() 
{	
	// メモリを解放
	delete 	pBrush;	
}


このOnCtlColor関数は、全てのコントロールの色を自由に変更することができます。 nCtlColorがどのコントロールの種類かを表しています。今はエディットコントロールの背景色を変えたいので、 CTLCOLOR_EDIT の場合だけをピックアップすればよいのですね。ここで現在の色を使ってブラシを作成します。 このブラシのハンドルをリターンすれば背景色を変更することができます。たったこれだけで背景色が変更できます。

上のコードではちょっと複雑なことをしているのですが、ブラシを作るときにnew演算子を使っているため、メモリをしっかり解放する作業を行わなければヤバイことになるのは分かりますか?つまりスクロールバーをぐりぐりっと動かすと、その分だけInvalidate関数が呼ばれるために、当然OnCtlColorが何回も呼ばれます。したがってnewでブラシを作成する前に、以前作成したブラシのポインタを確認して、メモリを動的確保していたらメモリを解放してあげなければリソースは一瞬でなくなり、即ハングアップ!です。まぁこの方法はあんまりよろしくない方法ですが、直感的でわかりやすい方法だと思います。

最後にOnDestroyでもメモリを解放しているのは、念のためでもあります。
これで、背景色をリアルタイムに変更できるでしょう。

言い忘れていましたが、垂直スクロールバーも全く同じ方法で実装できます。ただメッセージはWM_VSCROLLですから注意してくださいね。

本コーナーのソースファイルは載せませんが、ダウンロードのコーナーに似たようなソフト+ソースがあるのでそれを参考にしていただけたら幸いです!