DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

PictureBoxのImageプロパティに関するよくある勘違い

このサイトではPictureBoxに画像を表示する方法として、PictureBoxのImageプロパティを使う方法と、GraphicsのDrawImageメソッドを使う方法を紹介していますが、掲示板に寄せられる質問を拝見しますと、この2つの方法を混同し、正しく理解していない方が多いようです。

例えば、画像ファイルに文字列を合成した画像を作成し、保存しようとして、次のようなコードを書く方がいらっしゃいます。

VB.NET
コードを隠すコードを選択
'フォームのLoadイベントハンドラ
Private Sub Form1_Load(ByVal sender As Object, _
                       ByVal e As EventArgs) _
                       Handles MyBase.Load
    '画像ファイルを表示する
    PictureBox1.Image = New Bitmap("C:\test.jpg")
End Sub

'PictureBox1のPaintイベントハンドラ
Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As PaintEventArgs) _
                              Handles PictureBox1.Paint
    '画像の上に文字列を描画する
    e.Graphics.DrawString("DOBON.NET", Me.Font, Brushes.White, 1, 1)
    e.Graphics.DrawString("DOBON.NET", Me.Font, Brushes.Black, 0, 0)
End Sub

'Button1のClickイベントハンドラ
Private Sub Button1_Click(ByVal sender As Object, _
                          ByVal e As System.EventArgs) _
                          Handles Button1.Click
    'PictureBox1に表示されている画像を保存する
    PictureBox1.Image.Save("C:\test2.jpg", _
                           System.Drawing.Imaging.ImageFormat.Jpeg)
End Sub
C#
コードを隠すコードを選択
//フォームのLoadイベントハンドラ
private void Form1_Load(object sender, System.EventArgs e)
{
    //画像ファイルを表示する
    PictureBox1.Image = new Bitmap(@"C:\test.jpg");
}

//PictureBox1のPaintイベントハンドラ
private void PictureBox1_Paint(object sender, PaintEventArgs e)
{
    //画像の上に文字列を描画する
    e.Graphics.DrawString("DOBON.NET", this.Font, Brushes.White, 1, 1);
    e.Graphics.DrawString("DOBON.NET", this.Font, Brushes.Black, 0, 0);
}

//Button1のClickイベントハンドラ
private void Button1_Click(object sender, System.EventArgs e)
{
    //PictureBox1に表示されている画像を保存する
    PictureBox1.Image.Save(@"C:\test2.jpg",
        System.Drawing.Imaging.ImageFormat.Jpeg);
}

このコードを書いた人は、PictureBox.Imageプロパティの画像にPaintイベントハンドラで描いた文字列が合成されるため、"PictureBox1.Image.Save"で画像を保存すると、保存された画像は元の画像に文字列が合成された画像になると期待しています。しかし実際に保存された画像は元の画像"C:\test.jpg"のままで、文字列は合成されません。PictureBoxには、画像に文字列が合成された画像が確かに表示されているにもかかわらずです。

「なぜ?」と思った方は、まさに勘違いされています。PictureBoxのGraphicsオブジェクトに何を描画しようが、それがPictureBoxのImageプロパティに反映されることはありません。この2つは全くの別物だと思ってください。

このような勘違いをされている方(もしくは、よく理解されていない方)には、どちらかの方法しか使わないようにすることをおすすめします。つまり、以下のどちらかです。

  1. PictureBoxのImageプロパティを一切使用しない。
  2. PictureBoxのGraphicsオブジェクト(PictureBox.PaintイベントとPictureBox.CreateGraphicsメソッド)を一切使用しない。

PictureBoxのImageプロパティを一切使用しない方法

まずは、PictureBoxのImageプロパティを一切使用しないで、上記のコードを書き換えてみましょう。

画像に文字列を合成してPictureBoxに表示するだけならば、次のようになります。

VB.NET
コードを隠すコードを選択
'表示する画像を読み込む
Private originalImaga As Image = New Bitmap("C:\test.jpg")

'PictureBox1のPaintイベントハンドラ
Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As PaintEventArgs) _
                              Handles PictureBox1.Paint
    'PictureBoxに画像を表示する
    e.Graphics.DrawImage(Me.originalImaga, _
        0, 0, Me.originalImaga.Width, Me.originalImaga.Height)
    'さらに文字列を描画する
    e.Graphics.DrawString("DOBON.NET", Me.Font, Brushes.White, 1, 1)
    e.Graphics.DrawString("DOBON.NET", Me.Font, Brushes.Black, 0, 0)
End Sub
C#
コードを隠すコードを選択
//表示する画像を読み込む
private Image originalImaga = new Bitmap(@"C:\test.jpg");

