MDIで画像表示

概略:SDKのMDIで画像を表示するアプリの作り方教えます!

MDIの解説をする時、作るのは決まってメモ帳。
なぜ?基本は丸描いたりするんじゃないの?とか思いますよねー。
答えはメモ帳の方が遙かに簡単だからです(笑)!
裏を返せば、画像を表示させるのは難しいんです(泣)!
ただ表示させるだけでも一苦労です。
メモ帳のときは“EDIT”クラス自らがテキストの内容を自動的に保存してくれていましたが、
自前のクラスではそうはいきません。

では、MDIで画像を表示させるのに最大の問題となるのは何でしょう?
それは、各ドキュメントウィンドウ(以降:ドキュメント)が
どの画像を表示させるのかを区別する方法です。
先ず私は、あらかじめポインタを用意しておいて、
ドキュメントのIDやハンドルを使って関連付ける方法を考えました。
しかし、あらかじめ用意しておいたポインタの数は有限です。
それに、使わないポインタは無駄だし、関連付けるのもややこしくなりそうです。

ここで、私は困ってしまったのですが、
さるお方からウィンドウ自体にデータを保存させる事が出来るという事を教えて貰いました。
各ドキュメント自身がどのデータと関連付けられているのかを知っていれば、
簡単にドキュメント毎に処理を変えることができそうです。

まとめると、MDIで重要なのは
「各ドキュメントが関連付けられているデータはどれなのかを区別する方法」
にあると言えます。

MDI雛形

申し訳ありませんが、MDIの雛形に関してここでは詳しく解説しません。
ちなみに、私のプログラムは猫でも分かる〜のそれを改造したものになっておりますので。

ソース
リソース
リソースヘッダー

ダウンロード

リンクをクリックすると別窓が開きます。
こちらも参照しつつ以降の解説をお読み下さい。
体裁が崩れて見づらいー!という方はダウンロードより
もとのファイルをダウンロードしてVC++でご覧下さい。
また、実行ファイルもダウンロードすることが出来ます。
解説を読む前に自分であれこれ操作してみると理解しやすいと思いますよ。

MDI雛形でちょっと難しい、重要なところは赤字にしました。

#define IDM_FIRSTCHILD 0x00001000

これは、最初に作ったドキュメントのIDになります。
その次に作ったドキュメントのIDはこの番号をインクリメントしたものになります。
ここで重要なのは、これらの番号は他のどのメニュー、
コントロールIDとも重なってはいけないという事です。
もしかして重なってしまった場合、意図しない動作をします。
これらの番号はいつ送られて来るのかというと、
ウィンドウが作成されるたびにメニューに自動追加される項目を選択した時です。
このプログラムの場合だと「ドキュメント No. = x」ですね。
ここで例えば「ドキュメント No. = 5」を選んだのに、
そのドキュメントがアクティブにならないどころか、
閉じちゃったよ……みたいなことになるかも知れません。
私もコレで失敗しました(泣)。

default:
  hChild=(HWND)SendMessage(hClient,WM_MDIGETACTIVE,0,0);
  if(IsWindow(hChild))
    SendMessage(hChild,WM_COMMAND,wp,lp);
  break;


これは、フレームウィンドウプロシージャ(以降:フレームプロシージャ)に
WM_COMMAND が送られてきた時の default 処理です。
重要なのはアクティブなドキュメントがあれば、
そのドキュメントに処理を任せている点です。
フレームプロシージャで処理するのが基本ですが、
特定のドキュメントに画像を表示させる等の固有処理は
ドキュメント自身に任せた方が明解です。
また、default 処理はメニューからウィンドウをアクティブにしようと選択した時の
処理も担っているので、このような自分で処理しないメッセージは
DefFrameProc() に渡さなければいけません。

case WM_MDIACTIVATE:
  if(lp==(LPARAM)hWnd)
    SendMessage(hClient,WM_MDISETMENU,(WPARAM)hMenuDoc,(LPARAM)hMenuDocWnd);
  else
    SendMessage(hClient,WM_MDISETMENU,(WPARAM)hMenuFirst,(LPARAM)hMenuFirstWnd);
  DrawMenuBar(hFrame);
  return 0;


