月刊ソフト作り!
Make The Software For VisualBasic


『ビットマップビューアを作る(その5)』
2002.Jun.01

 Presented by kouta_y
感想等は掲示板、苦情はメールへ。

会社
前回までのサンプルに保存機能を付け加えます。
メニューバーの作成サンプルでもあります。
画像がちと重たいので、一般回線の人は辛抱してください。。。


前回までのあらすじ

前回はインチキスクロールバーをつけました。
別になくてもいいって感じでしたが、メモリDCの有効活用の簡単な例として見てください。
さて、今回は「保存」をやります。
保存といっても、ビットマップファイルのみです。
ここでは読み込んだビットマップを24ビットDIBとして保存します。
メニューバーの作り方の例でもあります。


どうやって保存するのか

はい。ここで紹介するやり方はとても遠回りなやり方です
まず読んでもらう前に、そのことを頭に入れていおいてください。


DDBをDIBに変換する。

前回までのサンプルは、読み込む時はDIBをDDBに変換してウィンドウに表示させました。
保存はその逆をやります。
表示されているDDBを希望のDIBの形式に変換させ、DIBつまりビットマップファイルとして保存させます。

DIBの構造は「ビットマップビューアを作る(その1)」で説明しています。
この構造でファイルへ保存を行います。
24ビットなのでカラーテーブルはありません。(つまりBITMAPINFOのbmiColorsメンバは無視)


使うAPI

さてまた段々と増えてきました。
しかしこれは、少し遠回りなやり方をしている為で、おそらく普通は使わなくても十分できます。

では宣言です。

' オブジェクトの情報を取得
Public Declare Function GetObject Lib "gdi32" Alias "GetObjectA" ( _
  ByVal hObject As Long, _
  ByVal nCount As Long, _
  lpObject As Any _
) As Long

' DDBをDIBに変換
' 宣言を少し変えています

Public Declare Function GetDIBits Lib "gdi32" ( _
  ByVal hdc As Long, _
  ByVal hBitmap As Long, _
  ByVal nStartScan As Long, _
  ByVal nNumScans As Long, _
  lpBits As Any, _
  lpBI As Any, _
  ByVal wUsageAs Long _
) As Long

' 現在選択されているGDIオブジェクトを取得
Public Declare Function GetCurrentObject Lib "gdi32" ( _
  ByVal hdc As Long, _
  ByVal uObjectType As Long _
) As Long

今更ですが、時たま「関数の宣言を変えているか」とコメントをつけていますが、これは引数の受け取り方を変えています。
今回はGetDIBitsのlpBIの型をBITMAPINFOからAnyへ変えています。
これは、上にも書いた通り今回はbmiColorsメンバは無視する為BITMAPINFOHEADER構造体を直接渡すからです。
型がBITMAPINFOのままだと、VBの仕様により違う型を渡すことができません。
「じゃあ型をBITMAPINFOHEADERにすればいーじゃん」
と思う人もいると思いますが、Anyに変えるのは筆者の趣味ですので、Anyでなくてはダメというわけではありません。もちろんBITMAPINFOHEADER型にしてもなんら問題はありません。

ではそれぞれの関数の説明です。


GetObject

指定されたGDIオブジェクトの情報を構造体へ格納します。
今回はウィンドウに表示されているビットマップハンドルを渡し、BITMAP構造体を得ます。

hObject  GDIオブジェクトのハンドルを渡します。
nCount  構造体(バッファ)の大きさを指定。
lpObject  構造体(バッファ)を指定。

戻り値は、成功するとバッファに格納された情報のバイト数が返ります。lpObjectにNULLを指定すると必要なバイト数が返ります。
失敗すると0が返ります。


GetDIBits

指定されたビットマップのビットを取得し、指定された形式でバッファにコピーします。
今回はウィンドウに表示されているビットマップのビットを取得し、DIBの形式に変換するのに利用します。

hdc       互換をとるデバイスコンテキストハンドルを指定。
hBitmap    取得したいビットマップハンドルを渡します。
nStartScan  取得する最初の走査行を指定。通常は最初からなので0を指定。
nNumScan  取得する走査行を指定。全て取得する場合は画像の高さを指定。
lpBits     取得したビットを格納するためのバッファ。
lpBI      希望するDIBの情報。
wUsage   カラーテーブルの形式を指定。今回はDIB_RGB_COLORSを指定。

戻り値は、成功するとコピーされた走査行の数。失敗すると0が返ります。


GetCurrentObject

指定されたデバイスから、指定されたGDIオブジェクトを取得します。
ウィンドウに選択されているビットマップを取得するのに使用します。

hdc        デバイスコンテキストを指定
uObjectType  取得したいオブジェクトの種類を指定。指定できる定数は後述。

戻り値は、成功したら指定された種類のオブジェクトのハンドル。失敗したらNULLが返ります。

