SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

OpenGLで描画した内容をMFCのコントロール上に描画する

最近、MFCを使うようになったので、ちょっとGLとMFCを組み合わせてみたサンプルの備忘録。
MFCで、3D表示を行うソフトを作ろうとする場合、
GLの初期化など、毎度毎度面倒な処理が多々あるので、サブクラス化をしてまとめました。

MFCでGLを使いたいだけであれば、記事の最後の「再利用方法」の手順を行えばOKです。

結果画面


MFCのピクチャーコントロールを貼り付けて、そこにOpenGLで描画を行っています。

サブクラス化について

サブクラス化という言葉はちょっと紛らわしいので、以下にまとめておきます。

  • C++などの言語使用上での意味
    • あるクラスを継承して派生クラスを作成すること。
  • Win32APIでのサブクラス化
    • ウィンドウや、エディットコントロールなど各種コントロールのインスタンスが持つウィンドウプロシージャを、独自のコールバック関数に入れ替えて動作をカスタマイズすること。

今回のテーマで用いる「サブクラス化」という言葉は、後者のコントロールの特殊化を指します。
ただし、MFCではコントロールのサブクラス化を行う際に、CEditなどMFCのクラスを継承して独自のクラス定義を行うため、前者の意味でのサブクラス化も同時に行うことになります。

今回の目的

MFCで、PictureControlというコントロールが用意されていますが、ここにOpenGLで描画した内容を表示することが目的です。
OpenGLで描画するための準備は、そこそこ面倒ですが、頻繁に使うコードなので、
PictureControlをサブクラス化して、OpenGLで描画が簡単にできるコントロールを作って再利用したい、というのが今回の要旨です。

MFCでのコントロールのサブクラス化の方法

方法は何種類かあるみたいですが、ココでは比較的簡単な以下の手順でサブクラス化を行います。

  1. 元となるコントロールを、リソースエディタで画面上に配置
  2. クラスの追加で、MFCクラスを追加(このとき、↑で選んだコントロールの派生クラスとして作成)
  3. 最初に作ったコントロールの、DDX変数(コントロール変数)を作成
  4. 追加されたDDX変数の型を、追加したクラスの型に変更する。
    1. このとき、追加したクラスの.hファイルもインクルードする。

GL+PictureControl

ということで、PictureControlをサブクラス化して、OpenGLの設定をする手順を以下に書いておきます。
今回のサンプルは、ダイアログベースアプリで説明をします。
(MDI, SDiの場合も、PictureContorolを同様にサブクラス化すればできます。)

ダイアログベースの雛形を作成

まずは、新規プロジェクトの作成で、ウィザードからダイアログベースを選択して、雛形を作成します。

PictureControlの作成

リソースエディタで、ピクチャーコントロールを貼り付け。

IDの変更

DDX変数の作成

DDX変数を作成します。PictureControlは、CStaticの変数として作成することになります。


クラスの追加

サブクラス化を行うために、新しいクラスを作成します。
CStaticを継承したクラスとして作成します。



DDX変数の型変更

先ほど作成したDDX変数の型を、新たに作成したクラスに変更します。

変更前 CStatic m_xcGLPicture;
変更後 CGLPictureCtrl m_xcGLPicture;

ここまでの手順で、PictureControlをサブクラス化した雛形が作成できました。


次に、このサブクラス内に、OpenGLで描画を行うための処理を追記します。

新規クラスのソースに以下のコードを追加

GLPictureCtrl.cppの先頭に以下のコードを追記します。
OpenGLを使うためのヘッダのインクルードとライブラリの設定

// GLPictureCtrl.cpp : 実装ファイル
//

#include "stdafx.h"
#include "MFCGL.h"
#include "GLPictureCtrl.h"

//	OpenGLのヘッダをインクルード
#include <GL/gl.h>
#include <GL/glu.h>

#pragma comment(lib, "glu32.lib")
#pragma comment(lib, "opengl32.lib")
ヘッダに以下のコードを追加
protected:
	HGLRC m_hRC;
	CDC* m_pDC;

	BOOL SetupPixelFormat();
	BOOL InitGLContext();
OpenGLの初期化・破棄の関数作成

まず、OpenGLで描画するためのレンダリングコンテキストの作成・破棄をする関数を用意します。
以下の関数を追加。

BOOL CGLPictureCtrl::SetupPixelFormat()
{
	PIXELFORMATDESCRIPTOR pfd = {
		sizeof(PIXELFORMATDESCRIPTOR),
		1,
		PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
		PFD_TYPE_RGBA,
		24,
		0, 0, 0, 0, 0, 0,
		0,
		0,
		0,
		0,  0, 0, 0,
		16,
		0,
		0,
		PFD_MAIN_PLANE,
		0,
		0, 0, 0
	};

	int pixelformat;
	if (0 == (pixelformat = ::ChoosePixelFormat(m_pDC->GetSafeHdc(), &pfd))) {
		return FALSE;
	}

	if (FALSE == ::SetPixelFormat(m_pDC->GetSafeHdc(), pixelformat, &pfd)) {
		return FALSE;
	}

	return TRUE;
}


