«ゼブラパターンの映り込み 最新 MFC によるダイアログベースのアプリケーション(2)»

床井研究室

※このブログは遅くとも 2027 年 3 月に管理者の定年退職により閉鎖します (移転先は管理者本人共々模索中)

■ 2010年08月20日 [OpenGL] MFC によるダイアログベースのアプリケーション(1)

2019年05月21日 10:57更新

MFC って

実は私, 使ったことないんです. それに, いまさら MFC というのもなんだかなあとは思います. 実際, 某所で UI 用のツールキットって何がいいかなって聞いてみたら Qt 一択みたいに教えてもらいました. でも私, 自分の Linux マシンから KDE 関係排除しようとして Qt も全部消しちゃったくらい Qt には近づかんとこうとしてました. んで, 今回なぜか MFC を使っちゃったんですね. いろいろ事情もあったんですけど(でもその目論見は失敗したのですけど). で, そのプログラムを以前に私自身が FLTK 使えばってアドバイスした学生さんに送っちゃったから, 罪滅ぼしに勉強した MFC の使い方をメモっておきます. もちろん, 間違ってるところがあると思います.

今回はこういうものを作ります.

実行結果

以下は数学と計算さまに大変お世話になりました. 他にもお世話になったところがたくさんあるのですけど, ありすぎて覚えてないので, 後々追加させてください.

プロジェクトの作成

Visual Studio を起動して, 新しいプロジェクトを作成します.

新規プロジェクトの作成

プロジェクトの種類は MFC アプリケーションにします. 「プロジェクト名」を設定した後「OK」をクリックしてください. 「ソリューションのディレクトリ」は作成するまでもないでしょう.

新しいプロジェクト
MFC アプリケーションウィザードが起動したら「次へ >」をクリックして先に進みます.
MFC アプリケーションウィザード

アプリケーションの種類は「ダイアログベース」にします. これが一番簡単みたいです.

アプリケーション種類

ユーザインタフェース機能は「最小化ボタン」だけ付けておくことにします. サイズ変更や最大化ができるようにすると, 後の処理が少し面倒になります. 例によって手抜きですね. ウィンドウを隠すために「最小化ボタン」は付けておきます. あと, ウィンドウのタイトルバーなどに表示する「ダイアログタイトル」も設定します.

ユーザインタフェース機能

「高度な機能」は一切使わないので, チェックボックスを全部外します.

高度な機能

これで終わりです. 「Cプロジェクト名App」と「Cプロジェクト名Dlg」の二つのクラスができていると思います. 「完了」をクリックします.

生成されたクラス

ソリューションのビルド

試に, ここで一度アプリケーションをビルドしてみます. 「ビルド」メニューから「ソリューションのビルド」を選ぶか [F7] をタイプします.

ソリューションのビルド

コンパイルエラーが出なければ (まだ自分でコード書いてないので多分出ないと思いますが), [F5] をタイプするなどして実行してみます.

デバッグ開始

プログラムの変更

プログラムが正常に実行できるようなら, このコードに変更を加えていきます. 最初に「プロジェクト名.cpp」ファイル (ここでは GLsample.cpp) の初期化のところで SetRegistryKey() の引数を変更します. これは別にどうでもいいんでしょうね.

GLsample.cpp の変更

OpenGL を使うので, ヘッダファイルやライブラリの指定を stdafx.h の最後に追加します.

#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glu32.lib")
 
#include <GL/gl.h>
#include <GL/glu.h>

GLEW やそのほかの標準ライブラリのヘッダファイルを #include する場合も, ここに書けばいいんじゃないでしょうか.

stdafx.h の変更

ソースコードの上のタブをクリックして, ダイアログエディタに切り替えます.

ダイアログエディタに切り替え

ウィンドウの中央にある「TODO: ダイアログのコントロールをここに配置」という文字を選択し, [Delete] キーをタイプするなどして削除します. 「OK」や「キャンセル」も使わないので削除してしまいたいところですが, なんかもったいない気がする?ので今は置いておくことにします.

ダイアログの編集

ダイアログエディタの上と左にあるルーラをマウスでクリック・ドラッグして, ガイドを設定します. グリッドシステムは画面設計の基本ですね.

