C#プログラミングTips

ウィンドウに画像を表示する

デジタルアドバンテージ
2001/08/08


[注意]

本稿でご紹介しているサンプル・プログラムは、.NET Framework SDKベータ2 日本語版をベースに開発・検証されています。クラスライブラリなどが変更されているため、ベータ1ではコンパイルできませんのでご注意ください。

 今回は簡単な画像ビューアを作成しながら、.NET Frameworkを使ったWindowsアプリケーションでのビットマップ表示について解説する。

 以下は、今回作成するサンプル・プログラムの実行画面である。使い方は至極簡単。プログラムを起動してウィンドウを開き、エクスプローラなどから画像ファイルをドラッグ&ドロップすればよい。

今回作成するサンプル・プログラムの実行画面
プログラムを起動してウィンドウを開き、エクスプローラから画像をドラッグ&ドロップすると、このように画像イメージがウィンドウに表示される。

 ウィンドウのサイズは、ドロップされた画像の大きさに自動的に調節される。ドロップ可能(表示可能)な画像ファイルのフォーマットはBMP、GIF、JPEG、EXIF、PNG、TIFFである。これだけ多くの種類の画像ファイルを扱えるのは、今回使用する強力なBitmapクラスのおかげだ。

サンプル・プログラム imageview

 まずはサンプル・プログラムのソース・コードを次に示す。プログラムのファイル名は「imageview.cs」とした。

 1: // imageview.cs
 2:
 3: using System.Drawing;
 4: using System.Windows.Forms;
 5:
 6: class ImageView: Form {
 7:
 8:   Bitmap bitmap = null;
 9:
10:   protected void form_DragEnter(object s,DragEventArgs e) {
11:     e.Effect = DragDropEffects.All;
12:   }
13:
14:   protected void form_DragDrop(object s,DragEventArgs e) {
15:     if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
16:       string filename
17:         = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
18:       this.Text = filename;
19:
20:       bitmap = new Bitmap(filename);
21:       this.ClientSize = bitmap.Size;
22:       this.Invalidate();
23:     }
24:   }
25:
26:   protected override void OnPaint(PaintEventArgs e) {
27:     Graphics g = e.Graphics;
28:     if (bitmap != null) {
29:       g.DrawImage(bitmap, ClientRectangle);
30:     }
31:   }
32:
33:   public ImageView() {
34:     this.AllowDrop  = true;
35:     this.DragEnter += new DragEventHandler(form_DragEnter);
36:     this.DragDrop  += new DragEventHandler(form_DragDrop);
37:     //this.SetStyle(ControlStyles.ResizeRedraw, true);
38:   }
39:
40:   public static void Main() {
41:     Application.Run(new ImageView());
42:   }
43: }
サンプル・プログラムimageviewのソース・コード

 プログラムのコンパイルは次のようにして行う。特にコンパイル・オプションは必要ない。

csc imageview.cs

 今回のサンプル・プログラムでは、以前に「エクスプローラからのドラッグ&ドロップを受け付けるには」で解説したドラッグ&ドロップのテクニックを使用している。上に示したプログラム・コードの約半分はドラッグ&ドロップ処理のものなので、詳細についてはそちらを参照していただきたい。

Bitmapオブジェクトの作成

 最終的に、ドロップされたファイルから、表示しようとする画像ファイルのパス名を取得しているのは次の部分である。

16: string filename
17:   = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
ドロップされたファイルのパス名を配列から取り出す処理

 この方法では、複数ファイルのドロップを受け取ることが可能である。複数ファイルのドロップを受けた場合、それらのパス名は、文字列の配列としてプログラムに与えられる。従って配列を処理すれば、複数の画像ファイルを扱うことができる。ただし今回は、説明を簡単にするために、配列の最初の要素だけを取り出している。

 同じメソッド内の20行目で、Bitmapクラスを用いてBitmapオブジェクトを作成している

