[VC++ & IT] [English]

VC++によるコントロールの描画

VC++には標準で自作コントロールを作成する機能も実装されていますが、この機能の代わりに、描画等をカスタマイズすれば、多少手間はかかりますが、より精密で自由度の高いコントロールを作ることができます。ここにおいて、識別できるコントロールの数は32個までです。プロジェクトはダイアログベースをもとにして、コントロールとして発色ボタン、スピンコントロール機能を付加させたスライダを扱います。マウスがボタンの上に移動したときのボタンの発色と、スライダ、スピンボタンによる、値、あるいは値の範囲の取得ができるようになります。

ダイアログイメージ
ダイアログのイメージ(Button2にマウスカーソルが移動した状態)

準備として以下のビットマップを用意し、ビットマップリソースとしてインポートしておきます。ダイアログの左上隅にピクチャーボックスをおき、ベース(ダミー)を、そこへ貼り付けておきます。

リソースID ビットマップ クライアント座標(原点左上)
(ベースから見た座標と同じ)
IDB_BASE_D IDB_BASE_D ベース(ダミー) (0, 0)
IDB_BASE IDB_BASE ベース (0, 0)
IDB_SPINUP IDB_SPINUP (41, 20)
IDB_SPINUPM IDB_SPINUPM 発色用 (41, 20)
IDB_SPINDOWN IDB_SPINDOWN (189, 20)
IDB_SPINDOWNM IDB_SPINDOWNM 発色用 (189, 20)
IDB_MIN IDB_MIN (25, 20)
IDB_MINM IDB_MINM 発色用 (25, 20)
IDB_MAX IDB_MAX (203, 20)
IDB_MAXM IDB_MAXM 発色用 (203, 20)
IDB_SLIDERBTN IDB_SLIDERBTN (55, 20) 開始値
IDB_SLIDERBTNM IDB_SLIDERBTNM 発色用 (55, 20) 開始値
IDB_SELECTBARBK IDB_SELECTBARBK (55, 20)
IDB_SELECTBAR IDB_SELECTBAR (56, 20)
IDB_BUTTON1 IDB_BUTTON1 (25, 54)
IDB_BUTTON1M IDB_BUTTON1M 発色用 (25, 54)
IDB_BUTTON2 IDB_BUTTON2 (175, 54)
IDB_BUTTON2M IDB_BUTTON2M 発色用 (175, 54)

コントロールの識別のために適当な場所に以下のようなマクロを定義します。グローバルな定数型、const unsinged int型として宣言しても問題ありません。UINT型変数のビット演算で、コントロールの識別をするため、コントロールの数は最大で32となります。

#define B_SPINUP        (1 << 0)
#define B_SPINDOWN      (1 << 1)
#define B_MIN           (1 << 2)
#define B_MAX           (1 << 3)
#define B_SLIDERBTN     (1 << 4)
#define B_BUTTON1       (1 << 5)
#define B_BUTTON2       (1 << 6)

また、スライダの状態(ドラッグ、ドロップ)を識別するためのマクロも定義します。

#define DS_BTNDOWN 1
#define DS_DROPPED 2

ヘッダーファイルには、コントロールの領域を確保するためのクラスメンバとしてCRect型を宣言します。また、クラスウィザードを使い、マウスイベントに対応した関数として、OnMouseMove, OnLButtonDown, OnLButtonUp, OnLButtonDblClkを追加します。LButtonDownはOnLButtonUp, OnLButtonDblClkから呼び出される関数です。また、マウス移動時の描画を行うDispOnMouseCtrl関数を定義します。

class CMainDlg : public CDialog
{
public:
	CMainDlg(CWnd* pParent = NULL);

	//{{AFX_VIRTUAL(CMainDlg)
	protected:
	virtual void DoDataExchange(CDataExchange* pDX);
	//}}AFX_VIRTUAL

protected:
	HICON m_hIcon;

	//{{AFX_MSG(CMainDlg)
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
	afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
	afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point);
	afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
	afx_msg void OnMouseMove(UINT nFlags, CPoint point);
	
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()

private:
	void OnButton1();
	void OnButton2();
	void OnSpinUp();
	void OnSpinDown();
	void OnMin();
	void OnMax();

	CRect rBase, rSpinUp, rSpinDown, rMin, rMax, rSliderBtn, rSliderBk, rButton1, rButton2;

	BOOL flagBtnMove, flagReleased;
	UINT nLDownButton, nOnMouse;
	UINT nMousePos;

	//nTotalはスライダの終端位置、nSpinPosはスライダボタンの現在位置、nMinは選択範囲の開始位置、nMaxは選択範囲の終了位置を表す
	UINT nTotal, nSpinPos, nMin, nMax;

	void Draw(BOOL flagRedraw = FALSE);
	void LButtonDown(CPoint point);
	void SetSlider(UINT nFlags = NULL, CPoint point = NULL);
	void DispOnMouseCtrl(CPoint point, BOOL flagClear = FALSE);
	void DispBitmap(UINT nIDResource, int x, int y, int w, int h, int x0 = 0, int y0 = 0);
};