ガイドの設定

「ツールボックス」のウィンドウから「Picture Control」を選びます.

ピクチャコントロールの選択

ガイドに沿ってマウスをドラッグして, 「Picture Control」を配置します.

ピクチャコントロールの配置

「プロパティ」のウィンドウの「ID」を「IDC_STATIC」から「IDC_GLVIEW」などほかの名前に変更します. 「IDC_STATIC」は内容が変更されないコントロールの ID だそうです.

ピクチャコントロールの ID の変更

メンバ変数 (クラス変数) の追加

「クラスビュー」の中にある「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) を右クリックして, 「追加」から「変数の追加」を選びます.

変数の追加

このクラスのメンバ変数を追加します. この変数の「アクセス」は "private", 「変数の種類」は "CStatic" とします. 「変数名」は, ここでは "m_glView" にすることにします. その後「コントロール変数」にチェックを入れ, 「コントロールID」に "IDC_GLVIEW" (Picture Control に設定した ID) を選択して「完了」をクリックします.

変数 m_glView の追加

「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) にもう一つメンバ変数を追加します. これに Picture Control のデバイスコンテキストを保持します.

変数の追加

この変数の「アクセス」は "private", 「変数の種類」は "CDC *" にします. 「変数名」は "m_pDC" とかにするのが習わしなのでしょうか. 最後に「完了」をクリックします.

変数 m_pDC の追加

「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) に更にもう一つメンバ変数を追加します. これに Picture Control 上に表示する OpenGL の領域のレンダリングコンテキストを保持します.

変数の追加

この変数の「アクセス」は "private", 「変数の種類」は "HGLRC" とします. 「変数名」は "m_GLRC" ということにします. 最後に「完了」をクリックします.

変数 m_GLRC の追加

メンバ関数 (メソッド) の追加

同じようにして, メンバ関数も追加します. 「クラスビュー」の中にある「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) を右クリックして, 「追加」から「関数の追加」を選びます.

関数の追加

この関数の「戻り値の型」は "BOOL" とし, 「関数名」は "SetUpPixelFormat" にすることにします. 次に「パラメータの型」に "HDC" を設定し, 「パラメータ名」に "hdc" を設定した後, 「追加」を忘れずにクリックします. また, 「アクセス」は "private" にします. 最後に「完了」をクリックします.

関数 SetupPixelFormat の追加

すると「プロジェクト名Dlg.cpp」ファイル (ここでは GLsampleDlg.cpp) にメンバ関数 (ここでは SetupPixelFormat()) が追加され, ソースコードを編集する状態になります. ここで SetupPixelFormat() の内容を実装します.

BOOL CGLsampleDlg::SetupPixelFormat(HDC hdc)
{
  PIXELFORMATDESCRIPTOR pfd = {
    sizeof(PIXELFORMATDESCRIPTOR),  // PFD のサイズ
    1,                              // バージョン
    PFD_DRAW_TO_WINDOW |            // ウィンドウに描画する
    PFD_SUPPORT_OPENGL |            // OpenGL を使う
    PFD_DOUBLEBUFFER,               // ダブルバッファリングする
    PFD_TYPE_RGBA,                  // RGBA モード
    24,                             // カラーバッファは 24 ビット
    0, 0, 0, 0, 0, 0,               //  (各チャンネルのビット数は指定しない) 
    0, 0,                           // アルファバッファは使わない
    0, 0, 0, 0, 0,                  // アキュムレーションバッファは使わない
    32,                             // デプスバッファは 32 ビット
    0,                              // ステンシルバッファは使わない
    0,                              // 補助バッファは使わない
    PFD_MAIN_PLANE,                 // メインレイヤー
    0,                              //  (予約) 
    0, 0, 0                         // レイヤーマスクは無視する
  };
  
  int pf = ChoosePixelFormat(hdc, &pfd);
  
  if (pf != 0) return SetPixelFormat(hdc, pf, &pfd);
  
  return FALSE;
}