このメッセージは、アクティブになるウィンドウと非アクティブになるウィンドウに送られます。
アクティブになる時は LPARAM に、非アクティブになる時は WPARAM にメッセージが入ります。
今処理しているのはアクティブになる時ですね。
このメッセージは来る順番が重要で、
まず非アクティブになるウィンドウにメッセージが送られて、
次にアクティブになるウィンドウにメッセージが送られます。
つまり、アクティブになるウィンドウ(実際にはないかも知れない)にメッセージが送られた時、
アクティブになるウィンドウが自分ならば、ウィンドウは一つ以上存在するものと考えられ、
アクティブになるウィンドウが自分じゃないならば、ウィンドウは一つも存在しないものと考えられます。
自分というのが分かりづらいですが、
アクティブになるウィンドウのプロシージャにメッセージを送ってるのに
それが自分じゃなければ、ウィンドウは一つも存在しないという事です。
これにより、メニューを切り替えています。
メニューはドキュメントの有る無しで二つ用意しています。
ここの処理は難しいですが、
以降のプログラムでも利用するので、それを見れば掴めてくるかも。

さて話は変わりますが、
私本来のプログラミングスタイルではディフォルトプロシージャは switch() 文の default に書きます。
しかし、今回そうせずに switch() 文の直後に書いたのにはちゃんと理由があります。
MDIの場合は特定の処理( WM_SIZE など)はディフォルトプロシージャに
必ず渡さなければならないという鉄則があるからです。
例え自分で任意の処理をしたとしても、です。
その為、switch() 文を break して抜ければ、ディフォルトプロシージャに渡す事が出来る
このような書き方にせざるを得なかった、という事です。
switch() 文の中に return 0 と break が混在するのはどうにも美しくない気がしますけどね……。

取り敢えず、ここまでの内容を理解してから次にお進み下さい。

MDI画像Viewer

実行画面と仕様



○ドキュメント毎に別々の画像を表示させる(当たり前!)
○画像の入力はドキュメントへのファイルドロップ、クリップボードペースト
○画像の出力はクリップボードコピー
○ドキュメント毎に画像の表示倍率を変更可能

大まかな仕様はこんな感じです。
ドキュメントを開かなければ画像を読み込めないという点が重要です。
また、コピーの可否や表示倍率を変えることで、
各ドキュメントが別々の処理を行っているということが分かるはずです。
別々の処理と言ってもプロシージャは同じなので、
正確にはドキュメント毎に処理を分岐させているんですけどね。

重要箇所の解説

さっきの別窓は閉じて、以下の別窓を開きましょう。

メインソース
リソース
リソースヘッダー
画像入力用ソース
画像入力用ヘッダー
画像出力用ソース
画像出力用ヘッダー

ダウンロード

もちろん実行ファイルもあるので、
自分であれこれ操作してみた方が以降の解説は分かり易いはずです。

プログラムで注目して欲しいのは赤字にしたところです。
プログラムは膨大なので、全てを解説することは出来ませんが、
重要な箇所は詳しく解説します。

〜 WinMain() 〜

ShowWindow(hFrame,SW_MAXIMIZE);

フレームウィンドウ(以降:フレーム)を最大化させます。
CreateWindow() に WS_MAXIMIZE を設定してもダメです。
また、フレームプロシージャで WM_CREATE が来た時に ShowWindow() を実行するのもダメです。
ShowWindow() を実行するタイミングとしては一回目の ShowWindow() 以降、
メッセージループに入る前まで、です。

〜 BMP_in.h , BMP_out.h 〜

typedef struct tagBMPINFO{
  LPDWORD lpPixel;
  BITMAPINFO biInfo;
  HDC hdcBMP;
  double dw,dh;
}BMPINFO,*LPBMPINFO;


LPDWORD lpPixel;    ピクセル列へのポインタ
BITMAPINFO biInfo;   BITMAPINFO構造体
HDC hdcBMP;      DIBSectionのハンドル
double dw,dh;       表示倍率

各ドキュメントは BMPINFO構造体 のメモリ領域へのポインタを保持することになります。
ピクセル列だけポインタとしたのは、サイズが不定だからです。
ピクセル列へのポインタを保持させて、メモリ領域は別に確保すれば
ポインタを辿る事でピクセル列にアクセル出来るというわけです。

〜 DocProc() ... WM_CREATE 〜

lpBmpInfo=(LPBMPINFO)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(BMPINFO));
SetWindowLong(hWnd,GWL_USERDATA,(LONG)lpBmpInfo);