以下、インプリメントです。

BOOL CMainDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		CString strAboutMenu;
		strAboutMenu.LoadString(IDS_ABOUTBOX);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	SetIcon(m_hIcon, TRUE);
	SetIcon(m_hIcon, FALSE);
	
	rBase.SetRect(0, 0, 350, 100);
	rSpinUp.SetRect(41, 20, 55, 35);
	rSpinDown.SetRect(189, 20, 203, 35);
	rMin.SetRect(25, 20, 41, 35);
	rMax.SetRect(203, 20, 220, 35);
	rSliderBtn.SetRect(55, 20, 62, 35);
	rSliderBk.SetRect(55, 20, 189, 35);
	rButton1.SetRect(25, 54, 175, 80);
	rButton2.SetRect(175, 54, 326, 80);

	nTotal = nMax = 100;
	nSpinPos = nMin = 1;

	flagBtnMove = FALSE;

	nLDownButton = 0;
	nOnMouse = 0;
	
	return TRUE;
}

void CMainDlg::OnPaint() 
{
	if (IsIconic())
	{
		CPaintDC dc(this);

		SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);

		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		dc.DrawIcon(x, y, m_hIcon);
	}
	else
		CDialog::OnPaint();

	UpdateData(TRUE);//エディットコントロールなど全角入力に対応
	Draw(TRUE);
}

HBRUSH CMainDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) //ベースの背景色
{
	HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

	if (nCtlColor == CTLCOLOR_DLG)
		return CreateSolidBrush(RGB(224, 223, 227));
	else
		return hbr;
}

void CMainDlg::Draw(BOOL flagRedraw)
{
	CWnd *h = GetDlgItem(IDC_BASE);//ピクチャーボックスのコントロールIDはIDC_BASE
	CDC *pDC = h->GetDC(), mDC, mbDC;
	CBitmap *pOldb, *pbOldb, b, mb, bMask;

	mDC.CreateCompatibleDC(pDC);
	b.CreateCompatibleBitmap(pDC, rBase.Width(), rBase.Height());
	pOldb = mDC.SelectObject(&b);
	mbDC.CreateCompatibleDC(&mDC);

	mb.LoadBitmap(IDB_BASE);
	pbOldb = mbDC.SelectObject(&mb);
	mDC.BitBlt(rBase.left, rBase.top, rBase.Width(), rBase.Height(), &mbDC, 0, 0 , SRCCOPY);
	mbDC.SelectObject(pbOldb);
	mb.DeleteObject();

	//スライダの表示
	if (!flagRedraw && !flagBtnMove)
		SetSlider();

	int nLeft, nRight;
	if (nTotal == 1)
	{
		nLeft = 0;
		nRight = rSliderBk.Width() + 1 - 2;
	}
	else
	{
		nLeft = (int) ((float) (nMin - 1) / (nTotal - 1) * (rSliderBk.Width() + 1 - 3)) + 1;
		nRight = (int) ((float) (nMax - 1) / (nTotal - 1) * (rSliderBk.Width() + 1 - 3)) + 1;

		if (nMin == 1)
			nLeft = 0;
		if (nMax == 1)
			nRight = 0;
		
		if (nLeft == nRight)
		{
			if (nMax != nTotal)
				nRight++;
			else
				nLeft--;
		}
	}

	mb.LoadBitmap(IDB_SELECTBARBK);
	pbOldb = mbDC.SelectObject(&mb);
	mDC.BitBlt(rSliderBk.left, rSliderBk.top, rSliderBk.Width() + 1, rSliderBk.Height(), &mbDC, 0, 0 , SRCCOPY);
	mbDC.SelectObject(pbOldb);
	mb.DeleteObject();

	mb.LoadBitmap(IDB_SELECTBAR);
	pbOldb = mbDC.SelectObject(&mb);
	mDC.BitBlt(rSliderBk.left + 1 + nLeft, rSliderBk.top + 1, nRight - nLeft, rSliderBk.Height() - 2, &mbDC, nLeft, 0, SRCCOPY);
	mbDC.SelectObject(pbOldb);
	mb.DeleteObject();
	
	mb.LoadBitmap(nLDownButton == B_SLIDERBTN ? IDB_SLIDERBTNM : IDB_SLIDERBTN);
	pbOldb = mbDC.SelectObject(&mb);
	mDC.BitBlt(rSliderBtn.left, rSliderBtn.top, rSliderBtn.Width(), rSliderBtn.Height(), &mbDC, 0, 0, SRCCOPY);
	mbDC.SelectObject(pbOldb);
	mb.DeleteObject();

	pDC->BitBlt(rBase.left, rBase.top, rBase.Width(), rBase.Height(), &mDC, 0, 0, SRCCOPY);

	mDC.SelectObject(pOldb);
	b.DeleteObject();
	DeleteDC(mbDC);
	DeleteDC(mDC);
	h->ReleaseDC(pDC);

	nOnMouse = 0;
	CPoint point;
	::GetCursorPos(&point);
	GetDlgItem(IDC_BASE)->ScreenToClient(&point);
	DispOnMouseCtrl(point);
}