//PictureBox1のPaintイベントハンドラ
private void PictureBox1_Paint(object sender, PaintEventArgs e)
{
    //PictureBoxに画像を表示する
    e.Graphics.DrawImage(this.originalImaga,
        0, 0, this.originalImaga.Width, this.originalImaga.Height);
    //さらに文字列を描画する
    e.Graphics.DrawString("DOBON.NET", this.Font, Brushes.White, 1, 1);
    e.Graphics.DrawString("DOBON.NET", this.Font, Brushes.Black, 0, 0);
}

これは、説明するまでもないでしょう。問題は、PictureBoxに表示されている画像をどうやって保存するかという点です。一つの方法としては、Graphicsオブジェクトに描画する部分をメソッドにするという方法があります。つまり、次のような感じです。

VB.NET
コードを隠すコードを選択
'表示する画像
Private originalImaga As Image = New Bitmap("C:\test.jpg")

'指定されたGraphicsに画像と文字列を描画するメソッド
Private Sub DrawImageAndString(ByVal g As Graphics)
    '画像を描画する
    g.DrawImage(Me.originalImaga, _
        0, 0, Me.originalImaga.Width, Me.originalImaga.Height)
    'さらに文字列を描画する
    g.DrawString("DOBON.NET", Me.Font, Brushes.White, 1, 1)
    g.DrawString("DOBON.NET", Me.Font, Brushes.Black, 0, 0)
End Sub

'PictureBox1のPaintイベントハンドラ
Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As PaintEventArgs) _
                              Handles PictureBox1.Paint
    Me.DrawImageAndString(e.Graphics)
End Sub

'Button1のClickイベントハンドラ
Private Sub Button1_Click(ByVal sender As Object, _
                          ByVal e As System.EventArgs) _
                          Handles Button1.Click
    '保存する画像を描画するBitmapを作成する
    Dim saveImg As New Bitmap(Me.originalImaga.Width, Me.originalImaga.Height)
    'Bitmapに描画する
    Dim g As Graphics = Graphics.FromImage(saveImg)
    Me.DrawImageAndString(g)
    g.Dispose()
    'Bitmapをファイルに保存する
    saveImg.Save("C:\test2.jpg", System.Drawing.Imaging.ImageFormat.Jpeg)
    '後始末する
    saveImg.Dispose()
End Sub
C#
コードを隠すコードを選択
//表示する画像
private Image originalImaga = new Bitmap(@"C:\test.jpg");

//指定されたGraphicsに画像と文字列を描画するメソッド
private void DrawImageAndString(Graphics g)
{
    //画像を描画する
    g.DrawImage(this.originalImaga,
        0, 0, this.originalImaga.Width, this.originalImaga.Height);
    //さらに文字列を描画する
    g.DrawString("DOBON.NET", this.Font, Brushes.White, 1, 1);
    g.DrawString("DOBON.NET", this.Font, Brushes.Black, 0, 0);
}

//PictureBox1のPaintイベントハンドラ
private void PictureBox1_Paint(object sender, PaintEventArgs e)
{
    this.DrawImageAndString(e.Graphics);
}

//Button1のClickイベントハンドラ
private void Button1_Click(object sender, System.EventArgs e)
{
    //保存する画像を描画するBitmapを作成する
    Bitmap saveImg = new Bitmap(
        this.originalImaga.Width, this.originalImaga.Height);
    //Bitmapに描画する
    Graphics g = Graphics.FromImage(saveImg);
    this.DrawImageAndString(g);
    g.Dispose();
    //Bitmapをファイルに保存する
    saveImg.Save(@"C:\test2.jpg",
        System.Drawing.Imaging.ImageFormat.Jpeg);
    //後始末する
    saveImg.Dispose();
}

このようなメソッドを作成する以外の方法としては、

などの方法も考えられます。

PictureBoxのGraphicsオブジェクトを一切使用しない方法

この場合は、PictureBoxのImageプロパティに設定されたイメージに直接描画します。

この方法により、上記のコードを書き換えると、次のようになります。

VB.NET
コードを隠すコードを選択
'フォームのLoadイベントハンドラ
Private Sub Form1_Load(ByVal sender As Object, _
                       ByVal e As EventArgs) _
                       Handles MyBase.Load
    '画像ファイルを表示する
    PictureBox1.Image = New Bitmap("C:\test.jpg")

    'PictureBox1.Imageのイメージに文字列を描画する
    Dim g As Graphics = Graphics.FromImage(PictureBox1.Image)
    g.DrawString("DOBON.NET", Me.Font, Brushes.White, 1, 1)
    g.DrawString("DOBON.NET", Me.Font, Brushes.Black, 0, 0)
    g.Dispose()

    'PictureBoxを再描画する
    '(ここではまだPictureBox1が表示されていないので無意味だが、
    'Imageプロパティに描画した後に再描画しないと更新された画像が表示されない)
    PictureBox1.Invalidate()
