第三話 早くもクラス作成、取りあえず画像表示コントロールを作ってみる

そもそもクラス使う必要ないんでね?な人のための説得工作

そんではWin32で作ったゲームとEXCELとメモ帳とデフラグとDOS窓の共通項を探してみましょう。

答え書いちゃうと、Windowアプリであり、ハンドルデバイスコンテキストを持っているという事なんですな。(.netだとその辺り少し違うんだが、それはそれ)となると、構築する手順も同じような物なので、ゴリゴリせずにそれを共通化して差分だけで何とかしたくなるのが人情という物。それがクラス化という事ですな。(MFCではWindowコントロールの粗方がCWndクラス派生で作られている)

講座でやたらとWindowクラスという単語が出ましたが、このWindowクラス(CWndクラス)、ハンドルとデバコン持っているのは当然として、描画やらサイズ変更やら、メッセージ通信やらメッセージドリブンやらとやたらと多機能に出来ています。もちろんWindowメッセージは粗方網羅。また派生して差分を作る事も可能なので、構築手続きはMFCに丸投げして描画だけ美味しいので自分でやるという事が出来る訳です。(クラスのメリットには共通インタフェースでメモリを投げると自動的に解析するコンテナ作れたりとか、 一連の処理をクラス構築~破棄で閉じれるというのもあるけど、一番は共通化だと思う)

ではさっそく派生クラスを作ってみましょう。(この程度のアプリではゼロからクラス作る必要がないのよ)

CWnd派生で画像を表示するコントロールクラスを作ってみる

まずはクラスを作ります、といってもゼロから作るのも派生で作るのも手順は大して変わりません。[プロジェクト]→[クラスの追加]クラスの追加ダイアログを出して下さい。
001
C++とC++クラスを選択し、追加ボタンをクリックして下さい。汎用C++クラスウィザードが出るので、クラス名CGDIStatic基本クラス名CWndを入力して完了ボタンをクリックして下さい。ちなみに基本クラス名を未入力で作ると、基本クラス無しのクラスも作れます。
002

これでクラスは完成しました。煮て食うなり焼いて食うなりができるCWndと同等のクラスが出来た訳です、これを派生と言います。MFCフォルダ漁ってCWndのソースコピってファイル名とクラス名変えてカスタマイズなんてダサイ事しなくても良いんですね(*1)。そもそもそれじゃ派生にならんし。

(*1):クラスを理解してなかったVC++2の頃やらかした。

次にファイル読み込みをクラスに実装します。広義で言うとクラスにアクセスするための独自のメンバメソッドを組み込む訳ですね。こうする事で処理をクラス内に閉じる事が出来て処理のスパゲティ化や悪名高いグローバル変数を避けられます。

クラスビューから作成したクラスを選択し、右クリックで[追加]→[関数の追加]をクリックして下さい。
003
メンバ関数追加ウィザードが起動するので、戻り値をvoid、関数名loadPictureと入力し、パラメータの型にCStringW、パラメータ名strFileNameを入力して追加ボタンをクリック後、完了ボタンをクリックして下さい。
該当CPPファイルにメソッドが出来れば成功です。
004
void CGDIStatic::loadPicture(CStringW strFileName)
{
}

それでは読み込み処理を実装しましょう。こんな感じになります。読み込んでコピーしてBitmapクラスに転送するのが処理の全てとなります。
loadPicture()はメソッド内全部です

#include "StdAfx.h"
#include "Chromakey.h"
#include "GDIStatic.h"

CGDIStatic::CGDIStatic(void)
{
	m_dMagni = 0;
	m_image = NULL;

}

CGDIStatic::~CGDIStatic(void)
{
	m_image = NULL;
}

void CGDIStatic::loadPicture(CStringW strFileName)
{

	BOOL bRet = FALSE;

	// 画像ファイルを取得(ファイルロックがかかる)
	Image* imageOrgn = Bitmap::FromFile(strFileName);
	Status st = imageOrgn->GetLastStatus();
	if(st == Ok){
		CSize szImage(imageOrgn->GetWidth(), imageOrgn->GetHeight());

		// 指定サイズのBitmapを作成
		delete m_image;
		m_image = NULL;
		m_image = new Bitmap(szImage.cx, szImage.cy);

		// 仮想グラフィックス領域作成
		Graphics* grpTmp = Graphics::FromImage(m_image);

		// imageOrgnをimageにプリントする
		grpTmp->DrawImage( imageOrgn, 0, 0, szImage.cx, szImage.cy);

		// 仮想グラフィックス破棄
		delete grpTmp;
		grpTmp = NULL;

		// imageOrgnを開放(ファイル開放)
		delete imageOrgn;
		imageOrgn = NULL;

		// 自身のピクセルサイズ取得
		CRect rt;
		GetClientRect(rt);

		// 比率計算
		double dWid1 = (double)m_image->GetWidth();
		double dWid2 = (double)rt.Width();
		double dMagniW = dWid1 / dWid2;

		double dHgt1 = (double)m_image->GetHeight();
		double dHgt2 = (double)rt.Height();
		double dMagniH = dHgt1 / dHgt2;

		// 大きいほうを使用
		m_dMagni = max(dMagniW, dMagniH);

	} else {
		// imageOrgnを開放(ファイル開放)
		delete imageOrgn;
		imageOrgn = NULL;

		// 旧画像を解放
		delete m_image;
		m_image = NULL;

	}

	// 再描画
	RedrawWindow();

}