void CMainDlg::OnSpinUp() 
{
	nSpinPos = max(1, nSpinPos - 1);

	Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない
}

void CMainDlg::OnSpinDown() 
{	
	nSpinPos = min(nTotal, nSpinPos + 1);

	Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない
}

void CMainDlg::OnMin() 
{
	nMin = nSpinPos;
	if (nMin > nMax && nTotal != 0)
		nMax = nTotal;
	
	Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない
}

void CMainDlg::OnMax() 
{
	nMax = nSpinPos;
	if (nMin > nMax && nTotal != 0)
		nMin = 1;

	Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない
}

void CMainDlg::OnButton1()
{
	//ユーザ定義関数
}

void CMainDlg::OnButton2()
{
	//ユーザ定義関数
}

void CMainDlg::OnLButtonDown(UINT nFlags, CPoint point) 
{
	CPoint newpoint = point;
	this->ClientToScreen(&newpoint);
	GetDlgItem(IDC_BASE)->ScreenToClient(&newpoint);

	LButtonDown(newpoint);
	
	CDialog::OnLButtonDown(nFlags, point);
}

void CMainDlg::OnLButtonDblClk(UINT nFlags, CPoint point) 
{
	CPoint newpoint = point;
	this->ClientToScreen(&newpoint);
	GetDlgItem(IDC_BASE)->ScreenToClient(&newpoint);

	LButtonDown(newpoint);
	
	CDialog::OnLButtonDblClk(nFlags, point);
}

void CMainDlg::LButtonDown(CPoint point)
{	
	SetForegroundWindow();

	if (rSliderBk.PtInRect(point))
	{
		if (rSliderBtn.PtInRect(point))
			nLDownButton = B_SLIDERBTN;
		SetSlider(DS_BTNDOWN, point);

		Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない。ここに、スライダと同期する関数をおく。また、この関数が、描画を含む関数である場合、if文前にUINT npPos = nSpinPosを定義して、SetSlider呼出し後、条件、nSpinPos != npPosとしてスライダのみを描画する関数をおくと描画がより滑らかになる
	}
	else if (rSpinUp.PtInRect(point))
		OnSpinUp();//左ボタンを押したとき、呼び出される
	else if (rSpinDown.PtInRect(point))
		OnSpinDown();
	else if (rMin.PtInRect(point))
		nLDownButton = B_MIN;
	else if (rMax.PtInRect(point))
		nLDownButton = B_MAX;
	else if (rButton1.PtInRect(point))
		nLDownButton = B_BUTTON1;
	else if (rButton2.PtInRect(point))
		nLDownButton = B_BUTTON2;

	SetCapture();
}

void CMainDlg::OnLButtonUp(UINT nFlags, CPoint point) 
{
	CPoint newpoint = point;
	this->ClientToScreen(&newpoint);
	GetDlgItem(IDC_BASE)->ScreenToClient(&newpoint);

	if (flagBtnMove && nLDownButton == B_SLIDERBTN)
	{
		SetSlider(DS_DROPPED, newpoint);

		Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない。ここに、スライダと同期する関数をおく
	}
	else if (rMin.PtInRect(newpoint) && nLDownButton == B_MIN)
		OnMin();//左ボタンを離したとき、呼び出される
	else if (rMax.PtInRect(newpoint) && nLDownButton == B_MAX)
		OnMax();
	else if (rButton1.PtInRect(newpoint) && nLDownButton == B_BUTTON1)
		OnButton1();
	else if (rButton2.PtInRect(newpoint) && nLDownButton == B_BUTTON2)
		OnButton2();

	nLDownButton = 0;

	ReleaseCapture();
	
	DispOnMouseCtrl(newpoint);//ウィンドウの外側で左ボタンを離した場合に対応する
	
	CDialog::OnLButtonUp(nFlags, point);
}