ドキュメントが作成されたら直ぐに BMPINFO構造体 のメモリ領域を確保してしまいます。
最初は、画像が入力されたら……という仕様だったのですが、
画像が入力されていないのに BMPINFO構造体 のメモリ領域に
アクセスされるとマズイことになるので、仕様変更してしまいました。
もちろん、メニューを使用不可にすれば大丈夫ですが、
より強固にするための処置であると考えて下さい。
以降も、無駄なエラー処理がたくさんありますが、
それらは全て安全性を高めるための過剰防衛(笑)。
さて、ウィンドウにデータを設定する為の処理が SetWindowLong() です。
非常に便利な関数です。
これにより、BMPINFO構造体 のメモリ領域への“ポインタ”の値を
各ドキュメントに保持させます。
ところで、このプログラムをビルドすると警告が出るかも知れません。
しかし、これは64ビット対応を意識しての警告なので、今は関係ありません。

〜 DocProc() ... WM_DROPFILES 〜

if(!BMPfile_to_DIBSection(hWnd,szFileName)) return 0;
SendMessage(hWnd,WM_ZoomMenu,0,0);


ドロップファイルの画像を読み込みます。
SendMessage() は何をしているのかというと、
画像が入力されてない場合は表示倍率のメニュー自体を使用不可にするのですが、
それを解除してくれーというメッセージを送っています。
WM_ZoomMenu は私が定義したメッセージです。
関数にしても良かったんですが、DocProc() 固有の処理であるということを強調したかったんです。

〜 BMP_in.cpp ... BMPfile_to_DIBSection() , PasteDIBSection() 〜

lpBmpInfo=(LPBMPINFO)GetWindowLong(hWnd,GWL_USERDATA);
if(lpBmpInfo==NULL) return FALSE;


画像入力用の関数です。
8ビット、24ビットBMPからDIBSectionを作成します。
詳しい処理は余り気にしないで下さい。
関数の最初の処理は、画像情報を保持する BMPINFO構造体 のメモリ領域の有無を調べます
(このプログラムの場合は必ずあるはずですが……)。
メモリ領域が無ければ画像入力は失敗です。

lpBmpInfo->biInfo=biInfo;
lpBmpInfo->lpPixel=lpPixel;
lpBmpInfo->hdcBMP=hdcBMP;
lpBmpInfo->dw=1;
lpBmpInfo->dh=1;


画像が無事読み込めたらそれらのデータを BMPINFO構造体 に保存しましょう。
あらかじめ用意しておいたメモリに保存するだけなので、
画像情報を設定したらもう一度 SetWindowLong() とか実行しちゃダメですよ。
SetWindowLong() で設定したのは BMPINFO構造体 のメモリ領域への“ポインタ”でしたね。

〜 DocProc() ... WM_ZoomMenu 〜

case WM_ZoomMenu:
  hMenu=GetMenu(hFrame);
  mi.cbSize=sizeof(MENUITEMINFO);
  mi.fMask=MIIM_STATE;

  lpBmpInfo=(LPBMPINFO)GetWindowLong(hWnd,GWL_USERDATA);

  if(lpBmpInfo!=NULL && lpBmpInfo->lpPixel!=NULL) mi.fState=MFS_ENABLED;
  else mi.fState=MFS_GRAYED;
  SetMenuItemInfo(hMenu,2,TRUE,&mi);

  DrawMenuBar(hFrame);
  return 0;


「表示倍率」メニューの使用可、不可を切り替えます。
切り替える条件は画像が有るのか無いのか。
画像がなければ表示倍率を変更する意味はありませんし、
もしかしたら BMPINFO構造体 のメモリ領域も確保されていないかも知れません。
従って、無意味に操作すべきではありません。

〜 DocProc() ... WM_MDIACTIVATE 〜

case WM_MDIACTIVATE:
  if(lp==(LPARAM)hWnd){
    SendMessage(hClient,WM_MDISETMENU,(WPARAM)hMenuDoc,(LPARAM)hMenuDocWnd);
    SendMessage(hWnd,WM_ZoomMenu,0,0);
  }else{
    SendMessage(hClient,WM_MDISETMENU,(WPARAM)hMenuFirst,(LPARAM)hMenuFirstWnd);
    DrawMenuBar(hFrame);
  }
  return 0;


WM_ZoomMenu メッセージの登場によりちょっとだけ処理内容が変わっています。
表示倍率メニューの使用可、不可を切り替えるタイミングはいつでしょう?
ドキュメントによって画像の有る無しは違いますから、
アクティブなウィンドウが変更になった時に判定すれば良さそうです。
ドキュメント毎にメニュー等の状態を変えたい時は、全てここから処理を分岐させていきます。

