スクロールバー

ウィンドウに表示したい情報量が多くなると一枚のウィンドウ上に表示することが難しくなってきます。
スクロールバーを使用すればクライアント領域をスクロールできるようになります。

スクロールバーの表示

ウィンドウにスクロールバーを表示するにはCreateWindow関数の実行時にウィンドウスタイルにWS_VSCROLLWS_HSCROLLフラグを指定します。
WS_VSCROLLは垂直のスクロールバー、WS_HSCROLLは水平のスクロールバーです。


HWND hWnd = CreateWindow(
	szWindowClass, szTitle,
	WS_OVERLAPPEDWINDOW |
	WS_VSCROLL | WS_HSCROLL,	//スクロールバーの表示
	CW_USEDEFAULT, CW_USEDEFAULT,
	CW_USEDEFAULT, CW_USEDEFAULT,
	NULL,
	NULL,
	hInstance,
	NULL
);

両方のスクロールバーを表示したところです。
ちなみにスクロールバーのつまみ(ドラッグ操作可能なところ)はスクロールボックスと言います。
スクロールバーの表示

表示するだけならこれでOKですが、この状態ではスクロールバーは機能しません。
プログラムでスクロールバーの動作を実装する必要があります。
処理は

  • スクロール量をSetScrollInfo関数とSCROLLINFO構造体で設定する
  • スクロールバーを操作するとWM_VSCROLLWM_HSCROLLメッセージが送信される
  • このメッセージ内でScrollWindowEx関数を実行し、実際にクライアント領域のスクロール処理を行う

という流れになります。

文字サイズの取得

スクロール処理実装の前に、一文字当たりの幅と高さを取得しておきます。
これはスクロール量を決定するために使用します。
文字サイズ取得はGetTextMetrics関数を使用します。

ついでにDrawText関数DT_CALCRECTフラグを指定して、一行の横幅も取得しておきます。

今回は文字サイズや文字列を途中で変更したりはしないので、これらはWM_CREATEメッセージ内で実行します。
変更する場合はその都度再計算が必要です。


//このコードは不完全です