GetCurrentObjectのuObjectTypeで指定できる定数は以下
Public Const OBJ_PEN   As Long = 1 ' ペン
Public Const OBJ_BRUSH  As Long = 2 ' ブラシ
Public Const OBJ_PAL   As Long = 5 ' パレット
Public Const OBJ_FONT   As Long = 6 ' フォント
Public Const OBJ_BITMAP  As Long = 7 ' ビットマップ



メニューを付ける

メニューバーは皆さんしょっちゅう見ています。
見たことがないという人はいないと思います。



この「ファイル(F)」というヤツです。
もちろん他のメニューを付加したり、「バージョン情報(A)」などといったよく見かけるメニューも作れます。
今回はこのメニューを選択して、ビットマップ保存をユーザーに促します。
では、これの付け方です。


まず通常のウィンドウデザインの画面を開いてください。
ウィンドウの絵が表示されたら、右クリックをでポップアップメニューから「メニューエディタ(M)」を選択します。


するとメニューエディタのウィンドウが開きます。
このエディタでメニュー作りをすることになります。


パソコンに慣れている人なら、見ただけで使い方がバレそうです。


簡単なメニューを作ってみる

まず最初に「ファイル(F)」というキャプションを持ったメニューを作ります。
「名前(M)」のボックスにはオブジェクト名を入れます。
ここでは「m_File」という名前にします。
他に「チェック」とか「ウィンドウリスト」とかいうチェックボックスがありますが、ここでは無視です。チェックは入れないでください。



こんな感じです。
「&F」としているのは、アクセスキーを割り当てています。つまりAlt + Fキーで「ファイル(F)」をクリックした時と同じ動作が得られます。
しかしこれだけでは「ニュッ」としたメニューは出てきません。
今度はこの「ファイル(F)」の配下にさらにメニューを付け加えます。
「→」とか「←」ボタンで、インデントを下げたり上げたりできます。
最終的には以下のようなメニューが出来上がります。

上から各プロパティを表にしました。
m_File
キャプション(P) ファイル(&F)
m_Save
キャプション(P) 保存(&S)
ショートカット Ctrl + S
m_Line
キャプション(P) -
m_Close
キャプション(P) 閉じる(&X)

こんな感じです。
他のコントロールと同様、メニューもオブジェクト名でプロージャ名が決まります。
因みにメニューバーのイベントは標準で「クリックイベント」しかありません。

とりあえず今はイベントや追加のコードなんて無視して、一回実行してみましょう!

ここまでのプロパティでの実行結果(63KB)


イベントの追加

まず最初にさっきメニューで付けた「閉じる(X)」のクリッリイベントからです。
デザインの画面からメニューをクリックすれば、自動でイベントの追加が出来ます。
コードはたった、
Private Sub m_Close_Click()
' 閉じる
Unload Me
End Sub
これだけ。
ウィンドウを閉じる命令を与えます。


保存をしよう!

さて、ここからが今回の本番です。
さっそくビットマップ保存の方法に取り掛かります。

まず大まかな流れを説明します。
上で宣言したAPIもここで全て使います。

変数の宣言 いろいろ
現在表示しているビットマップのハンドルを取得 GetCurrentObject
取得したビットマップの情報を取得 GetObject
希望のDIB形式のビットマップ情報を設定 BITMAPINFOHEADER
希望のDIB形式のファイルヘッダの設定 BITMAPFILEHEADR
ビットデータの領域確保 ReDimステートメント
DDBをDIBに変換して取得 GetDIBits
ファイルへ保存 Openステートメント

左が処理の説明。右が使うAPIなどです。
ではまず完成コードです。

Private Sub m_Save_Click()
Dim BFH    As BITMAPFILEHEADER
Dim BIH    As BITMAPINFOHEADER
Dim BM    As BITMAP
Dim hBmp   As Long
Dim Bit()   As Byte
Dim FileName As String
Dim NO    As Integer

' ビットマップハンドルを取得
hBmp = GetCurrentObject(hMemDC, OBJ_BITMAP)

' ビットマップ情報を取得
Call GetObject(hBmp, Len(BM), BM)

' 希望のDIB形式を設定
With BIH
  .biSize = Len(BIH)
  .biWidth = BM.bmWidth
  .biHeight = BM.bmHeight
  .biSizeImage = .biHeight * .biWidth * 3 ' 24ビットは3バイト
  .biPlanes = 1
  .biBitCount = 24 ' 24ビット
End With

' 希望DIB形式にみあったファイルヘッダの設定
With BFH
  .bfType = BMTYPE             ' BM(ビットマップ形式)
  .bfOffBits = Len(BFH) + Len(BIH)     ' ビットパタンの開始位置を計算
  .bfSize = .bfOffBits + BIH.biSizeImage  ' ファイル全体のサイズを計算
End With

' ビットデータの領域確保
ReDim Bit(BIH.biSizeImage - 1) As Byte ' -1は領域を1バイト多く取ってしまうから

' DDBを希望のDIBに変換して取得

