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


『ビットマップビューアを作る(その2)』
2002.Apr.25

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

会社
1ビットのモノクロカラーから、32ビットのTrue Colorまで対応したビットマップビューアを作ります。


前回までのあらすじ

前回は16ビット以上のハイカラービットマップを表示するものを作りました。
今回は8ビット(256色)以下でも表示させるものを作ります。
前回同様APIは1コだけです。


256色ビットマップについて

16ビット以上のビットマップの場合、ビットパタンが表しているのはRGB値です。
実は256色(もしくはそれ以下の)ビットマップの場合、ビットパタンはRGB値ではありません。

では何を表しているかというと、カラーテーブルのインデックス値を表しています。

カラーテーブルとは?

まずファイルの構造から見てみましょう。
16ビット以上の場合、ビットマップファイルの構造は以下の形式になります。


8ビット以下の場合、ビットパタンの前にカラーテーブルが追加されます。



筆者はあまり絵心はありませんが、中学校の美術の時間など水墨画など描いた事はあると思います。
その時、絵の具をパレットというものにたらし、ごちゃごちゃと混ぜて色を作ったはずです。(よく白がなくなる)
まさにそれです。

カラーテーブルとは、いうなれば色のデータベースです。
アプリケーションはビットパタン(インデックス値)を読み込み、カラーテーブルを参照してその色を表示させる。という動作をします。


なぜカラーテーブルが必要なの?

RGB値を表そうが、インデックス値を表そうが、どうせ同じ1バイト領域を必要とするなら色を全部統一しとけばいいんじゃないの?
カラーテーブルなんてわざわざ面倒くさいもん作んなくていいじゃん!

ところがどっこいそうじゃないんです。
確かにディスプレイが256種類の色しか表示できないのであれば、それでもいいかもしれません。
しかし今のディスプレイは24ビット(約1600万色)以上表示可能なディプレイが出ています。
余談ですが24ビットというのは人間が見分けられる最大の色数らしいです。
という事は、勝手にウィンドウズOSやデバイスが256色を定義してしまうと、その中にあなたが使いたい色がないかもしれません。
それにそれだと折角のディスプレイの性能が勿体無いです。
256色ビットマップというのは、256色しか表示できないのであって、何も「この色しか表示できない」という訳ではありません。
カラーテーブルにはRGBQUAD構造体を使いますが、この構造体の大きさは32ビット(4バイト)です。
つまりディスプレイが32ビットに対応しているなら、0〜32ビットの上限値までの内、256色を使えるという訳です。

という事で256色DIBにはカラーテーブルというものが存在します。
そして、それがDIBとDDBの違いでもあります。
(デバイスに依存してしまうDDBにはカラーテーブルは存在しません)



作る

ではダラダラ説明は終わりにして、早速作ってみましょう。
APIや構造体の宣言は全て同じですが、BITMAPINFO構造体の宣言のみ以下のように変えてください。

'DIB情報
Public Type BITMAPINFO
  bmiHeader    As BITMAPINFOHEADER
  bmiColors(255) As RGBQUAD ' 256色
End Type

今回は256色までのカラーテブルを読み込む為、256個の配列として宣言します
「あれ?わざわざ静的に宣言するの?」
はい。これについては後述します。


まずコードを見てみましょう。
そんなに難しくはありません。

Private Const BM       As Long = &H4D42 ' BM
Private Const DIB_RGB_COLORS As Long = 0    ' RGBパタン
Private Const DIB_PAL_COLORS As Long = 1    ' パレット


'ビットマップファイル情報
Public Type BITMAPFILEHEADER
  bfType   As Integer ' BM
  bfSize   As Long   ' ファイルサイズ
  bfReserved As Long   ' リザーブ(0)
  bfOffBits  As Long   ' ビットパタンまでのオフセット
End Type

'DIB情報ヘッダ
Public Type BITMAPINFOHEADER
  biSize     As Long  ' 構造体のサイズ
  biWidth     As Long  ' 画像の横幅
  biHeight    As Long  ' 画像の高さ
  biPlanes    As Integer ' プレーン数
  biBitCount   As Integer ' ビット数
  biCompression  As Long  ' 圧縮コード(RLE圧縮)
  biSizeImage   As Long  ' 画像のサイズ(バイト)
  biXPelsPerMeter As Long  ' 水平解像度
  biYPelsPerMeter As Long  ' 垂直解像度
  biClrUsed    As Long  ' カラーテーブルの数
  biClrImportant As Long  ' 重要な色の数(0〜biClrImportantまで)