〜 FrameWndProc() ... IDM_ZOOM 〜

case IDM_ZOOM1: case IDM_ZOOM2: case IDM_ZOOM3: case IDM_ZOOM4:
case IDM_ZOOM02: case IDM_ZOOM03: case IDM_ZOOM04:

  hChild=(HWND)SendMessage(hClient,WM_MDIGETACTIVE,0,0);
  lpBmpInfo=(LPBMPINFO)GetWindowLong(hChild,GWL_USERDATA);
  if(lpBmpInfo==NULL || lpBmpInfo->lpPixel==NULL) return 0;

  switch(LOWORD(wp)){
    case IDM_ZOOM1: lpBmpInfo->dw=1; lpBmpInfo->dh=1; break;
    case IDM_ZOOM2: lpBmpInfo->dw=2; lpBmpInfo->dh=2; break;
    case IDM_ZOOM3: lpBmpInfo->dw=3; lpBmpInfo->dh=3; break;
    case IDM_ZOOM4: lpBmpInfo->dw=4; lpBmpInfo->dh=4; break;
    case IDM_ZOOM02: lpBmpInfo->dw=1.0/2; lpBmpInfo->dh=1.0/2; break;
    case IDM_ZOOM03: lpBmpInfo->dw=1.0/3; lpBmpInfo->dh=1.0/3; break;
    case IDM_ZOOM04: lpBmpInfo->dw=1.0/4; lpBmpInfo->dh=1.0/4; break;
  }
  InvalidateRect(hChild,NULL,TRUE); return 0;


表示倍率メニューの項目を選択した時に送られてきます。
ドキュメント自身ではなく、フレームからドキュメント毎に処理を変える場合は
何はともあれ、どのドキュメントがアクティブなのかを調べます。
そしてアクティブなウィンドウに対して処理を行えば良いはずです。
ここでも、もしかして画像がない場合は処理を中断します。
最後に、アクティブなドキュメントに対して再描画メッセージを送ることも忘れないで下さい。

〜 FrameWndProc() ... WM_INITMENU 〜

if(lpBmpInfo!=NULL && lpBmpInfo->lpPixel!=NULL) dw=lpBmpInfo->dw;
else dw=0;


メニュー項目の状態を更新する処理です。
BMPINFO構造体 のメモリ領域が無い、あるいは画像が無いからといって、
WM_INITMENU メッセージ処理を中断するわけにはいきません。
しかし、このプログラムの直後には表示倍率がいくつなのか?の判定があります。
表示倍率を不定値のまま判定するとエラーが発生します。
そこで、あり得ない表示倍率 0 を設定してやれば、すべてのチェックは外れます
(もっとも、選択できないように先ほどプログラムしましたけどね)。

〜 DocProc() ... WM_COMMAND 〜

case WM_COMMAND:
  switch(LOWORD(wp)){
    case IDM_COPY:
      CopyDIB(hWnd);
      return 0;
    case IDM_PASTE:
      if(!PasteDIBSection(hWnd)) return 0;
      SendMessage(hWnd,WM_ZoomMenu,0,0);
      InvalidateRect(hWnd,NULL,TRUE);
      return 0;
  }
  break;


このメッセージはどこからやって来るのかが重要です。
メニューはフレームが持っていますから、直接ドキュメントに送られることはありません。
しかし、フレームプロシージャの WM_COMMAND メッセージ処理で
フレームで処理しないメッセージはアクティブなドキュメントにそのままメッセージを送っていましたね。
そのメッセージがここに来ているんです。
ドキュメント毎に処理を変えたい場合は、ドキュメント自身に処理を委託するのが自然です。
ここでも処理しないメッセージは DefMDIChildProc(hWnd,msg,wp,lp) で処理され、
更にフレームに戻って DefFrameProc(hWnd,hClient,msg,wp,lp) が処理することになります。

〜 DocProc() ... WM_PAINT 〜

lpBmpInfo=(LPBMPINFO)GetWindowLong(hWnd,GWL_USERDATA);
if(lpBmpInfo==NULL || lpBmpInfo->lpPixel==NULL){
  EndPaint(hWnd,&ps);
  return 0;
}

biInfo=lpBmpInfo->biInfo;
iWidth=biInfo.bmiHeader.biWidth;
iHeight=biInfo.bmiHeader.biHeight;
lpPixel=lpBmpInfo->lpPixel;
dw=lpBmpInfo->dw;
dh=lpBmpInfo->dh;