void CMainDlg::OnMouseMove(UINT nFlags, CPoint point) 
{
	CPoint newpoint = point;
	this->ClientToScreen(&newpoint);
	GetDlgItem(IDC_BASE)->ScreenToClient(&newpoint);

	if (flagBtnMove)
	{
		SetSlider(NULL, newpoint);
		Draw();//使用したいユーザ関数内にDrawがあれば、呼び出す必要はない。ここに、スライダと同期する関数をおく
	}
	else
		DispOnMouseCtrl(newpoint);
	
	CDialog::OnMouseMove(nFlags, point);
}

void CMainDlg::SetSlider(UINT nFlags, CPoint point)
{
	int pos;
	if (nFlags & DS_BTNDOWN)
	{
		if (rSliderBtn.PtInRect(point))
		{
			flagBtnMove = TRUE;
			nMousePos = point.x - rSliderBtn.left;//nMousePos:ボタンビットマップから見たマウスカーソルの相対座標
		}
		else
		{
			if (point.x < rSliderBtn.left)
				nSpinPos = nSpinPos <= 10 ? 1 : nSpinPos - 10;
			else
				nSpinPos = nTotal == 0 ? 1 : min(nSpinPos + 10, nTotal);
		}

		return;
	}
	else if (flagBtnMove)
	{
		pos = point.x - nMousePos - rSliderBk.left;
		pos = max(0, pos);

		//ドロップ後の値の変化を避けるための処理
		if (!(nFlags & DS_DROPPED))
		{
			nSpinPos = (UINT) ((float) pos / (rSliderBk.Width() + 1 - 7) * nTotal) + 1;
			nSpinPos = nTotal == 0 ? 1 : min(nTotal, nSpinPos);
		}
		else
			flagBtnMove = FALSE;
	}

	if (nTotal == 0 || nTotal == 1)
		rSliderBtn.right = (rSliderBtn.left = rSliderBk.left) + 7;
	else if (flagBtnMove)
	{
		pos = point.x - nMousePos;
		pos = max(pos, rSliderBk.left);
		pos = min(rSliderBk.right + 1 - 7, pos);
		rSliderBtn.left = pos;
		rSliderBtn.right = min(rSliderBk.right, rSliderBtn.left + 7);//スクロールボタンの一番左のピクセルをrSliderBkに合わせる
	}
	else
	{
		pos = (int) ((float) (nSpinPos - 1) / (nTotal - 1) * (rSliderBk.Width() + 1 - 8)) + 1;
		pos = min(rSliderBk.Width() + 1 - 7, pos);
		if (nSpinPos == 1)
			pos = 0;
		rSliderBtn.left = rSliderBk.left + pos;
		rSliderBtn.right = min(rSliderBk.right, rSliderBtn.left + 7);
	}
}