End Type

'1ピクセルの色
Public Type RGBQUAD
  rgbBlue   As Byte
  rgbGreen  As Byte
  rgbRed   As Byte
  rgbReserved As Byte
End Type

'DIB情報
Public Type BITMAPINFO
  bmiHeader   As BITMAPINFOHEADER
  bmiColors(255) As RGBQUAD      ' 256色
End Type


' DIBをデバイスに描画する
' 宣言はAPIビューワのものと多少違います

Public Declare Function SetDIBitsToDevice Lib "gdi32" ( _
  ByVal HDC As Long, _
  ByVal X As Long, _
  ByVal Y As Long, _
  ByVal dx As Long, _
  ByVal dy As Long, _
  ByVal SrcX As Long, _
  ByVal SrcY As Long, _
  ByVal Scan As Long, _
  ByVal NumScans As Long, _
  Bits As Any, _
  BitsInfo As Any, _
  ByVal wUsage As Long _
) As Long
Public Sub Main()
Dim BFH  As BITMAPFILEHEADER
Dim BI   As BITMAPINFO
Dim Bit() As Byte
Dim Cmd  As String
Dim Argv() As String
Dim Argc  As Long
Dim NO   As Integer


Cmd = Command$()
Argc = ArgvArgc(Cmd, Argv)
If Argc <= 0 Then Exit Sub

NO = FreeFile()
Open Argv(0) For Binary As #NO
  Get #NO, , BFH
  If BFH.bfType <> BM Then
'    ビットマップではない
    MsgBox "形式エラー"
    Close
    Exit Sub
  End If
  Get
#NO, , BI.bmiHeader
  With BI.bmiHeader
    If .biBitCount < 16 Then
'      8Bit以下の場合カラーテーブルを読み込む
      Get #NO, , BI.bmiColors
    End If
'    ReDim Bit(.biHeight * .biWidth * 4) As Byte ' 余分に確保
    ReDim Bit(((.biWidth * .biBitCount + 31) \ 32) * 4 * .biHeight) As Byte ' バイト数を計算
  End With
  Seek
#NO, BFH.bfOffBits + 1  ' ピクセルビットのポインタへ移動
  Get #NO, , Bit
Close

Load Form1
Form1.Width = (BI.bmiHeader.biWidth * Screen.TwipsPerPixelX) + _
        (Form1.Width - (Form1.ScaleWidth * Screen.TwipsPerPixelX))  ' Twipに変換
Form1.Height = (BI.bmiHeader.biHeight * Screen.TwipsPerPixelY) + _
        (Form1.Height - (Form1.ScaleHeight * Screen.TwipsPerPixelY)) ' Twipに変換
Form1.Caption = Argv(0)


' DIBを転送
Call SetDIBitsToDevice(Form1.HDC, 0, 0, BI.bmiHeader.biWidth, BI.bmiHeader.biHeight, _
                    0, 0, 0, BI.bmiHeader.biHeight, Bit(0), BI, DIB_RGB_COLORS)

Form1.Show
End Sub

まず引数の解析をし、ビットマップファイルをバイナリで開きます。
引数解析のArgvArgc関数は自作関数です。コードはサンプルプロジェクトを見てください。
BITMAPFILEHEADER構造体を読み込み、ビットマップ形式かどうかを判別します。
次にBITMAPINFOHEADR構造体を読み込みます。
ここでビットマップの1ピクセルのビット数によってカラーテーブルを読み込むかどうかをIF分岐させます。
ピクセルビットを読み込む為の領域を確保します。

前回は説明しませんでしたが、どのビットマップ(DIB)でも画像の横幅のバイト数は4の倍数でなくてはならないという制約があります。
これを4バイト境界と呼びます。
もしバイト数が4の倍数ではない場合、0(ゼロ)で空き領域を埋め、無理矢理4の倍数にさせなくてはなりません。(ビットマップ保存する場合)
横幅のピクセル数をwとし、ビット数をbとすると、計算式は次のようになります。

w × b で横幅のビット数を出します。(バイト数ではありません)
w × b + 31 としているのは、4の倍数はその余りから、

