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


『ビットマップビューアを作る(その4)』
2002.May.21

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

会社
前回の続きです。
今回はグラフィックスの技というより、VBの技です。


前回までのあらすじ

前回はメモリDCを使って再描画を実現させました。
折角メモリDCがあるので、いろいろ活用したいところですが、今回はVBのコントロールの方を活用させたいと思います。
そういえばVBのコントロール使ったサンプルってないなぁ・・・。


スクロールバーを使う

スクロールバーというのは、皆さん一度以上は必ずみたことがあるはずです。
IEでいえば、表示できない部分がでてくると、ウィンドウの右端と下にバーが出てきますよね?
それです。
今回は、大きい画像の場合スクロールバーを使用して画像を描画させる位置を移動させてみたいと思います。


こういうやつです。


スクロールバーコントロールの挿入

スクロールバーコントロールは、VisualBasic6.0の標準ランタイムライブラリに入っていますので、特殊な.OCXファイルなどはいりません。
今回もいつも通り、前回のサンプルをそのまま使います。


スクロールバーはどれ?

コントロールバーのところに他のと一緒にあります。


赤丸内の右が、垂直スクロールバー。左が水平スクロールバーと呼ばれています。
ドラッグ&ドロップでフォームに貼り付けてください。
プロパティなどは実行時(つまりソースに直接)に指定しますので、位置や他のプロパティは適当でかまいません。
ただ、オブジェクト名と横幅や縦幅だけはここで指定します。

変更したプロパティ
VScroll1
(オブジェクト名) VScrl
Width 17
HScroll1
(オブジェクト名) HScrl
Height 17

ちなみに「V」というのは「Verticality」の略で「垂直」。「H」は「Horizontality」の略で「水平」という意味です。
野球好きの人なら「Vスライダー」「Hスライダー」と覚えれば簡単です。


筆者のレイアウトはこんな感じです。


随分アバウトですけど、デザイン時はこれでオッケーです。
フォームの背景色も変えてあります。


コードは?

では早速コーディングに入りましょう。
今回はAPIの追加はありません。(ほっ)
スタートアップルーチンはほとんど同じですが、一部変更があります。
おまけコードは邪魔なのではぶいています。

Public BI    As BITMAPINFO
Public hMemDC As Long
Public hBmp  As Long
Public hOldBmp As Long

Public Sub Main()
Dim BFH  As BITMAPFILEHEADER
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)

' メモリDCを作成
hMemDC = CreateCompatibleDC(Form1.hdc)
' ビットマップを作成
hBmp = CreateCompatibleBitmap(Form1.hdc, Form1.ScaleWidth, Form1.ScaleHeight)
' ビットマップを選択
hOldBmp = SelectObject(hMemDC, hBmp)

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

Form1.Show

End Sub

さて、前回との変更点は、

BI変数をグローバル変数にした。
フォームのサイズは変更しない。
フォームの表示はモードレスで。
APIの後処理は、フォームのアンロードイベント内で。(後述)

これだけです。
ここで重要なのはBIをグローバル変数としているところです。
後にも説明しますが、これはスクロールバーの値(位置)や、画像の大きさを計算しないといけないためです。


スクロールバーはどうやって使う

結構面倒くさいです。
スクロールバーをスクロールしたら、勝手に画像が動いてくれるわけではありません。
自分で画像の表示位置、スクロールのさせかたなど全部計算してやらなくてはいけません。
しかしVBはまだ簡単な方なのです。
ですが、こういう計算がパパッとできるようになれば、他のことにもかなり応用がきくようになるはずです。

まず、ここでやる簡単な「流れ」で説明します。
分かりやすくするため、垂直スクロールで説明します。

・ 最初に画像の大きさからスクロールの量を決める。
・ フォームのサイズが変更されたら、(画像の高さ−フォームの描画領域の高さ+スクロールバーの幅)でスクローロバーのMAX値を出す。
・ フォームのサイズが変更されたら、スクロールバーの位置とサイズをフォームにあわせて変更する。
・ フォームの高さが画像の高さを超えてたら、スクロールバーを無効にする。
・ ユーザーがスクロールしたら、再描画を行う。
・ 再描画は、スクロールバーの値をBitBlt関数に渡してメモリDCをフォームに転送する。

こんな感じです。
文中にでてくる描画領域とは、タイトルバーやメニューバーなどを除いた領域のことです。
例えば、BitBlt関数とかで、X,Yの引数に0,0を渡しても、タイトルバーに描画されることはありませんよね。
つまりは「描画領域」とは、実際に描画させることが可能なウィンドウの領域で「クライアント領域」とも呼ばれます。
これは大事なことなので覚えておいてください。
では実際のフォームモジュール内のコードを見てみましょう

Private Sub Form_Load()
' 1/10くらい(テキトー)
VScrl.LargeChange = BI.bmiHeader.biHeight \ 10
HScrl.LargeChange = BI.bmiHeader.biWidth \ 10
End Sub

Private Sub Form_Paint()
' メモリDCの内容をフォームに転送
Call BitBlt(Me.hdc, 0, 0, Me.ScaleWidth, Me.ScaleHeight, _
       hMemDC, HScrl.Value, VScrl.Value, vbSrcCopy)
End Sub