20: bitmap = new Bitmap(filename);
21: this.ClientSize = bitmap.Size;
22: this.Invalidate();
Bitmapオブジェクトを作成し、クライアント領域をビットマップと同じサイズにする

 今回使用したBitmapクラスのコンストラクタは、引数としてファイルのパス名をとり、ファイルのオープン処理などを行う。先ほど挙げた種類のファイル(今回作成するプログラムが表示可能なファイル)なら、どれでも引数に指定できる。従ってプログラム側では、ビットマップを明示的にオープンする処理を記述する必要はない。非常に便利なコンストラクタである。

 続く21行目では、FormオブジェクトのClientSizeプロパティにBitmapオブジェクトのSizeプロパティを代入している。これにより、画像ファイルがぴったり収まるようなサイズにウィンドウがリサイズされる。ClientSizeとSizeはともにSize型であり、プロパティとしてHeight(高さ)とWidth(幅)を含んでいる。

 22行目のthis.Invalidate()についてはすぐ次で詳細を述べる。

OnPaintメソッドのオーバーライド

 Bitmapオブジェクトを作成したら、次はそれを描画するのだが、これにはFormクラスのOnPaintメソッドをオーバーライドして記述するのがWindowsアプリケーションでの基本的な方法である。

 OnPaintメソッドは、ウィンドウ内の描画が必要なときに呼び出されるメソッドである。ウィンドウ内の描画は、ユーザーによりウィンドウがリサイズされた場合や、最小化/最大化された場合、あるいは重なっていたウィンドウが移動されて新たな露出部分が生じた場合などに必要となる。もちろん、アプリケーションの起動時にも描画処理は必要だ。

 今回のサンプル・プログラムでは、OnPaintメソッド内でビットマップの表示処理を記述することによって、必要なときにこれが呼び出され、常にウィンドウ内に正しく画像を表示しておけるようになる。次のコードは、このOnPaintメソッドを抜き出した部分である。

26: protected override void OnPaint(PaintEventArgs e) {
27:   Graphics g = e.Graphics;
28:   if (bitmap != null) {
29:     g.DrawImage(bitmap, ClientRectangle);
30:   }
31: }
ビットマップの表示処理を行うOnPaintメソッド

ビットマップの表示

 .NET Frameworkにおけるグラフィックス描画では、まずGraphicsオブジェクトを生成することから始まる。

 OnPaintメソッドが呼び出されると、メソッドの引数として、システムからPaintEventArgsオブジェクトが渡される。このオブジェクトのプロパティとして、Graphicsオブジェクトが含まれている(27行目)。

 そして、すべての描画処理はこのGraphicsオブジェクトのメソッドにより行う。例えば、ウィンドウ内に線を引くなら"g.DrawLine(…)"、文字列を描画するなら"g.DrawString(…)"といった具合である(ここで、"g"はGraphicsオブジェクトを表す)。Graphicsクラスは、非常に多くの描画メソッドを備えた巨大なクラスだ。同様に、ビットマップの表示には"g.DrawImage(…)"を使用する(29行目)。このとき第1引数には、先ほど作成したBitmapオブジェクトを、第2引数にはビットマップの描画開始位置と描画するサイズとして、FormオブジェクトのClientRectangleプロパティ(Rectangle型)を渡している。この行は次のように記述してもよい。

g.DrawImage(bitmap, 0, 0, ClientSize.Width, ClientSize.Height);

 ここで描画サイズを変えれば、ビットマップを任意の大きさに拡大/縮小表示することもできる。すでにウィンドウをビットマップと同じ大きさにしているので、画像ファイルのドロップ直後はビットマップの拡大/縮小は起こらない。しかしこのようにしておけば、ウィンドウ・サイズを変更したときに、画像を拡大/縮小してクライアント領域(ウィンドウ内の描画可能部分)いっぱいに画像を表示できるようになる。

 DrawImageメソッドを呼び出す前に、Bitmapオブジェクトが入る変数bitmapがnullかどうかチェックしている。これを忘れてはいけない。先に述べたように、OnPaintメソッドはプログラムの起動直後にも呼び出される。この時点ではまだBitmapオブジェクトを作成しておらず、bitmapの内容はnullのままだからだ。

描画領域のクリッピング

 それでは、説明を後回しにした29行目の"this.Invalidate()"について解説しよう。この処理は、Bitmapオブジェクトを作成し、ウィンドウのクライアント領域をビットマップのサイズに合わせた直後に呼び出している。例えば、この1行を省略すると、どうなるだろうか? 29行目の"this.Invalidate()"をコメント・アウトして、サンプル・プログラムのウィンドウに画像をドロップすると、ウィンドウの表示は次のようになってしまう。