バイト数 = 4w
バイト数 = 4w + 1
バイト数 = 4w + 2
バイト数 = 4w + 3

のいずれかで表せます。
4の倍数より1ビットでも多ければ、+4バイト(+32ビット)すれば良いわけなので31ビット足します。

(w × b + 31) ¥ 32 でDWORD数を出します。DWORDは4バイトです。
((w × b + 31) ¥ 32) × 4 でバイト数を出します。
((w × b + 31) ¥ 32) × 4 × 高さ で全体のバイト数が出せます。

実はこれだと、実際の領域+1の領域を確保した事になりますが(0から要素が始まるから)、多く確保する分には全然構いません。
計算が面倒臭く感じる人は、コメントアウトしている計算式でもOKです。前述した通り多く確保しても関数は構造体からその画像サイズを計算する為です。

では話を戻して、seekメソッドでピクセルビットへのポインタまで移動します。
これはカラーテーブルを読み込んだ場合、256個分ファイルポインタが移動してしまう為です。(カラーテーブルが256個とは限らない)

あとは前回と同じです。
SetDIBitsToDevice関数の最後の引数はDIB_RGB_COLORSとなります。
256色以下の場合、ピクセルビットはインデックス値を表すのでDIB_PAL_COLORSを指定しそうなもんですが、このフラグはデバイスのパレットを参照する場合にのみ使います。
デバイスのパレットと、読み込んだDIBのカラーテーブルは同じ内容ではありませんので、このフラグを指定すると色がグチャグチャになります。
逆にいえば、デバイスのパレットをカラーテーブルの内容と同じにすれば、今回と同じ結果が得られるという事になります。
関数はBITMAPINFO構造体から、カラーテーブルその他もろもろを読み込んで、表示まで全てやってくれます。




前回の画像をペイントソフト256色減色し、今回のサンプルで表示させたものです。
画像自体が黒ばっかなのでサンプルとしては不適切な気もしますが、ちゃんと表示できました。


余談

さて、余談ですが、今回のサンプルを256色環境で動作させてみてください。
256色環境にするには、そういうディスプレイを使うか、デイスプレイモードを256色にします。
ディスプレイモードを変えるには、画面のプロパティ → 設定タブ → 色 です。
おそらくきちんと表示されないハズです。
筆者の環境では、上のキャラクターの髪の色がおかしくなりました。

はい。これはパレットというものが深く関わっています。
コードの説明の所でチラっとパレットという言葉が出ましたが、あの場合はDIBのカラーテーブルの事を指しました。
しかし実は、パレットとカラーテーブルは別物だと思ってください。
パレットとはデバイスの持っているカラーテーブルの事です。
256色環境で色が出ないのは、デバイスは自分の持っているパレットを駆使し画像を表示させるためです。
「256色環境でもきちんとした色を出したい!」という場合、そのパレットをDIBのカラーテーブルと同じに設定させなくてはなりません。

パレットの詳しい説明は省きます。
チャンスがあれば、パレットを使った表示方法もやりたいと思っています。
意外とインターネットでも、VBでパレット操作サンプルを公開している人は少なかった気がしますので、、、


静的宣言の理由

今回のサンプルでは、BITMAPINFO構造体のbmiColorsメンバは静的配列として宣言しました。
静的とは、「固定」という意味だと思ってください。

DIBのカラーテーブルは256個とは限りません。
4ビットカラーの場合であれば、16以下のテーブルになりますし、動的にメモリを確保した方が感じが良い気がします。
筆者もそう思います。
しかし動的にメモリを確保してしまうと、BITMAPINFO構造体のメンバがメモリ内で整列せず、色が狂ってしまいます。
難しい話で申し訳ありませんが、これを解消するには、一度ファイルの中身を全てバイト型配列に読み込んだのち、それぞれのポインタをそれぞれの構造体へ設定し、変数操作は全てポインタで、、、とかなり処理が面倒くさくなる上、VBでポインタ操作するにはビットマップとはおよそ関係ない知識まで必要となってきます。

ですので、今回は静的宣言としました。


終わり

今回はいろいろ難しい話ばっかでした。
しかしコード自体は実にシンプルなものです。
この調子で今度はDDBを扱うサンプルをやってみようと思います。

今回のサンプルプロジェクト
vb07.lzh(7.12KB)