Private Sub Form_Resize()
With VScrl
'  MAX値を計算
  .Max = BI.bmiHeader.biHeight - Me.ScaleHeight + .Width
  If .Max <= 0 Then
    .Enabled = 0 ' 領域をオーバーしたら無効にする
  Else
    .Enabled = 1
  End If
'  位置とサイズを計算
  .Top = 0
  .Left = Me.ScaleWidth - .Width
  If Me.ScaleHeight >= HScrl.Height Then
    .Height = Me.ScaleHeight - HScrl.Height
  End If
End With
With
HScrl
  .Max = BI.bmiHeader.biWidth - Me.ScaleWidth + .Height
  If .Max <= 0 Then
    .Enabled = 0
  Else
    .Enabled = 1
  End If
  .Left = 0
  .Top = Me.ScaleHeight - HScrl.Height
  If Me.ScaleWidth >= VScrl.Width Then
    .Width = Me.ScaleWidth - VScrl.Width
  End If
End With
End Sub

Private Sub
Form_Unload(Cancel As Integer)
' 前のビットマップに戻す
Call SelectObject(hMemDC, hOldBmp)
' ビットマップを解放
Call DeleteObject(hBmp)
' メモリDCを解放
Call DeleteDC(hMemDC)
End Sub

Private Sub
VScrl_Change()
Me.Refresh
End Sub

Private Sub HScrl_Change()
Me.Refresh
End Sub

さて、一番の見所がForm_Resizeイベントでしょうか。いろいろ書いてあります。
まず最初に説明した通り、スクロールバーの移動量を画像の1/10としています。これは適当です。
Loadイベントの次に呼ばれるのが、ここではResizeイベントです。
まずスクロールバーのMAX値を計算しています。式は上で書いた通りです。
なぜ+Widthしているのかは後で説明します。
計算結果がマイナスなら、画像の大きさよりフォームの方が大きいという事ですので、スクロールバーを無効にします。
垂直スクロールバーのTOPは常に0です。
Leftは上手く右端にくるようにします。
次に高さですが、ここでもマイナス値にならないようにIF文でフォームの大きさをみてから計算しています。
HScrlも全く同じ事をしています。

各スクロールバーのChangeイベントは、スクロールバーがスクロールされたら呼び出されます。
このプロージャは再描画命令だけ出して終わります。

では再描画プロージャ(Paintイベント)ですが、前回同様BitBlt関数でメモリDCの内容を転送するだけです。
しかし、今回の見所は後ろから1番目と2番目の引数です。
この引数にはスクロールバーの現在の値を渡します。スクロールバーの値はそのまま画像の位置として計算しているので、そのままValueを渡します。

最後にフォームが閉じられたら、APIで作成したリソースを解放してあげます。
これを忘れると、リソース消費しまくりの最低なアプリケーションとなってしまいます。


完成

ではビットマップファイルをドラッグ&ドロップで渡してみましょう。



こんな感じで表示されました?
スクロールしてみたり、サイズを変更したりといろいろ試してみてください。
今回はこれで終わりです。



ちょっと待ったー!

何かおかしなことに気付きませんか?
はい。
「え?何が?」
と思った人は、ウィンドウの右下の方をみてください。
真っ黒な正方形な部分がありますよね。
「え?もしかして・・・」
はい。その通り。画像の一部です。

なんとこのスクロールバーとんだ欠陥品です。

そうなんです。
通常ならば、あそこには斜め線があって、サイズ変更可の表示がされて、マウス近づけるとマウスアイコンが変わったりするんですが・・・。
このサンプルにそんな現象はおきません

「ふざけるな!ここまでの苦労はなんだったんだ!」
まぁまぁ。(←あほ)
もちろん筆者はこうなることを知ったうえで今回やりました。
なんてことでしょう。確信犯です。

実は、スクロールバーにはスクロールバーコントロールウィンドウスクロールバーという2つのものがあるんです。
上で書いたようなスクロールバーはウィンドウスクロールバーで、今回はそれに似せたインチキスクロールバーなのです。
ウィンドウスクロールバーを作るには、作るだけならそれほど難しくありません。
ですが、今回やったような画像の移動など実際に使う場合、VB向きではないんです

あとあとの「月刊ソフト作り」でもちょっとずつ説明していきますが、これを実現させるにはウィンドウメッセージを受け取れるウィンドウプロージャというものが必要になり、
フォームモジュールは一切使わず、全て標準モジュール内でコードを組まなければなりません。
しかもその方法は、API関数を山ほど使いますので、宣言も増えてめちゃくちゃ面倒くさいです。
フォームモジュールを使わないVBなんて、既にVBではないと思います。
そんな事をするくらいなら、VBではなくC言語など別の言語で作るべきです。(まあ例外もあるでしょうが)

ってなわけで、今回のこのやり方はインチキスクロールバーだということを、十分認識しておいてください。
ちなみにチカチカとスクロールバーが光るのは、OSの仕様らしいです。
光らせないことも可能ですが、ここでは割愛させていただきます。


終わり

さて、今回はコントロールの技となってしまいましたね。
上でインチキ、インチキと連呼しましたが、プログラムというのは「そう見せている」といったインチキ技が多数存在します。
再描画なども、「消えていないように見せている」だけで実際消えています。
それがコンマ数ミリ秒の世界なので、人間には確認することができないだけなんです。

例の如く、今回のサンプルは下から落とせます。
参考にしてください。

vb10.lzh(9.64KB)