6.4 画像の実行時生成
6.4.1 ビットマップを生成する
これまでは、ディスク上のイメージファイルを読み込んで Image オブジェクトを生成しましが、プログラムが実行時に動的にイメージを生成することも可能です。実行時にイメージを生成するには Bitmap クラスのコンストラクタから Bitmap インスタンスを生成して、ビットマップイメージをメモリ上に展開します。この場合、イメージはメモリ上に存在するだけなのでディスクには存在しません。
Bitmap コンストラクタには、イメージの幅と高さを指定します。生成されたインスタンスは何も描かれていない空白のイメージとなります。
public Bitmap(int width, int height)
このコンストラクタで生成されたビットマップイメージのピクセル情報は、不透明度を表すアルファ、赤要素、緑要素、青要素で構成されます。各要素は 8 ビットで表現されるため、1 ピクセル 32 ビットの情報量が必要となります。巨大なイメージを扱うとビットマップイメージは大量のメモリを消費するので注意してください。
さて、動的にイメージを生成しても、このイメージはすべてのピクセルが 0 の値で初期化されているので、DrawImage() メソッドで表示しても何も描画されません。これは、すべてのピクセルの不透明度を表すアルファ要素が 0 なので、イメージ全体が完全な透明になっているためです。
ビットマップイメージはピクセルの集合(配列)なので、個々のピクセルの色情報を更新することでイメージを操作することができます。ピクセルにアクセスするして色を変更するには、Bitmap クラスの SetPixel() メソッドを使います。特定のピクセルの現在の色を取得するには GetPixel() メソッドを使います。
public void SetPixel(int x, int y, Color color);
public Color GetPixel(int x, int y);
x パラメータと y パラメータにはビットマップ上のピクセルの X 座標と Y 座標を指定します。SetPixel() メソッドの color パラメータには、指定した座標に設定する新しい色を指定します。GetPixel() メソッドの場合は、指定したピクセルの現在の色を返します。
SetPixel() メソッドの一度の呼び出しで変更することができるピクセルは全体の 1 ピクセルのみです。そのため、ビットマップ全体を変更しようと考えた場合は for 文などで繰り返しながらすべてのピクセルの色を指定する必要があります。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Bitmap image; protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); e.Graphics.DrawImage(image, 0, 0); } public Test() { image = new Bitmap(400, 300); for(int y = 0 ; y < image.Height ; y++) { for(int x = 0 ; x < image.Width ; x++) { int r = x * 255 / image.Width; image.SetPixel(x, y, Color.FromArgb(r, 0, 0)); } } } static void Main() { Application.Run(new Test()); } }
コード1では、Test クラスのコンストラクタで幅 400 ピクセル、高さ 300 ピクセルの Bitmap インスタンスを生成してます。このビットマップのピクセルはすべて 0 で初期化されているので、このまま描画しても何も表示されません。そこで、for 文を使ったループで、生成したビットマップのすべてのピクセルを SetPixel() メソッドから上書きしています。ここでは、設定するピクセルの X 座標を元に左から右に向かってグラデーションするように色を計算させています。
6.4.2 イメージに描画する
Bitmap クラスのインスタンスがあれば SetPixel() メソッドから自由にイメージ上のピクセルを変更することができます。しかし、図形やイメージをビットマップ上に描画したいときに、ピクセルの座標を計算して個々のピクセルを SetPixel() メソッドで更新するという作業は現実的ではありません。
メモリ上のイメージはディスプレイと同様に、グラフィックスの仮想的な出力デバイスであると考えることができます。Graphics クラスには Image オブジェクトから Graphics オブジェクトを取得する FromImage() メソッドが提供されているので、特定のイメージを参照する Graphics オブジェクトから描画を行うことで、メモリ上のイメージを仮想的な出力デバイスとして制御することができます。
public static Graphics FromImage(Image image)
FromImage() メソッドは、パラメータ image に指定した Image オブジェクトに対応した Graphics オブジェクトを生成して返します。このメソッドが返した Graphics オブジェクトから描画を行うと、参照しているイメージに図が描画されます。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Image image; protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); e.Graphics.DrawImage(image, 0, 0); } public Test() { image = Image.FromFile("test.bmp"); Graphics g = Graphics.FromImage(image); Pen pen = new Pen(Color.Black, 10); for(int y = 0 ; y < image.Height ; y += (int)pen.Width * 2) g.DrawLine(pen, 0, y, image.Width, y); } static void Main() { Application.Run(new Test()); } }
コード2は、ディスク上のビットマップファイル test.bmp から Image オブジェクトを生成し、生成したイメージオブジェクトにさらに Graphics オブジェクトから線を描画することで、イメージに直接描画を行っています。
このように、メモリ上に展開されているイメージを仮想的な出力デバイスと見立てて Graphics オブジェクトを用いて図形を描画することができます。このようなメモリ上のイメージをメモリデバイスまたはオフスクリーンと呼ぶこともあります。メモリ上のイメージに描画するこの手法はゲームのような画面の更新処理が頻繁に行われるアプリケーションでは極めて重要になります。
複雑な描画処理を行う場合は、ひとつのフレーム(完成されたひとつの画面)を構築するまでに描画を行う様々な処理工程を通ります。もし、直接フォームに描画を行った場合、画面が完成されるまでの描画過程がユーザーに見られてしまう可能性があります。しかし、メモリ上のイメージは最終的に DrawImage() メソッドを使って描画されるまでは画面に表示されません。複雑な描画処の過程はすべてメモリ上のイメージに行い、最終的に完成したイメージを DrawImage() メソッドで画面に描画すれば効率的になります。
また、一度作成したイメージをメモリ上に保存しておけるという点でも便利です。OnPinat() や Paint イベントで直接フォームに描画した図形は、フォームが隠れてしまうとその内容を失ってしまいます。しかし、メモリ上のイメージに描画した図形は明示的にピクセルを上書きしない限り消えることはありません。
ペイントソフトのように、イメージを直接編集するソフトウェアの構築を考えると、Image オブジェクトに Graphics から描画する処理が活躍することになります。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Image image; private readonly Size es = new Size(10, 10); protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); e.Graphics.DrawImage(image, 0, 0); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove (e); if (e.Button == MouseButtons.Left) { Graphics g = Graphics.FromImage(image); Brush brush = new SolidBrush(Color.Black); g.FillEllipse( brush, e.X - (es.Width / 2), e.Y - (es.Height / 2), es.Width, es.Height ); Invalidate(); } } public Test() { image = new Bitmap(400, 300); Graphics g = Graphics.FromImage(image); Brush brush = new SolidBrush(Color.White); g.FillRectangle(brush, 0, 0, image.Width, image.Height); } static void Main() { Application.Run(new Test()); } }
コード3は、ウィンドウ上で左ボタンを押したまま移動すると、ドラッグに応じてカーソル上に線が引かれるというプログラムです。実際には FillEllipse() メソッドでカーソルを中心に円を描画しています。この動作はペイントソフトの基本原理になります。ここで重要なのは、カーソルの入力に対してフォーム上に直接描画しているのではなく、OnMouseMove() メソッド内でメモリ上のイメージに描画を行っている点です。
ウィンドウ上に直接図形を描画した場合、ウィンドウを再描画すると以前に描画していた情報が失われてしまいます。編集中のイメージが他のウィンドウに隠されただけで消えてしまうようではペイントソフトとして使えません。また、OnMouseMove() メソッドや MouseMove イベントには Graphics オブジェクトが渡されません。よって、ここから直接フォームに描画するのも簡単ではありません。こうした問題は、オフスクリーンとして扱われるメモリ上のイメージに描画することで解決できます。
6.4.3 イメージの回転と反転
イメージに対して細かい操作が必要な場合は、Graphics オブジェクトから描画を行ったり、ピクセルを直接操作する必要がありますが、Image クラスにはイメージの反転処理を行ってくれる RotateFlip() メソッドが用意されています。
public void RotateFlip(RotateFlipType rotateFlipType)
rotateFlipType パラメータには、イメージの回転や反転を指定する System.Drawing.RotateFlipType 列挙体のいずれかのメンバを指定します。
[Serializable] public enum RotateFlipType
この列挙体には、水平方向や垂直方向への反転と、90度回転、180度回転、270度回転を組み合わせてイメージを回転させたり、反転させることができます。RotateFlip() メソッドは反転や回転でイメージオブジェクトを更新してしまうので、元の画像を保存しておきたい場合は注意してください。
イメージを編集するときに、元のイメージを保存しておきたい場合は Image クラスの Clone() メソッドを使います。Clone() メソッドは、現在のイメージの複製を生成して返してくれます。
public virtual object Clone()
このメソッドの戻り値型は object ですが、安全に Image 型のキャスト変換することができます。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Image imageSrc, imageClone; protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); if (imageSrc == null) { imageSrc = Image.FromFile("test.bmp"); imageClone = (Image)imageSrc.Clone(); imageClone.RotateFlip(RotateFlipType.RotateNoneFlipXY); } e.Graphics.DrawImage(imageSrc, 0, 0); e.Graphics.DrawImage(imageClone, imageSrc.Width, 0); } static void Main() { Application.Run(new Test()); } }
コード4は、ディスクから読み込んだ test.bmp 画像ファイルのイメージを水平方向と垂直方向に反転して描画するプログラムです。ディスクから読み込んだ元のイメージは imageSrc 変数に保存され、編集用に新しく生成するコピーを imageClone 変数に保存しています。その後、コピーしたイメージを RotateFlip() メソッドで反転しているため、実行結果を見ると imageClone のイメージが水平・垂直方向共に反転していることがわかります。