Invalidateメソッドを呼ばなかった場合の表示
描画可能な領域がクリッピングされるため、プログラムでは全体を描画することはできない。

 ここでグレーの部分は、アプリケーションの起動時にウィンドウがあった部分である。画像ファイルをドロップして、クライアントのサイズを変更した時点で、これまで表示されていなかった部分(上の画像でビットマップが正しく表示されている部分のこと)を描画する必要があるため、最初にOnPaintメソッドが呼び出される。OnPaintメソッドではビットマップ全体を表示しているにもかかわらず、このように一部が描画されずに残ってしまうのは、描画可能な領域が「クリッピング」されてしまうからである。これは、プログラムによるウィンドウの描画を最小限の領域にとどめるための措置だ。つまり上の画像のグレー部分は、以前から表示されていた部分であるため、再描画する必要がないと判断されているわけだ。

 しかし今回のような場合には、いったん全クライアント領域の描画を行う必要がある。これを強制的に行うのがFormクラスのInvalidateメソッドである(正確には、このメソッドはFormクラスのベース・クラスの1つであるControlクラスから継承している)。「Invalidate」は「無効にする」という意味で、このメソッドをパラメータなしで呼び出すと、クライアント領域全体が無効となり、新たに描画する必要が生じる(これは、ウィンドウ全体を覆っていた別のウィンドウが移動した状態に等しい)。その結果OnPaintメソッドが呼ばれ、かつクライアント領域全体が描画されるようになる。

リサイズ時の再描画

 実はこのプログラムでは、ウィンドウのサイズを変更した場合にも同じような現象が起こる。次の画面は、ウィンドウの右下隅をマウスでつまんで、ウィンドウを大きくしたときの画面だ。よく見ると、この場合にもリサイズにより大きくなった部分だけが再描画されているのが分かる。サイズの異なるイメージが何段にも表示されているのは、マウスの移動中に何回かOnPaintが呼び出されたためだ。

画像を表示した後、ウィンドウを大きくしたときの画面
よく見るとリサイズにより大きくなった部分だけが再描画されているのが分かる。サイズの異なるイメージが何段にも表示されているのは、マウスの移動中に何回か描画が行われたため。

 この問題を解決するには、フォーム・コンストラクタ内の37行目でコメント・アウトしている次の行を有効にする。

this.SetStyle(ControlStyles.ResizeRedraw, true);

 このFormクラスのSetStyleメソッドは、コントロールのさまざまな「スタイル」や「挙動」を設定するためのものだ。設定可能なスタイルはControlStyles列挙型で定義されている。そのうちResizeRedrawは、コントロールがリサイズされたときに、コントロールが毎回再描画されるようにするものだ。つまり、毎回OnPaintメソッドが呼ばれるようになる。ただし、リサイズ時にいったん画面全体を消去してしまうようで、リサイズ中は画面消去とビットマップの描画の繰り返しで画面がチラついてしまう。あまり格好はよくないが、何とか使い物にはなる。

強制的な描画

 リサイズ時に画面のチラつきをなくすための1つの方法は、ResizeRedrawを設定せずに、リサイズされたときに、プログラム自身でビットマップ全体を描画してしまう方法だ。ウィンドウのリサイズ時に処理を実行するには、OnPaintメソッドと同じようにして、OnResizeメソッドを置き換える。詳細はまたの機会にするが、OnResizeメソッドの書き出しは次のようになる。

protected override void OnResize(EventArgs e) {
  Graphics g = CreateGraphics();
  ・・・
}
リサイズ時に呼び出されるOnResizeメソッドでのGraphicsオブジェクト取得例

 OnResizeメソッドのパラメータは、PaintEventArgsではなくEventArgsである。そのため描画に必要なGraphicsオブジェクトも自分自身で取得する必要がある。ここで示したように、Controlクラスから継承しているCreateGraphicsメソッドを使用するのが1つの方法だ。ゲームなどのように、タイマやキー入力をトリガとして描画を行うアプリケーションでも、このようにしてGraphicsオブジェクトを取得することになるだろう。End of Article

「C#プログラミングTips」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間