Call GetDIBits(Form1.hdc, hBmp, 0, BM.bmHeight, Bit(0), BIH, DIB_RGB_COLORS)

' ファイル名取得
FileName = InputBox("ファイル名を入力してください")
If FileName = "" Then Exit Sub

' ファイルへ書き込み
NO = FreeFile()
Open App.Path & "\" & FileName For Binary Access Write As #NO
  Put #NO, , BFH
  Put #NO, , BIH
  Put #NO, , Bit
Close

MsgBox "保存しました!"
End Sub

「保存(S)」をクリックしたら、このプロージャが呼ばれます。
まず、GetCurrentObjectでメモリDCのビットマップハンドルを取得します。
その後、GetObjectでそのビットマップ(DDB)の情報をBITMAP構造体に貰います。
次に、BITMAPINFOHEADER構造体の設定をします。
横幅と縦幅は取得したビットマップと同じです。
biSizeImageはイメージ全体をバイト数で表します。ここでは24ビットにしていますので「横幅×縦幅×3バイト(24ビット)」となります。
1バイトは8ビットですので、24ビットをバイト数で表すと3バイトになります。
プレーン数は1で固定です。
biBitCountは24ビットを指定します。
次に、BITMAPFILEHEADER構造体を設定します。
bfTypeにBMTYPEを指定していますが、これはグローバル定数です。前回までのサンプルのグローバル定数「BM」を「BMTYPE」と変更しただけです。
bfOffBitsはビットパタンまでのオフセット値を指定します。
DIBの構造はファイルヘッダ→ビットマップ情報ヘッダ→カラーテーブル→ビットデータとなっていますので、この内ビットデータを除いたバイト数を計算します。
24ビットマップにカラーテブルは存在しなので、ファイルヘッダとビットマップ情報ヘッダのバイト数を足した数がビットデータまでのオフセット値となります。
bfSizeというのはファイル全体のサイズです。この計算は簡単です。
ビットマップ情報ヘッダの設定の所でイメージ全体のサイズを計算しています。オフセット値の所でヘッダ構造体の合計値を出しています。
この2つを足せば、ファイル全体の合計値がだせます。

長い説明ですが、もう少し我慢してください、、、

で、次にビットデータを格納するためのバッファの領域確保を行います。
ビットデータはイメージの大きさと同じなので、ビットマップ情報ヘッダで指定したbiSizeImageメンバの値を指定します。
しかしここで注意すべきは、通常配列の要素は0から始まります。
つまり、そのままイメージのサイズを渡してしまうと0が余計ですから1バイト分大きめに確保してしまいます。
ですので、−1し1バイト分を差っぴきます。
もしくは(1 To 大きさ)とすればOKです。
以前にも書きましたが、メモリ上では多く確保する分には全然構いません
しかしファイルに保存するとなると、オフセットやバイト数の計算などによって誤差がでてきてしまい、正しく表示できなくなる場合もあります。
ディスクの容量などもあります。
今回の例では−1しなくても表示にも、容量的にも全く問題なく表示させることが出来ますが、なるべくファイルへ保存させる場合はキッチリとした値を入れるようにしましょう。

さて次がメインの一文ですが、DDBを希望DIB形式にしてビットデータを取得します。
デバイスコンテキストにフォームのDCを指定しています。メモリDCは指定しないでください。
この関数はビットマップのハンドルがデバイスコンテキストに選択されている場合、呼び出すと失敗します。

次にInputBox関数を使用し、ファイル名の入力をユーザに促します。
ファイル保存のところで「App.Path」を指定しているので、ファイル名のみ入力します。
本当なら入力チェックもしないといけないのですが、ここでは省きます。

最後にファイルへ書き込みます。
Openステートメントを使用し、バイナリで入力されたファイルを開きます。
アクセス方法は書き込みモードですので、ファイルが存在しない場合、自動的に空のファイルが作られます。
で、ここまででごちゃごちゃと設定した構造体やらなにゃらをDIBの構造に合わせてPutステートメントでファイルへ保存していきます。

保存が完了したら、MsgBoxで保存の完了を知らせてあげます。


実行してみる

さて、ここまで出来たら実行してみましょう。
ちゃんと保存できたら、他のペイントソフトや、このサンプルできちんと表示できたら大成功です。




終わり

最初の方に「ウィンドウに表示してあるビットマップを保存」と書いてしまいましたが、実際には読み込んだビットマップを24ビットにして保存しています。
実際にウィンドウに表示してあるビットマップを保存する場合、今回のサンプルが理解できれば同じ方法で保存することができます。
メモリDCは、バックグラウンドで何かを処理する場合の常套手段です。
今回のサンプルは、逆にいえばビットマップ全体を保存するにはメモリDCが必須となります。(もちろん他の方法もありますが)

では今回のサンプルプロジェクトを下から落とせるようにしました。
おそらくこのサンプルを使用するのはこれで最後となります。
レアですよ。レア。(すんません)

vb11.lzh(11.7KB)