End Sub

'Button1のClickイベントハンドラ
Private Sub Button1_Click(ByVal sender As Object, _
                          ByVal e As System.EventArgs) _
                          Handles Button1.Click
    'PictureBox1に表示されている画像を保存する
    PictureBox1.Image.Save("C:\test2.jpg", _
                           System.Drawing.Imaging.ImageFormat.Jpeg)
End Sub
C#
コードを隠すコードを選択
//フォームのLoadイベントハンドラ
private void Form1_Load(object sender, System.EventArgs e)
{
    //画像ファイルを表示する
    PictureBox1.Image = new Bitmap(@"C:\test.jpg");

    //PictureBox1.Imageのイメージに文字列を描画する
    Graphics g = Graphics.FromImage(PictureBox1.Image);
    g.DrawString("DOBON.NET", this.Font, Brushes.White, 1, 1);
    g.DrawString("DOBON.NET", this.Font, Brushes.Black, 0, 0);
    g.Dispose();

    //PictureBoxを再描画する
    //(ここではまだPictureBox1が表示されていないので無意味だが、
    //Imageプロパティに描画した後に再描画しないと更新された画像が表示されない)
    PictureBox1.Invalidate();
}

//Button1のClickイベントハンドラ
private void Button1_Click(object sender, System.EventArgs e)
{
    //PictureBox1に表示されている画像を保存する
    PictureBox1.Image.Save(@"C:\test2.jpg",
        System.Drawing.Imaging.ImageFormat.Jpeg);
}

この方法では、PictureBox.ImageプロパティがNothing(C#では、null)であってはいけません。もし、はじめにPictureBox.Imageプロパティに設定する画像がない場合は、「画像を動的に作成する」で紹介したように新しくイメージを作成し、Imageプロパティに設定します。

VB.NET
コードを隠すコードを選択
'フォームのLoadイベントハンドラ
Private Sub Form1_Load(ByVal sender As Object, _
                       ByVal e As EventArgs) _
                       Handles MyBase.Load
    '300x300の大きさのイメージ作成し、Imageプロパティに設定する
    PictureBox1.Image = New Bitmap(300, 300)

    'PictureBox1.Imageのイメージに文字列を描画する
    Dim g As Graphics = Graphics.FromImage(PictureBox1.Image)
    g.DrawString("DOBON.NET", Me.Font, Brushes.White, 1, 1)
    g.DrawString("DOBON.NET", Me.Font, Brushes.Black, 0, 0)
    g.Dispose()

    'PictureBoxを再描画する
    PictureBox1.Invalidate()
End Sub
C#
コードを隠すコードを選択
//フォームのLoadイベントハンドラ
private void Form1_Load(object sender, System.EventArgs e)
{
    //300x300の大きさのイメージ作成し、Imageプロパティに設定する
    PictureBox1.Image = new Bitmap(300, 300);

    //PictureBox1.Imageのイメージに文字列を描画する
    Graphics g = Graphics.FromImage(PictureBox1.Image);
    g.DrawString("DOBON.NET", this.Font, Brushes.White, 1, 1);
    g.DrawString("DOBON.NET", this.Font, Brushes.Black, 0, 0);
    g.Dispose();

    //PictureBoxを再描画する
    PictureBox1.Invalidate();
}

2つの方法の違い

実は、PictureBox.Imageプロパティで画像を表示する方法は、Paintイベントで画像を描画する方法と全く同じことを内部で行っています。ピクチャボックスのImageプロパティに設定された画像は、ピクチャボックスのPaintイベントが発生する直前(OnPaintメソッド)にDrawImageメソッドを使って描画されているだけです。よって「2つの方法」というのは正確ではなく、実際には1つの方法しかありません。PictureBox.Imageプロパティは、画像をPictureBoxに簡単に表示する方法として用意されているだけと考えてよいでしょう。

  • 履歴:
  • 2010/4/30 「2つの方法の違い」を追加。
  • 2010/9/11 具体例を中心とした説明に書き換えた。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • このサイトで紹介されているコードの多くは、例外処理が省略されています。例外処理については、こちらをご覧ください。
  • イベントハンドラの意味が分からない、C#のコードをそのまま書いても動かないという方は、こちらをご覧ください。
  • Windows Vista以降でUACが有効になっていると、ファイルへの書き込みに失敗する可能性があります。詳しくは、こちらをご覧ください。