この関数では, コンピュータが備える OpenGL のサブシステムが用意しているピクセルフォーマットをの中から, 変数 pfd に設定した仕様を満たすものを ChoosePixelFormat() を使って選択し, 見つかったものの番号 pf を SetPixelFormat() で現在のデバイスコンテキスト hdc に設定します. 見つからなければ, pf は 0 になります.

関数 SetupPixelFormat() の実装

イベントハンドラの修正

次に, ダイアログウィンドウが開かれるときに呼ばれるメンバ関数 OnInitDialog() に処理内容を追加します. クラスビューの「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) を選択し, その下のメンバ一覧にある OnInitDialog(void) をダブルクリックします. 関数の本体が表示されますから, 「// TODO: 初期化をここに追加します。」の後に下記の内容を追加します.

BOOL CGLsampleDlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  
  ...
  
  // TODO: 初期化をここに追加します。
  m_pDC = new CClientDC(&m_glView);
  
  if (SetupPixelFormat(m_pDC->m_hDC) != FALSE) {
    m_GLRC = wglCreateContext(m_pDC->m_hDC);
    wglMakeCurrent(m_pDC->m_hDC, m_GLRC);
    
    CRect rc;
    m_glView.GetClientRect(&rc);
    GLint width = rc.Width();
    GLint height = rc.Height();
    GLdouble aspect = (GLdouble)width / (GLdouble)height;
    
    // OpenGL の初期設定
    glClearColor(0.0f, 0.0f, 0.5f, 1.0f);
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-aspect, aspect, -1.0, 1.0, -10.0, 10.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
  }
  
  return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

Picture Control に OpenGL による描画を行うので, そのクライアント領域にアクセスする CClientDC オブジェクトを, それを制御するメンバ変数 (ここでは m_glView) 使って生成します. これでいいんでしょうか? 自信がアリマセン. OpenGL の初期設定も, ここでやってしまいます. 多分 OpenGL の機能の呼び出しは別の関数にまとめて, それをここで呼び出すようにしたほうがいいんでしょうけど, ここに書く手順が増えるので手を抜きます.

2014 年 6 月 3 日修正:OnInitDialog() で m_pDC->m_hDC = m_glView.GetDC()->GetSafeHdc(); という代入は不要だというご指摘を頂きました.ありがとうございます.
関数 OnInitDialog() の実装

同様に, ウィンドウの描画が必要になった時に呼ばれるメンバ関数 OnPaint() を変更します. クラスビューの「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) を選択し, その下のメンバ一覧にある OnPaint(void) をダブルクリックします. 関数の本体が表示されますから, その中にある if 文の else 節に下記の内容を追加します.

void CGLsampleDlg::OnPaint()
{
  if (IsIconic())
  {
    ...
  }
  else
  {
    CDialog::OnPaint();
    
    // OpenGL による描画
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // ここでシーンを描く
    
    SwapBuffers(m_pDC->m_hDC);
  }
}

シーンの描画はこの部分で行います. 今のところは, 画面クリアだけしておきます. これも別の関数にまとめておいた方がいいでしょうね.

関数 OnPaint() の実装

イベントハンドラの追加

ウィンドウを閉じた時に発生するイベント WM_DESTROY に対するハンドラを追加します. クラスビューの「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) を選択し, 「プロパティ」ウィンドウの「メッセージ」のボタン (左から6個目のボタン) をクリックします. イベントのリストの中から WM_DESTORY を選び, その右側の▼のボタンをクリックして, 「<追加>> OnDestroy」を選びます.

WM_DESTROY イベントハンドラの追加

すると「プロジェクト名Dlg.cpp」ファイル (ここでは GLsampleDlg.cpp) にメンバ関数 OnDestroy() が追加され, ソースコードを編集する状態になります. ここで OnDestroy() の内容を実装します.

void CGLsampleDlg::OnDestroy()
{
  CDialog::OnDestroy();
  
  // TODO: ここにメッセージ ハンドラ コードを追加します。
  wglMakeCurrent(NULL, NULL);
  wglDeleteContext(m_GLRC);
  delete m_pDC;
}

レンダリングコンテキストをウィンドウのデバイスコンテキストから結合解除し, そのレンダリングコンテキストを削除します. またクライアント領域のデバイスコンテキストを保持している CClientDC オブジェクトを削除します.