BOOL CGLPictureCtrl::InitGLContext()
{
	m_pDC = new CClientDC(this);

	if (NULL == m_pDC) {
		return FALSE;
	}
	if (!SetupPixelFormat()) return FALSE;
	if (0 == (m_hRC = ::wglCreateContext(m_pDC->GetSafeHdc()))) {
		return FALSE;
	}
	if (FALSE == ::wglMakeCurrent(m_pDC->GetSafeHdc(), m_hRC)) {
		return FALSE;
	}

	return TRUE;
}
オーバーライド、イベントの追加

・PreSubclassWindow
今回のサブクラス化の方法では、CGLPictureCtrlのインスタンスはアプリ作成時にはすでに作成されていて、サブクラス化した後ではOnCreateなどのイベントは呼ばれません。
そこで、サブクラス化をしたときに呼び出されるPreSubclassWindowというメソッドで、初期化を行います。
以下の手順で、PreSubclassWindowをオーバーライド。


PreSubclassWindowに以下のコードを追加します。

void CGLPictureCtrl::PreSubclassWindow()
{
	// TODO: ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
	LONG  style = GetWindowLong(this->m_hWnd, GWL_STYLE );
	style |=  WS_CLIPSIBLINGS | WS_CLIPCHILDREN ;
  	SetWindowLong( this->m_hWnd, GWL_STYLE, style );


	InitGLContext();
	CStatic::PreSubclassWindow();
}


・OnDestroy
レンダリングコンテキストの破棄は、OnDestroyで行います。

OnDestroyに以下のコードを記述。

void CGLPictureCtrl::OnDestroy()
{
	CStatic::OnDestroy();

	// TODO: ここにメッセージ ハンドラ コードを追加します。
	if(FALSE == ::wglMakeCurrent(NULL, NULL)) {
		//	必要に応じてエラーハンドリング
	}

	if(FALSE == ::wglDeleteContext(m_hRC)) {
		//	必要に応じてエラーハンドリング
	}

	if(m_pDC) delete m_pDC;
}

・OnPaint
OpenGLでの描画は、OnPaintに記述します。ここでは、サンプルとして四角形の描画だけを行っています。

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

	::glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	//	とりあえず、四角形を描画してみる。
	::glPushMatrix();
	::glColor3f( 0.0f, 1.0f, 0.0f );
	glColor3d(1.0, 0.0, 0.0);
	glBegin(GL_POLYGON);
	glVertex2d(-0.9, -0.9);
	glVertex2d(0.9, -0.9);
	glVertex2d(0.9, 0.9);
	glVertex2d(-0.9, 0.9);
	glEnd();

	::glPopMatrix();

	::glFinish();
	if( FALSE == ::SwapBuffers( m_pDC->GetSafeHdc())){}
}


・OnMove
画面をすばやく動かしたときに再描画が上手くされない場合があったので、以下の処理を追加。
(SDIのフォームビューでこのクラスを動作させた際に、フォームをスクロールさせた場合に再描画が行われないことがあったので・・・)

void CGLPictureCtrl::OnMove(int x, int y)
{
	CStatic::OnMove(x, y);

	// TODO: ここにメッセージ ハンドラ コードを追加します。

	Invalidate(TRUE);
	UpdateWindow();
}
Notifyの設定

ピクチャーコントロールをクリックした際などのイベント通知を受け取りたい場合には、以下のプロパティを編集します。

再利用方法

ここで作成した、CGLPictureCtrlを再利用する際は、以下のような手順でOKです。

  1. ダイアログorフォームにPictureControlを作成。
  2. PictureControlのIDを任意の名前に変更
  3. PictureControlにDDX変数(コントロール変数)を作成
  4. DDX変数が追加されたヘッダーファイルに、CGLPictureCtrlクラスのヘッダーをインクルードする。
  5. 作成したDDX変数の型をCGLPictureCtrlに変更する。
  6. CGLPictureCtrlでインクルードしている、○○.h(MFCで自動生成される、アプリケーションクラスが定義されているヘッダーファイル)を、現在のプロジェクトのアプリケーションクラスのヘッダファイルに変更する。
  7. (必要に応じて)PictureControlのNotifyプロパティをTrueに変更する。

これで、今回作成したものと同様の、OpenGLで描画する機能を持ったピクチャーコントロールが簡単に作成できます。
以下に、サブクラス化したファイルの全体のコードを張っておきます。