#define BUFFERSIZE 64

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static const int LINES = 30;	//行数
	static const WCHAR format[] =
		L"%d行目のテキスト。横になが~~~~~~~~~~~~い";

	static int charWidth;	//一文字の幅
	static int lineHeight;	//一行の高さ
	static int scrollWidthMax;//スクロールする最大幅

	HDC hdc;
	RECT rt;
	TEXTMETRIC tm;			//文字サイズの情報

	WCHAR buf[BUFFERSIZE];

	switch (message)
	{
	case WM_CREATE: //ウィンドウの作成
		//デバイスコンテキストの取得
		hdc = GetDC(hWnd);

		//スクロールする最大幅を取得
		StringCchPrintf(buf, BUFFERSIZE, format, LINES);
		rt = (RECT){ 0 }; //メンバを0で初期化
		DrawText(hdc, buf, -1, &rt, DT_CALCRECT);
		scrollWidthMax= rt.right;

		//文字サイズに関する情報の取得
		GetTextMetrics(hdc, &tm);
		charWidth = tm.tmAveCharWidth;	//一文字の幅
		lineHeight = tm.tmHeight;		//一行の高さ
		//lineHeight = rt.bottom;		//これでも良い

		//デバイスコンテキストの解放
		ReleaseDC(hWnd, hdc);
		break;
//以降省略

今回は横に長~いテキストを複数行出力することにします。

SetScrollInfo関数

スクロールバーの状態はSetScrollInfo関数で設定します。

int SetScrollInfo(
 HWND hwnd,
 int nBar,
 LPCSCROLLINFO lpsi,
 BOOL redraw
);
ウィンドウhwndのスクロールバーnBarをlpsiの状態に設定する。
redrawが真のとき、スクロールバーを再描画する。
戻り値はスクロールボックスの現在の位置。

第一引数hwndはウィンドウハンドルです。
ちなみにスクロールバーはウィンドウにも使用されますが、コントロールとしてのスクロールバーも存在します。
スクロールバーコントロールの設定をする場合はウィンドウハンドルではなくコントロールのハンドルを指定します。

第二引数nBarは設定するスクロールバーの種類を以下の定数のいずれかで指定します。

定数 説明
SB_HORZ 水平スクロールバー
SB_VERT 垂直スクロールバー
SB_CTL スクロールバーコントロール

第三引数lpsiは必要な情報を設定したSCROLLINFO構造体変数のポインタを指定します。
第四引数redrawTRUEに設定すると、関数実行後にスクロールバーを再描画します。

SCROLLINFO構造体

SCROLLINFO構造体は、スクロールバーの状態を格納し、状態の取得/設定に使用します。

typedef struct tagSCROLLINFO {
 UINT cbSize;
 UINT fMask;
 int nMin;
 int nMax;
 UINT nPage;
 int nPos;
 int nTrackPos;
} SCROLLINFO, *LPSCROLLINFO;
スクロールバーの情報を格納する構造体。

cbSizeメンバはこの構造体のサイズです。
つまりsizeof(SCROLLINFO)を指定します。

fMaskメンバは取得/設定したい状態を以下の定数で指定します。
指定しなかったものは無視されます。
(複数指定可)

fMask定数 説明
SIF_ALL すべての状態を取得/設定
(SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS)
SIF_RANGE スクロールの範囲をnMinnMaxに取得/設定
SIF_PAGE スクロールボックスの長さをnPageに取得/設定
SIF_POS スクロールボックスの位置をnPageに取得/設定
SIF_DISABLENOSCROLL スクロールバーが不要な場合(現在のウィンドウサイズで情報が全て表示可能なとき)、スクロールバーを除去せず無効状態にする
(デフォルトでは除去される)
(設定のみ)
SIF_TRACKPOS ユーザーがスクロールバーをドラッグしているときの位置をnTrackPosに取得
(設定はできない)

nMinメンバは最小のスクロール位置です。
nMaxメンバは最大のスクロール位置です。
スクロールバーを動かしたとき、値(nPos)はこの範囲内で変化します。

nPageメンバはページのサイズです。
つまり一度に表示される領域のサイズです。
ページ単位スクロールで移動する量やスクロールボックスのサイズがこの値により決定されます。

nPosメンバは現在のスクロール位置(=スクロールボックスの位置)です。

nTrackPosはユーザーがスクロールボックスをドラッグしているとき、その位置が格納されます。

この構造体が管理するスクロールの値はただの整数値で、何かに関連付けられているわけではありません。
(ピクセル単位などの決まりはない)
この値をどのように扱うかはプログラマの自由です。
今回はクライアント領域のサイズ情報をそのまま使用することにします。


//このコードは不完全です

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//行数
	const int LINES = 30;

	static int scrollWidthMax;//スクロールする最大幅
	static int lineHeight;	//一行の高さ

	SCROLLINFO si; //スクロールバーの情報

	//途中省略

	switch (message)
	{
	case WM_SIZE: //ウィンドウサイズの変更
		//垂直スクロールバーの設定
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_RANGE | SIF_PAGE;
		si.nMin = 0;
		si.nMax = LINES * lineHeight;
		si.nPage = HIWORD(lParam);	//クライアント領域の高さ
		SetScrollInfo(hWnd, SB_VERT, &si, TRUE);

		//水平スクロールバーの設定
		//si.cbSize = sizeof(SCROLLINFO);
		//si.fMask = SIF_RANGE | SIF_PAGE;
		//si.nMin = 0;
		si.nMax = scrollWidthMax;
		si.nPage = LOWORD(lParam);	//クライアント領域の幅
		SetScrollInfo(hWnd, SB_HORZ, &si, TRUE);
		break;
	//以降省略

ウィンドウのサイズが変更されるとスクロールバーの状態も変更されるため、スクロールバーの設定はWM_SIZEメッセージ内で行います。
WM_SIZEメッセージはウィンドウ生成時にも送られてくるため、初期化も同時に可能です。

SCROLLINFO構造体のfMaskメンバにSIF_RANGEフラグとSIF_PAGEフラグを設定します。

SIF_RANGEフラグは、スクロールの範囲をnMinnMaxの間に設定します。
水平スクロールバーでは、nMaxはテキストの最大横幅に設定します。
垂直スクロールバーでは、nMaxは行全体の高さに設定します。

SIF_PAGEフラグは、ページ送り時のスクロール量をnPageに設定します。
nPageは現在のクライアント領域ひとつ分の幅/高さに設定します。
(クライアント領域のサイズはWM_SIZEメッセージのLPARAMから取得できます)
これによりスクロールボックスのサイズも「全体サイズに対する現在表示中のサイズの割合」の大きさに設定されます。
(ただし操作しづらいので一定の大きさ以下にはなりません)

設定を格納したSCROLLINFO構造体変数をSetScrollInfo関数に渡すことでスクロールバーに反映させます。
第二引数にSB_HORZを指定すると水平スクロールバーに、SB_VERTを指定すると垂直スクロールバーに反映します。

GetScrollInfo関数

GetScrollInfo関数はスクロールバーの情報を取得します。

BOOL GetScrollInfo(
 HWND hwnd,
 int nBar,
 LPSCROLLINFO lpsi
);
ウィンドウhwndのスクロールバーnBarの情報をlpsiに格納する。
成功した場合は0以外を、失敗した場合は0を返す。

SetScrollInfo関数の逆の働きをする関数です。
引数nBarに指定する定数もSetScrollInfo関数と同じです。

WM_VSCROLLメッセージ

垂直スクロールバーを操作するとWM_VSCROLLメッセージが通知されます。
ウィンドウプロシージャのWPARAMにはスクロールバーの状態が格納されています。

WPARAMの下位ワード(LOWORDマクロで取得)には、ユーザーがスクロールバーに対して行った操作が送られてきます。
(通知コードという)
これは以下の定数のいずれかです。

定数 説明
SB_TOP 一番上へスクロール
SB_BOTTOM 一番下へスクロール
SB_LINEUP 一行上へスクロール
(上ボタン)
SB_LINEDOWN 一行下へスクロール
(下ボタン)
SB_PAGEUP 一ページ上へスクロール
(スクロールボックスの上側のクリック)
SB_PAGEDOWN 一ページ下へスクロール
(スクロールボックスの下側のクリック)
SB_THUMBTRACK スクロールボックスのドラッグ中
SB_THUMBPOSITION スクロールボックスのドラッグ終了
SB_ENDSCROLL スクロールの終了

WPARAMの上位ワード(HIWORDマクロで取得)には、スクロールバーの現在の位置が格納されています。

LPARAMは使用しません。
ただしスクロールバーはコントロールとして持つこともでき(スクロールバーコントロール)、その場合はコントロールのハンドルが格納されています。

ScrollWindowEx関数

WM_VSCROLLメッセージ内では、ユーザーの操作に対してスクロールバーの状態を更新し、反映させます。
そしてScrollWindowEx関数を実行して、実際にウィンドウをスクロールさせます。

int ScrollWindowEx(
 HWND hWnd,
 int dx,
 int dy,
 const RECT *prcScroll,
 const RECT *prcClip,
 HRGN hrgnUpdate,
 LPRECT prcUpdate,
 UINT flags
);
ウィンドウをスクロールする。

第一引数hWndはスクロールするウィンドウのハンドルです。

第二、第三引数のdxdyはスクロール量です。
垂直スクロールバーの場合はdx(x軸、水平)は常に0で、dy(y軸、垂直)を設定します。
dyにマイナス値を指定すると上にスクロールします。

第四引数prcScrollはスクロールする領域の矩形(RECT構造体)です。
NULLを指定するとクライアント領域全体をスクロールします。

第五引数prcClipはクリッピング領域の矩形(RECT構造体)です。
ここで指定された領域内のみスクロールされます。
この設定はprcScrollよりも優先されます。
必要ない場合はNULLを指定します。

第六引数hrgnUpdateと第七引数prcUpdateは、スクロールによって無効になった領域を格納する変数(リージョンハンドル)の指定です。
必要がない場合はNULLを指定します。

第八引数flagsはスクロールを制御するためのフラグです。
以下の定数を指定します。
(複数指定可)

定数 説明
SW_ERASE スクロールにより無効になった領域を消去する。
SW_INVALIDATEと同時指定する。
SW_INVALIDATE スクロールにより発生する無効領域を無効化する。
SW_SCROLLCHILDREN スクロール領域内にある子ウィンドウをスクロールし、WM_MOVEメッセージを送る。
SW_SMOOTHSCROLL スムーススクロール。
flagsの上位ワードでスクロール時間(ミリ秒)を設定する。

この関数の戻り値は無効化された領域の情報です。
これは以下の定数です。

定数 説明
ERROR エラー
NULLREGION 無効化された領域はない
SIMPLEREGION 単一の矩形
COMPLEXREGION 単一の矩形より複雑な形

スクロールにはScrollWindow関数を使用することもできますが、これは互換性のためだけに残されているもので、新しいアプリケーションには使用しないようにマイクロソフトが勧告しています。

説明が長くなりましたが、処理自体はそれほど複雑なものではありません。


//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static int lineHeight;	//一行の高さ	
	
	SCROLLINFO si;	//スクロールバーの情報
	int scrollPosY;	//垂直スクロールの位置

	switch (message)
	{
	case WM_VSCROLL: //垂直スクロールの操作
		//スクロールバーの状態の取得
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_ALL;
		GetScrollInfo(hWnd, SB_VERT, &si);

		//スクロールの位置を保存
		scrollPosY = si.nPos;

		//通知コードにより処理を分岐
		switch (LOWORD(wParam)) {
		case SB_TOP:
			si.nPos = si.nMin;
			break;
		case SB_BOTTOM:
			si.nPos = si.nMax;
			break;
		case SB_LINEUP:
			si.nPos -= lineHeight;	//一行上へ
			break;
		case SB_LINEDOWN:
			si.nPos += lineHeight;	//一行下へ
			break;
		case SB_PAGEUP:
			si.nPos -= si.nPage;	//一ページ上へ
			break;
		case SB_PAGEDOWN:
			si.nPos += si.nPage;	//一ページ下へ
			break;
		case SB_THUMBTRACK:
			si.nPos = HIWORD(wParam);
			break;
		case SB_THUMBPOSITION:
			si.nPos = HIWORD(wParam);
			break;
		}

		//いったんスクロールバーに設定を反映してから
		//スクロール位置を再取得
		//Windowsにより位置が調整されて値が変わることがある
		si.fMask = SIF_POS;
		SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
		GetScrollInfo(hWnd, SB_VERT, &si);

		//先ほど保存しておいた位置と値が異なるならスクロール実行
		if (si.nPos != scrollPosY)
		{
			//「以前の位置 - 新しい位置」でスクロール量を算出
			ScrollWindowEx(hWnd, 0, (scrollPosY - si.nPos),
				NULL, NULL, NULL, NULL, SW_INVALIDATE | SW_ERASE);

			//ウィンドウを更新
			UpdateWindow(hWnd);
		}
		break;
	//以降省略

まず現在のスクロール位置を変数に保存しておきます。
次にWPARAMの下位ワードに送られてくる通知コードを調べ、ユーザーの操作に応じてスクロール位置の増減を行います。
最後に先ほど保存しておいたスクロール位置から値が変更された場合に、ScrollWindowEx関数を実行してスクロール処理を行います。

nPosの値を増加させるとスクロールボックスの位置は下に移動しますが、その場合クライアント領域は上にスクロールしなければなりません。
つまり上から下に文書を読み進める場合はnPosの値は増加させますが、ScrollWindowEx関数に指定するスクロール量の値はマイナス値を指定しなければなりません。
感覚的に逆なので注意してください。

UpdateWindow関数

最後のUpdateWindow関数はウィンドウにWM_PAINTメッセージを送信します。

BOOL UpdateWindow(
 HWND hWnd
);
ウィンドウhWndに更新領域(無効領域)がある場合にWM_PAINTメッセージを送信する。
成功した場合は0以外を、失敗した場合は0を返す。

この関数は実行しなくても、スクロールにより無効領域が発生するのでWM_PAINTメッセージは送信されます。
しかしWM_PAINTメッセージは送信しても直ちに実行されるとは限らず、処理の優先順位は最も低く設定されています。
つまり他のメッセージの処理待ちがある間は画面は更新されません。
これは画面のちらつきの原因となることがあります。

UpdateWindow関数はメッセージキューではなくウィンドウプロシージャに即座にWM_PAINTメッセージを送信します。
そのため即座に画面は再描画され、ちらつきを抑えることができます。
ただし無効領域が無い状態ではWM_PAINTメッセージは送信されないので、この関数を実行すれば無条件に画面が更新されるというわけではありません。

WM_HSCROLLメッセージ

WM_HSCROLLメッセージは水平スクロールバーの操作時に通知されます。
ここでの処理はWM_VSCROLLメッセージとほぼ同じです。

WPARAMの下位ワードに送られてくる通知コードは以下です。

定数 説明
SB_LEFT 一番左へスクロール
SB_RIGHT 一番右へスクロール
SB_LINELEFT 一列左へスクロール
(左ボタン)
SB_LINERIGHT 一列右へスクロール
(右ボタン)
SB_PAGELEFT 一ページ左へスクロール
(スクロールボックスの左側のクリック)
SB_PAGERIGHT 一ページ右へスクロール
(スクロールボックスの右側のクリック)
SB_THUMBTRACK スクロールボックスのドラッグ中
SB_THUMBPOSITION スクロールボックスのドラッグ終了
SB_ENDSCROLL スクロールの終了

定数名が違うだけでWM_VSCROLLメッセージのものと役割は同じです。
実は定数が示す実際の値も同じなので、例えば「SB_LEFT」は「SB_TOP」でも代用できます。
(あまりお勧めしませんが)

垂直スクロール同じように水平スクロールの位置を増減させ、ScrollWindowEx関数で実際にスクロールさせます。


//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static int charWidth;	//一文字の幅
	
	SCROLLINFO si;	//スクロールバーの情報
	int scrollPosX;	//水平スクロールの位置

	switch (message)
	{
	case WM_HSCROLL: //水平スクロールの操作
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_ALL;
		GetScrollInfo(hWnd, SB_HORZ, &si);

		scrollPosX = si.nPos;
		switch (LOWORD(wParam)) {
		case SB_LEFT:
			si.nPos = si.nMin;
			break;
		case SB_RIGHT:
			si.nPos = si.nMax;
			break;
		case SB_LINELEFT:
			si.nPos -= charWidth;
			break;
		case SB_LINERIGHT:
			si.nPos += charWidth;
			break;
		case SB_PAGELEFT:
			si.nPos -= si.nPage;
			break;
		case SB_PAGERIGHT:
			si.nPos += si.nPage;
			break;
		case SB_THUMBTRACK:
			si.nPos = HIWORD(wParam);
			break;
		case SB_THUMBPOSITION:
			si.nPos = HIWORD(wParam);
			break;
		}

		si.fMask = SIF_POS;
		SetScrollInfo(hWnd, SB_HORZ, &si, TRUE);
		GetScrollInfo(hWnd, SB_HORZ, &si);

		if (si.nPos != scrollPosX)
		{
			ScrollWindowEx(hWnd, (scrollPosX - si.nPos), 0,
				NULL, NULL, NULL, NULL, SW_INVALIDATE | SW_ERASE);
			UpdateWindow(hWnd);
		}
		break;
	//以降省略

水平スクロールバーの場合、ScrollWindowEx関数で指定するのは第二引数(dx)になります。

WM_PAINTメッセージ

ここまでの処理でスクロール自体はできるようになっています。
最後にWM_PAINTメッセージで画面の描画を行います。


//このコードは不完全です

#define BUFFERSIZE 64

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//行数
	static const int LINES = 30;

	static int lineHeight;	//一行の高さ

	HDC hdc;
	PAINTSTRUCT ps;
	WCHAR buf[BUFFERSIZE];
	WCHAR text[] = L"%d行目のテキスト。横になが~~~~~~~~~~~~い";

	SCROLLINFO si;		//スクロールバーの情報

	int scrollPosX;		//水平スクロールの位置
	int scrollPosY;		//垂直スクロールの位置

	int drawnLineStart;	//描画される行の最初
	int drawnLineEnd;	//描画される行の最後

	int drawnX;			//描画位置のX座標
	int drawnY;			//描画位置のY座標

	//途中省略

	switch (message)
	{
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);

		//垂直スクロールの位置取得
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_POS;
		GetScrollInfo(hWnd, SB_VERT, &si);
		scrollPosY = si.nPos;

		//水平スクロールの位置取得
		GetScrollInfo(hWnd, SB_HORZ, &si);
		scrollPosX = si.nPos;

		//ps.rcPaintには無効領域が格納されている
		//この領域に含まれる行を描画する必要があるので
		//その行番号を取得する
		drawnLineStart = (ps.rcPaint.top + scrollPosY) / lineHeight;
		drawnLineEnd = (ps.rcPaint.bottom + scrollPosY) / lineHeight;
		//LINES行以上は描画しない
		if (drawnLineEnd > LINES - 1)
			drawnLineEnd = LINES - 1;

		for (int i = drawnLineStart; i <= drawnLineEnd; i++)
		{
			drawnX = -scrollPosX;
			drawnY = lineHeight * i - scrollPosY;

			StringCchPrintf(buf, BUFFERSIZE, format, i + 1);
			TextOut(hdc, drawnX, drawnY, buf, lstrlen(buf));
		}

		EndPaint(hWnd, &ps);
		break;
	//以降省略

新しい関数などは登場しませんが、計算がややこしいので注意してください。

クライアント領域全体を描画するのではなく、(スクロールによって発生した)無効領域に重なる行だけを描画します。
(→無効領域)
クライアント領域外は描画する必要がなく、無効領域でもないので処理を省略できます。

描画位置はスクロールしている量だけ上や左にズレます。
例えば「下に20、右に10」のスクロールをしている状態だと、スクロール無しの状態よりも「上に20、左に10」の位置から描画を開始します。
そのため、TextOut関数に指定する座標には負数が指定されることもあります。
スクロール時の描画位置

サンプルコード全体

説明で使用したサンプルコードの全体を示します。


#include <windows.h>
#include <strsafe.h>

#define BUFFERSIZE 64

//ウィンドウの生成等は省略

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static const int LINES = 30;	//行数
	static const WCHAR *format =
		L"%d行目のテキスト。横になが~~~~~~~~~~~~い";

	static int charWidth;		//一文字の幅
	static int lineHeight;		//一行の高さ
	static int scrollWidthMax;	//スクロールする最大幅

	HDC hdc;
	PAINTSTRUCT ps;
	RECT rt;
	SCROLLINFO si;			//スクロールバーの情報
	TEXTMETRIC tm;			//文字サイズの情報

	int scrollPosX;			//水平スクロールの位置
	int scrollPosY;			//垂直スクロールの位置
	int drawnLineStart;		//描画される行の最初
	int drawnLineEnd;		//描画される行の最後
	int drawnX;				//描画位置のX座標
	int drawnY;				//描画位置のY座標

	WCHAR buf[BUFFERSIZE];

	switch (message)
	{
	case WM_CREATE: //ウィンドウの作成
		//デバイスコンテキストの取得
		hdc = GetDC(hWnd);

		//スクロールする最大幅(文字列の幅)を取得
		StringCchPrintf(buf, BUFFERSIZE, format, LINES);
		rt = (RECT){ 0 }; //メンバを0で初期化
		DrawText(hdc, buf, -1, &rt, DT_CALCRECT | DT_EXTERNALLEADING);
		scrollWidthMax = rt.right;

		//文字サイズに関する情報の取得
		GetTextMetrics(hdc, &tm);
		charWidth = tm.tmAveCharWidth;	//文字幅
		lineHeight = tm.tmHeight;		//一行の高さ
		//lineHeight = rt.bottom;		//これでも良い
		
		//デバイスコンテキストの解放
		ReleaseDC(hWnd, hdc);
		break;

	case WM_SIZE: //ウィンドウサイズの変更
		//垂直スクロールバーの設定
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_RANGE | SIF_PAGE;
		si.nMin = 0;
		si.nMax = LINES * lineHeight;	//一行の高さ * 行数
		si.nPage = HIWORD(lParam);		//クライアント領域の高さ
		SetScrollInfo(hWnd, SB_VERT, &si, TRUE);

		//水平スクロールバーの設定
		//si.cbSize = sizeof(SCROLLINFO);
		//si.fMask = SIF_RANGE | SIF_PAGE;
		//si.nMin = 0;
		si.nMax = scrollWidthMax;		//文字列の最大幅
		si.nPage = LOWORD(lParam);		//クライアント領域の幅
		SetScrollInfo(hWnd, SB_HORZ, &si, TRUE);
		break;

	case WM_VSCROLL: //垂直スクロールの操作
		//スクロールバーの状態の取得
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_ALL;
		GetScrollInfo(hWnd, SB_VERT, &si);

		//スクロールの位置を保存
		scrollPosY = si.nPos;

		//通知コードにより処理を分岐
		switch (LOWORD(wParam)) {
		case SB_TOP:
			si.nPos = si.nMin;
			break;
		case SB_BOTTOM:
			si.nPos = si.nMax;
			break;
		case SB_LINEUP:
			si.nPos -= lineHeight;	//一行上へ
			break;
		case SB_LINEDOWN:
			si.nPos += lineHeight;	//一行下へ
			break;
		case SB_PAGEUP:
			si.nPos -= si.nPage;	//一ページ上へ
			break;
		case SB_PAGEDOWN:
			si.nPos += si.nPage;	//一ページ下へ
			break;
		case SB_THUMBTRACK:
			si.nPos = HIWORD(wParam);
			break;
		case SB_THUMBPOSITION:
			si.nPos = HIWORD(wParam);
			break;
		}

		//いったんスクロールバーに設定を反映してから
		//スクロール位置を再取得
		//Windowsにより位置が調整されて値が変わることがある
		si.fMask = SIF_POS;
		SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
		GetScrollInfo(hWnd, SB_VERT, &si);

		//先ほど保存しておいた位置と値が異なるならスクロール実行
		if (si.nPos != scrollPosY)
		{
			//「以前の位置 - 新しい位置」でスクロール量を算出
			ScrollWindowEx(hWnd, 0, (scrollPosY - si.nPos),
				NULL, NULL, NULL, NULL, SW_INVALIDATE | SW_ERASE);
			//ウィンドウを更新
			UpdateWindow(hWnd);
		}
		break;

	case WM_HSCROLL: //水平スクロールの操作
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_ALL;
		GetScrollInfo(hWnd, SB_HORZ, &si);

		scrollPosX = si.nPos;
		switch (LOWORD(wParam)) {
		case SB_LEFT:
			si.nPos = si.nMin;
			break;
		case SB_RIGHT:
			si.nPos = si.nMax;
			break;
		case SB_LINELEFT:
			si.nPos -= charWidth;
			break;
		case SB_LINERIGHT:
			si.nPos += charWidth;
			break;
		case SB_PAGELEFT:
			si.nPos -= si.nPage;
			break;
		case SB_PAGERIGHT:
			si.nPos += si.nPage;
			break;
		case SB_THUMBTRACK:
			si.nPos = HIWORD(wParam);
			break;
		case SB_THUMBPOSITION:
			si.nPos = HIWORD(wParam);
			break;
		}

		si.fMask = SIF_POS;
		SetScrollInfo(hWnd, SB_HORZ, &si, TRUE);
		GetScrollInfo(hWnd, SB_HORZ, &si);

		if (si.nPos != scrollPosX)
		{
			ScrollWindowEx(hWnd, (scrollPosX - si.nPos), 0,
				NULL, NULL, NULL, NULL, SW_INVALIDATE | SW_ERASE);
			UpdateWindow(hWnd);
		}
		break;

	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);

		//垂直スクロールの位置取得
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask = SIF_POS;
		GetScrollInfo(hWnd, SB_VERT, &si);
		scrollPosY = si.nPos;

		//水平スクロールの位置取得
		GetScrollInfo(hWnd, SB_HORZ, &si);
		scrollPosX = si.nPos;

		//ps.rcPaintには無効領域が格納されている
		//この領域に含まれる行を描画する必要があるので
		//その行番号を取得する
		drawnLineStart = (ps.rcPaint.top + scrollPosY) / lineHeight;
		drawnLineEnd = (ps.rcPaint.bottom + scrollPosY) / lineHeight;
		if (drawnLineEnd > LINES - 1)
			drawnLineEnd = LINES - 1;

		for (int i = drawnLineStart; i <= drawnLineEnd; i++)
		{
			drawnX = -scrollPosX;
			drawnY = lineHeight * i - scrollPosY;

			StringCchPrintf(buf, BUFFERSIZE, format, i + 1);
			TextOut(hdc, drawnX, drawnY, buf, lstrlen(buf));
		}

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

実行結果です。
スクロールバーを操作することで全てのテキストを読むことが出来ます。
スクロールバーのサンプルコードの実行結果