関数 OnDestroy() の実装

最後に, ウィンドウのサイズを変えた時に発生するイベント WM_SIZE に対するハンドラを追加します. ですが, 今作っているプログラムはウィンドウサイズの変更や最大化ができないようにしているので, 現時点ではこのイベントは発生しません. なので, ここは省略してかまいません.

クラスビューの「Cプロジェクト名Dlg」クラス (ここでは CGLsampleDlg) を選択し, 「プロパティ」ウィンドウの「メッセージ」のボタン (左から5個目のボタン) をクリックします. イベントのリストの中から WM_SIZE を選び, その右側の▼のボタンをクリックして, 「<追加> OnSize」を選びます.

WM_SIZE イベントハンドラの追加

すると「プロジェクト名Dlg.cpp」ファイル (ここでは GLsampleDlg.cpp) にメンバ関数 OnSize() が追加され, ソースコードを編集する状態になります. ここで OnSize() の内容を実装します.

void CGLsampleDlg::OnSize(UINT nType, int cx, int cy)
{
  CDialog::OnSize(nType, cx, cy);
  
  // TODO: ここにメッセージ ハンドラ コードを追加します。
  if (wglGetCurrentContext() != NULL) {
    CRect rc;
    m_glView.GetClientRect(&rc);
    GLint width = rc.Width();
    GLint height = rc.Height();
    GLdouble aspect = (GLdouble)width / (GLdouble)height;
    
    // OpenGL の初期設定
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-aspect, aspect, -1.0, 1.0, -10.0, 10.0);
    glMatrixMode(GL_MODELVIEW);
  }
}

OnSize() は OnInitDialog() の実行より前に一度実行されます. しかし, その時点ではまだ OpenGL が初期化されていないため, ここで OpenGL の機能を使おうとするとエラーになってしまいます (はまりました). そこで wglGetCurrentContext() を使って OpenGL が使えるかどうか確かめていますが, このやり方がいいのかどうかは知りません.

関数 OnSize() の実装

しかし, 繰り返しになりますが, このプログラムでは WM_SIZE イベントが OnInitDialog() 実行後に呼び出されることはありません. もし, ウィンドウのサイズを変更できるようにした場合は, OnSize() の引数 cx, cy を使って MoveWindow() などにより OpenGL の表示を行っている Picture Control のサイズを変更することになります. その場合は, 変更するサイズを使って width や height を求めればよいので, わざわざ GetClientRect() を使う必要はありません.

でも, ウィンドウのサイズを変更できるようにするには, すべてのコントロールの配置を計算し直さないといけないんですかね. もう考えるのはめんどくさいので, これは考えないことにします.

プログラムの実行

ここで一旦プログラムをビルドし, 実行してみます. 動くかな.

実行結果

今日は疲れたのでここまでにします. 次回以降, この OpenGL のウィンドウに描いた図形を Control で制御する方法をまとめる予定です.

コメント(3) [コメントを投稿する]
通りすがり 2014年05月28日 21:57

OnInitDialog()にある<br> m_pDC->m_hDC = m_glView.GetDC()->GetSafeHdc();<br>の行は不要と思われます。<br><br>この行を残したままの場合、単一のダイアログ構成では異常終了しないかもしれませんが、複数のダイアログ構成のアプリケーションにした場合、このOpen GL表示用のダイアログや別のダイヤログを開いたり閉じたりを数回繰り返すとアプリが異常終了します。

とこ 2014年06月02日 10:20

ありがとうございます!修正します!

わたなべ 2019年05月21日 10:57

> ウィンドウのサイズを変更できるようにするには, すべてのコントロールの配置を計算し直さないといけないんですかね. <br>VisualStudio2013以前ならおっしゃる通り配置を計算しなおす必要があります…<br>VisualStudio2015以降であれば、各コントロールのプロパティに「動的レイアウト」という項目が追加されているので、コードをかかずにウィンドウのサイズにあわせて大きくなったりサイズそのまま場所だけ移動したりすることが可能です。


編集 «ゼブラパターンの映り込み 最新 MFC によるダイアログベースのアプリケーション(2)»