画像を表示する時や画像に処理を施す時など、
必要な時に BMPINFO構造体 のメモリ領域へのポインタを取得して、アクセスしましょう。
よく使うデータなので、プロシージャの switch() 文に入る前にポインタを取得しても
良いかなー?とか考えましたが、
プロシージャには膨大なメッセージが送られて来ているという事を思い出してやめました。
プログラムは煩雑になってしまいますが、
必要な時にポインタを取得するようにしましょう。

また、ここでも失敗談を一つ。
今まで何となく BeginPaint() と EndPaint() の間に任意のプログラムを書いていませんでしたか?
しかし、BeginPaint() と EndPaint() を実行せずに
WM_PAINT メッセージ処理を抜けた場合どうなるでしょうか?
私は画像が無いのならば早く処理を抜けた方が良いと思い、
BeginPaint() の前にそのようなプログラムを書きました。
一見普通に動いているのですが、何とCPU使用率は100%!
何故これに気付いたかというと、メッセージボックスが表示されないという
摩訶不思議な症状が発生した為です(他の処理をするのに忙しかったから)。
あれこれプログラムをコメントアウトしているうちに
WM_PAINT メッセージ処理が原因であることが判明しました。
これは、無効領域(更新領域)がクリアされないうちはいつまでも
WM_PAINT メッセージが送られてきてしまう事が原因でした。
無効領域のクリアを担当しているのは BeginPaint() です。
EndPaint() はBeginPaint() で取得したデバイスコンテキストを解放しています。
もちろん、これらの関数を実行すれば、どこに任意の処理を書いても問題ありません。
こういう初歩的な事って意外と知らない(忘れてる?)ものですよねー。
って、こんな間違いをするのは私だけ!?

〜 DocProc() ... WM_DESTROY 〜

case WM_DESTROY:
  DeleteDIBSection(hWnd);
  break;


BMPINFO構造体 のメモリ領域、及びそのメンバーである
ピクセル列へのポインタからピクセル列のメモリ領域を開放しなければいけません。
本来ドキュメントの削除処理は DefMDIChildProc(hWnd,msg,wp,lp) に任せておけば良いんので、
割り込みで処理させて、最終的には DefMDIChildProc(hWnd,msg,wp,lp) に渡します。
DeleteDIBSection() はドキュメントが死ぬ時に必ず実行しなければいけません。

〜 BMP_in.cpp ... DeleteDIBSection() 〜

void DeleteDIBSection(HWND hWnd)
{
  LPBMPINFO lpBmpInfo;

  lpBmpInfo=(LPBMPINFO)GetWindowLong(hWnd,GWL_USERDATA);
  if(lpBmpInfo!=NULL){
    if(lpBmpInfo->lpPixel!=NULL){
      DeleteObject(lpBmpInfo->hdcBMP);
      lpBmpInfo->lpPixel=NULL;
    }
    HeapFree(GetProcessHeap(),0,lpBmpInfo);
    SetWindowLong(hWnd,GWL_USERDATA,NULL);
  }
}


画像が有れば画像を削除します。
そして、もうその領域にアクセスされては困るので、
ピクセル列へのポインタには NULL を設定しておきます。
同様に、BMPINFO構造体 のメモリ領域も解放し、
そのポインタには NULL を設定します。
このポインタを持っているのはドキュメントですから、
ここで BMPINFO構造体 のメモリ領域を確保した時以来に
SetWindowLong() を実行します。

動作確認と機能拡張

実行ファイルも含めた全ファイルへのリンクを以下にまとめておきました。
ところで、このプログラムに付け足したい機能としては次のようなものが考えられます。

○ドキュメントにスライドバーを付ける
○画像を保存できるようにする
○ドキュメントが無くても画像を読み込めるようにする(ドロップ、ペースト)

これらは皆さん自身でやって下さい。

そして、大きな問題としては
アクティブなドキュメントの最新のヒストグラムを別のドキュメントに表示させる
といったような、別の機能を持ったドキュメントを通常のドキュメントと混在させる方法です。
どうやって同期をとれば良いでしょうか?
これら特別なドキュメントは一つしか存在できないようにした方が良さそうですね。
そうです、Photoshop にもレイヤー等の画像情報を表示する唯一無二のウィンドウがありますね。
こんな感じで、インターフェースに困ったら自分が愛用しているソフトを参考にしてみましょう。

ダウンロード

MDI雛形

MDI画像Viewer

※前述のリンク「ダウンロード」と同じです


戻る / ホーム