GLPictureCtrl.h
#pragma once


// CGLPictureCtrl

class CGLPictureCtrl : public CStatic
{
	DECLARE_DYNAMIC(CGLPictureCtrl)

public:
	CGLPictureCtrl();
	virtual ~CGLPictureCtrl();
	//	★★追加
protected:
	HGLRC m_hRC;
	CDC* m_pDC;

	BOOL SetupPixelFormat();
	BOOL InitGLContext();

protected:
	DECLARE_MESSAGE_MAP()
	virtual void PreSubclassWindow();
public:
	afx_msg void OnDestroy();
	afx_msg void OnPaint();
	afx_msg void OnMove(int x, int y);
};
GLPictureCtrl.cpp
// GLPictureCtrl.cpp : 実装ファイル
//

#include "stdafx.h"
#include "MFCGL.h"
#include "GLPictureCtrl.h"

#include <GL/gl.h>		//	★★追加
#include <GL/glu.h>

#pragma comment(lib, "glu32.lib")
#pragma comment(lib, "opengl32.lib")


// CGLPictureCtrl

IMPLEMENT_DYNAMIC(CGLPictureCtrl, CStatic)

CGLPictureCtrl::CGLPictureCtrl()
{

}

CGLPictureCtrl::~CGLPictureCtrl()
{
}


BEGIN_MESSAGE_MAP(CGLPictureCtrl, CStatic)
	ON_WM_DESTROY()
	ON_WM_PAINT()
	ON_WM_MOVE()
END_MESSAGE_MAP()

BOOL CGLPictureCtrl::SetupPixelFormat()
{
	PIXELFORMATDESCRIPTOR pfd = {
		sizeof(PIXELFORMATDESCRIPTOR),
		1,
		PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
		PFD_TYPE_RGBA,
		24,
		0, 0, 0, 0, 0, 0,
		0,
		0,
		0,
		0,  0, 0, 0,
		16,
		0,
		0,
		PFD_MAIN_PLANE,
		0,
		0, 0, 0
	};

	int pixelformat;
	if (0 == (pixelformat = ::ChoosePixelFormat(m_pDC->GetSafeHdc(), &pfd))) {
		return FALSE;
	}

	if (FALSE == ::SetPixelFormat(m_pDC->GetSafeHdc(), pixelformat, &pfd)) {
		return FALSE;
	}

	return TRUE;
}


BOOL CGLPictureCtrl::InitGLContext()
{
	m_pDC = new CClientDC(this);

	if (NULL == m_pDC) {
		return FALSE;
	}
	if (!SetupPixelFormat()) return FALSE;
	if (0 == (m_hRC = ::wglCreateContext(m_pDC->GetSafeHdc()))) {
		return FALSE;
	}
	if (FALSE == ::wglMakeCurrent(m_pDC->GetSafeHdc(), m_hRC)) {
		return FALSE;
	}

	return TRUE;
}


// CGLPictureCtrl メッセージ ハンドラ



void CGLPictureCtrl::PreSubclassWindow()
{
	// TODO: ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
	LONG  style = GetWindowLong(this->m_hWnd, GWL_STYLE );
	style |=  WS_CLIPSIBLINGS | WS_CLIPCHILDREN ;
  	SetWindowLong( this->m_hWnd, GWL_STYLE, style );


	InitGLContext();
	CStatic::PreSubclassWindow();
}

void CGLPictureCtrl::OnDestroy()
{
	CStatic::OnDestroy();

	// TODO: ここにメッセージ ハンドラ コードを追加します。
	if(FALSE == ::wglMakeCurrent(NULL, NULL)) {
		//	必要に応じてエラーハンドリング
	}

	if(FALSE == ::wglDeleteContext(m_hRC)) {
		//	必要に応じてエラーハンドリング
	}

	if(m_pDC) delete m_pDC;
}

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

	::glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	//	とりあえず、四角形を描画してみる。
	::glPushMatrix();
	::glColor3f( 0.0f, 1.0f, 0.0f );
	glColor3d(1.0, 0.0, 0.0);
	glBegin(GL_POLYGON);
	glVertex2d(-0.9, -0.9);
	glVertex2d(0.9, -0.9);
	glVertex2d(0.9, 0.9);
	glVertex2d(-0.9, 0.9);
	glEnd();

	::glPopMatrix();

	::glFinish();
	if( FALSE == ::SwapBuffers( m_pDC->GetSafeHdc())){}
}

void CGLPictureCtrl::OnMove(int x, int y)
{
	CStatic::OnMove(x, y);

	// TODO: ここにメッセージ ハンドラ コードを追加します。

	Invalidate(TRUE);
	UpdateWindow();
}