ヘッダに画像格納クラスであるBitmapクラスと倍率格納変数も実装しましょう。

#pragma once
#include "afxwin.h"

class CGDIStatic :
public CWnd
{
public:
	CGDIStatic(void);
	~CGDIStatic(void);
	void loadPicture(CStringW strFileName);

protected:
	Bitmap* m_image;

	double m_dMagni; // 1倍(オブジェクト全体描画)時の比率

};

これでファイルの読み込みが出来るようになりました。GDIのヘッダ解析してHBITMAP展開して、と比べるとだいぶ簡単ですね。ちなみにnew演算子がC++のメモリ確保、delete演算子がメモリ解放。VC++では一部の例外除いてmallocfreeは使用しないほうが無難です。(一部malloc等で確保したメモリでないとアクセスできないAPIがあるため)

GDI+の解説はまた次回という事で。
では次に描画処理を実装しましょう、CWndのフレームワークには描画メッセージメソッドがあるのでそれをオーバーライドして使います。この辺り結構C++です、理屈を言い始めると落ちこぼれる人多数です。
クラスビュークラス名を選択して、クラスプロパティメッセージをクリックして下さい、Win32で見慣れたメッセージがずらずら出てくるのでコンボタブをクリックしてOnPaint()を追加して下さい。
005
void CGDIStatic::OnPaint()
{
CPaintDC dc(this); // device context for painting
	// TODO: ここにメッセージ ハンドラ コードを追加します。
	// 描画メッセージで CWnd::OnPaint() を呼び出さないでください。
}

追加されましたね?これでCWndの定型描画処理だったOnPaint()がユーザの自由に出来ます。これがオーバーライドで同じ要領でオーバーライドメソッドWindowProc()を呼び出すなんて事も出来たりします。 では中身を実装。

void CGDIStatic::OnPaint()
{
	CPaintDC dc(this); // device context for painting
	// TODO: ここにメッセージ ハンドラ コードを追加します。
	// 描画メッセージで CWnd::OnPaint() を呼び出さないでください。

	CRect rt;
	GetClientRect(rt);

	if(!m_image){
		dc.FillSolidRect(rt, ::GetSysColor(COLOR_3DFACE));

	} else {

		// DCをGDI+にする
		Graphics* graphics = Graphics::FromHDC(dc.m_hDC);

		// 描画サイズ
		Rect rtOut;

		rtOut.X = 0;
		rtOut.Y = 0;
		rtOut.Width = (INT)((double)m_image->GetWidth() / m_dMagni);
		rtOut.Height = (INT)((double)m_image->GetHeight() / m_dMagni);


		// オブジェクト全体のImageを描画していない矩形を取得
		CRect rtcOut(0,0, rtOut.Width, rtOut.Height);
		CRect rtFill;
		rtFill.SubtractRect(rt, rtcOut);


		// 背景塗りつぶし
		dc.FillSolidRect(rtFill, ::GetSysColor(COLOR_APPWORKSPACE));

		// DCにImageを転送
		graphics->DrawImage( m_image,
				rtOut,
				0, 0,
				m_image->GetWidth(),
				m_image->GetHeight(),
				UnitPixel);

	}

}

これで描画までは仕込みました。

作ったクラスを使おう

最後に、作ったクラスをダイアログクラスに実装しましょう。リソースからダイアログを表示し、IDC_STATIC1を右クリックし変数の追加を選択しましょう。(MFCクラスでクラス作成すると手間が一つ減るかもだが未確認)
002

アクセスProtected変数名m_GDIStatic1として完了を押してください。ダイアログクラスヘッダファイルに変数が追加されますのでこれを書き換えます。

// ChromakeyDlg.h : ヘッダー ファイル
//

#pragma once
#include "afxwin.h"
#include "GDIStatic.h"

// CChromakeyDlg ダイアログ
class CChromakeyDlg : public CDialog
{
// コンストラクション
public:
	CChromakeyDlg(CWnd* pParent = NULL); // 標準コンストラクタ

	// ダイアログ データ
	enum { IDD = IDD_CHROMAKEY_DIALOG };

protected:
	virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV サポート


	// 実装
protected:
	HICON m_hIcon;

// 生成された、メッセージ割り当て関数
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	afx_msg void OnBnClickedButtonload();
	DECLARE_MESSAGE_MAP()

protected:
	CGDIStatic m_GDIStatic1;
};
実行してファイルを選択するとダイアログがこうなるはずです。(当然画像はそれぞれのものになります)
007

おそらくここまででもキー叩く量はWin32でダイアログ作るよりも少ないはずです。
それでは今回はここまで。