void CMainDlg::DispOnMouseCtrl(CPoint point, BOOL flagClear)
{
	if (flagBtnMove)
		return;

	UINT nOn = 0;
	if (!flagClear)
	{	
		if (nLDownButton == 0 && rSpinUp.PtInRect(point))
		{
			if (!(nOnMouse & B_SPINUP))
			{
				DispBitmap(IDB_SPINUPM, rSpinUp.left, rSpinUp.top, rSpinUp.Width() + 1, rSpinUp.Height());
				nOnMouse |= B_SPINUP;
			}
			nOn = B_SPINUP;
		}
		else if (nLDownButton == 0 && rSpinDown.PtInRect(point))
		{
			if (!(nOnMouse & B_SPINDOWN))
			{
				DispBitmap(IDB_SPINDOWNM, rSpinDown.left, rSpinDown.top, rSpinDown.Width() + 1, rSpinDown.Height());
				nOnMouse |= B_SPINDOWN;
			}
			nOn = B_SPINDOWN;
		}
		else if (nLDownButton == B_MIN || (nLDownButton == 0 && rMin.PtInRect(point)))
		{
			if (!(nOnMouse & B_MIN))
			{
				DispBitmap(IDB_MINM, rMin.left, rMin.top, rMin.Width() + 1, rMin.Height());
				nOnMouse |= B_MIN;
			}
			nOn = B_MIN;
		}
		else if (nLDownButton == B_MAX || (nLDownButton == 0 && rMax.PtInRect(point)))
		{
			if (!(nOnMouse & B_MAX))
			{
				DispBitmap(IDB_MAXM, rMax.left, rMax.top, rMax.Width() + 1, rMax.Height());
				nOnMouse |= B_MAX;
			}
			nOn = B_MAX;
		}
		else if (nLDownButton == B_SLIDERBTN || (nLDownButton == 0 && rSliderBtn.PtInRect(point)))
		{
			if (!(nOnMouse & B_SLIDERBTN))
			{
				DispBitmap(IDB_SLIDERBTNM, rSliderBtn.left, rSliderBtn.top, rSliderBtn.Width(), rSliderBtn.Height());
				nOnMouse |= B_SLIDERBTN;
			}
			nOn = B_SLIDERBTN;
		}
		else if (nLDownButton == B_BUTTON1 || (nLDownButton == 0 && rButton1.PtInRect(point)))
		{
			if (!(nOnMouse & B_BUTTON1))
			{
				DispBitmap(IDB_BUTTON1M, rButton1.left, rButton1.top, rButton1.Width() + 1, rButton1.Height());
				nOnMouse |= B_BUTTON1;
			}
			nOn = B_BUTTON1;
		}
		else if (nLDownButton == B_BUTTON2 || (nLDownButton == 0 && rButton2.PtInRect(point)))
		{
			if (!(nOnMouse & B_BUTTON2))
			{
				DispBitmap(IDB_BUTTON2M, rButton2.left, rButton2.top, rButton2.Width(), rButton2.Height());
				nOnMouse |= B_BUTTON2;
			}
			nOn = B_BUTTON2;
		}
	}	

	UINT nOff;
	if (flagClear)
		nOff = nOnMouse;
	else
		nOff = nOnMouse == nOn ? 0 : nOnMouse & ~nOn;
	switch (nOff)
	{
	case B_SPINUP:

		DispBitmap(IDB_SPINUP, rSpinUp.left, rSpinUp.top, rSpinUp.Width() + 1, rSpinUp.Height());
		nOnMouse &= ~B_SPINUP;

		break;

	case B_SPINDOWN:

		DispBitmap(IDB_SPINDOWN, rSpinDown.left, rSpinDown.top, rSpinDown.Width(), rSpinDown.Height());
		nOnMouse &= ~B_SPINDOWN;

		break;

	case B_MIN:

		DispBitmap(IDB_MIN, rMin.left, rMin.top, rMin.Width() + 1, rMin.Height());
		nOnMouse &= ~B_MIN;

		break;

	case B_MAX:

		DispBitmap(IDB_MAX, rMax.left, rMax.top, rMax.Width(), rMax.Height());
		nOnMouse &= ~B_MAX;

		break;

	case B_SLIDERBTN:

		DispBitmap(IDB_SLIDERBTN, rSliderBtn.left, rSliderBtn.top, rSliderBtn.Width(), rSliderBtn.Height());
		nOnMouse &= ~B_SLIDERBTN;

		break;

	case B_BUTTON1:

		DispBitmap(IDB_BUTTON1, rButton1.left, rButton1.top, rButton1.Width(), rButton1.Height());
		nOnMouse &= ~B_BUTTON1;

		break;

	case B_BUTTON2:

		DispBitmap(IDB_BUTTON2, rButton2.left, rButton2.top, rButton2.Width(), rButton2.Height());
		nOnMouse &= ~B_BUTTON2;

		break;

	}

	if (nOnMouse)
	{
		SetCapture();
		flagReleased = FALSE;
	}
	else if (!flagReleased)
	{
		ReleaseCapture();
		flagReleased = TRUE;
	}
}

void CMainDlg::DispBitmap(UINT nIDResource, int x, int y, int w, int h, int x0, int y0)
{
	CWnd *hPic = GetDlgItem(IDC_BASE);
	CDC *pDC = hPic->GetDC(), mDC;
	CBitmap *pOldb, b;

	mDC.CreateCompatibleDC(pDC);
	
	b.LoadBitmap(nIDResource);
	pOldb = mDC.SelectObject(&b);
	pDC->BitBlt(x, y, w, h, &mDC, x0, y0, SRCCOPY);
	mDC.SelectObject(pOldb);
	b.DeleteObject();
	
	mDC.SelectObject(pOldb);
	DeleteDC(mDC);
	hPic->ReleaseDC(pDC);
}

同様にすれば、チェックボックス、スクロールバー、プログレスバーなども作れます。また、ビットマップダイアログにエディットボックスを配置することもできますが、多少、手を加える必要があります。新しいダイアログボックスを表示する場合の再描画正常化(CSpinPosEdit::RedrawWindowを使う)など、があります。この様に、素朴なコーディングによっても、低級でありながらも自由度の高いコントロールの描画が可能になります。

Since 2006.11.2