none
[VB.NET] グラフ描画・Paintイベントにて2回目の描画時に描画したものが消えてしまう RRS feed

  • 質問

  • お世話になります。
    色々、情報の不備あるかもしれませんが、よろしくお願いいたします。

    VB.NETにて、Windowsフォームアプリケーションを作成しています。
    VB6からVB.NETへの移行を行っています。
    VisualStudio2017を使用して、.NewFamworkは4.5を使用。

    グラフ描画をしたいと考えており、Panelへグラフを貼り付ける構造にしています。

    コードは公開できない部分がありますが、起きている現象で、自分の理解できていない現象は下記です。

    1)Paintイベントは2回呼ばれる ※これが正しいのかどうか。( Refresh, Update, Invalidate での呼び出しを除いて)

    2)Paintイベントで描画を確認。(デバッカにて)そのあと、ステップインにて処理を追うが、描画が消えてしまうのは、もう一度Paintイベントに入る部分。この部分で描画が消えてしまうので、デバッカで原因が追えなくなってしまっている。この現象を追う方法があれば原因調査を続けれるのだが・・・追い方が分からない。(自分が描画の仕組みが理解できていないのも原因だとは考えている)

    下記のコードで一度、

    Panel_Draw_Paint

    を抜けて、もう一度その部分へ入る時に、描画したものが消えてしまっています。

    Panel_Draw As New Dictionary(Of Integer, PictureBox)
    
    
    Private Sub FrmMain_Load(ByVal eventSender As System.Object, ByVal eventArgs As System.EventArgs) Handles MyBase.Load
    
       Panel_Draw.Add(0, _Panel_Draw_0)
       Panel_Draw.Add(1, _Panel_Draw_1)
       Panel_Draw.Add(2, _Panel_Draw_2)
       Panel_Draw.Add(3, _Panel_Draw_3)
    
            For i = 0 To 3
                AddHandler Panel_Draw(i).Paint, AddressOf Panel_Draw_Paint
            Next
    
    End Sub
    
    
    Private Sub Panel_Draw_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)
    
        ’ここで描画処理
        Dim grp As Graphics = Panel_Draw.CreateGraphics()
             ・
          ・
        '以降、Penを指定したり描画処理...
    
    End Sub

    この現象、どうデバックしたらいいのかなど、アドバイスを頂けますと幸いです。

    よろしくお願いいたします。

    2020年3月7日 0:18

回答

  • Paint イベントはそのコントロールに描くタイミングを与えるものであり、e.Graphics にその描く先が準備されています。
    その上で、さらに CreateGraphics で同じコントロールの描く先を作り出すと、奪い合うような状況になるわけです。(大雑把に書いていますので正確ではない)
    このため、最終結果がどうなるかは、確実なことが言えない状況になるので、「その影響が考えられるでしょうか?」については、「そうかもしれません」となります。

    また、CreateGraphics で描く戦略は、Paint イベントなどの再描画の契機によって失われる一時的なものです。
    描画はなるべく Paint イベントを使い、その e.Graphics に対して処理するように一本化しましょう。

    2020年3月7日 8:30
    モデレータ

すべての返信

  • めちゃくちゃ古いもので恐縮ですが、拙作が参考になるかも、です。
    http://numeric.world.coocan.jp/computer/vb/sampleindex.htm
    「小ネタ」なプロジェクト

    challenger-007さん、.NetFrameworkのWinフォームで描画をプログラムをされるのは初めてですか? 実のところ、ご提示いただいたプログラムは、解らないまま急に難しいことをやっているように見えます。

    Graphicsのオブジェクトをどう取得するか、「Dim grp As Graphics = Panel_Draw.CreateGraphics()」の部分が、意図がわかりません。なぜ、DictionaryクラスであるPanel_Drawに描画をする必要があるのか? (そもそもそんなことができるのか?) なぜ、e.Graphicsを使わないのか? (これがPaintイベントを使う場合の鉄則なのですが・・・。)
    2020年3月7日 0:54
  • ご返信、ありがとうございます。

    Panel_Drawには、格納されたPicterBoxが複数あり、それを一まとめにしています。
    Panelに、各Panel_Drawを貼り付けています。

    コードが一部、違っていました。下記が実際のコードになります。

    Paintの直下は下記のようになっています。

            Dim t As PictureBox = CType(sender, PictureBox)
            Dim Index, i As Short
            For i = 0 To Panel_Draw.Count - 1
                If Panel_Draw(i).Name = t.Name Then
                    Index = i
                    Exit For
                End If
            Next
    
         Dim grp As Graphics = Panel_Draw(Index).CreateGraphics()

    > なぜ、e.Graphicsを使わないのか? 

     → サンプルなどはそうなっていましたが、上記コードでも描画が出来ています。
       描画時に消えてしまうのは、その影響が考えられるでしょうか??

    2020年3月7日 7:57
  • Dictionary からループで Index を探しているのがまずおかしいです。値からキーを探すのでは使い方が逆ですし、そのパターンなら配列や List で十分ではありませんか?

    しかもそのあとで、
    「Dim grp As Graphics = Panel_Draw(Index).CreateGraphics()」
    を呼んでいるのが、2 つの意味でおかしいです。

    CreateGraphics を呼ぶのが目的なら、Index の逆引きは不要で
    「Dim grp As Graphics = t.CreateGraphics()」
    だけで事足りるでしょう。

    そしてそもそも、Paint イベントを使っておきながら、CreateGraphics を呼び出すというのがナンセンスです。CreateGraphics した結果に永続性は無いので、描画した結果は容易に失われます。

    描画結果が失われた後は、再描画が必要ということで Paint イベントが呼び出されますが、この時、描画すべきキャンバスは e.Graphics の方です。e.Graphics を使えば、正しいタイミングで再描画を繰り返し行えます。

    2020年3月7日 8:27
  • Paint イベントはそのコントロールに描くタイミングを与えるものであり、e.Graphics にその描く先が準備されています。
    その上で、さらに CreateGraphics で同じコントロールの描く先を作り出すと、奪い合うような状況になるわけです。(大雑把に書いていますので正確ではない)
    このため、最終結果がどうなるかは、確実なことが言えない状況になるので、「その影響が考えられるでしょうか?」については、「そうかもしれません」となります。

    また、CreateGraphics で描く戦略は、Paint イベントなどの再描画の契機によって失われる一時的なものです。
    描画はなるべく Paint イベントを使い、その e.Graphics に対して処理するように一本化しましょう。

    2020年3月7日 8:30
    モデレータ
  • あー、わかってきました。

    まず、Graphicsオブジェクトをどう取得するかは横においておいて、Paintイベントのハンドラーをどうしようとしているかは、わかりました。複数あるPictureBoxオブジェクトで生じるPaintイベントを、1つのPaintイベントハンドラーPanel_Draw_Paintに集約しているわけですね。その上で改めて、PictureBoxのインスタンスを識別してる。

    次に、識別したPictureBoxの描画サーフェス(Graphics)を得ようと・・・。これは、既にシステム側で済ませてくれてることですね。残念ながら、2回目の投稿で示して頂いた部分は無用なハズです。e.Graphicsが、再描画を要求している(Paintイベントを発生させた)当該PictureBoxの描画サーフェスそのものなので、e.Graphicsを使うべきです。

    あと、PictureBoxのインスタンスによって、描画する中身はかなり違うのでしょうか? Indexの値によってさらに描画のプログラムが分岐するのでしょうか? であれば・・・、設計を改めた方がいいですね。オブジェクト指向プログラミングのうま味を台無しにしているように見えます。

    2020年3月7日 8:38
  • 詳しくご教授いただき、ありがとうございます。
    大変助かりました!

    自分の分かっていない部分が明確になりました。
    e.Graphicsにて、描画すべきオブジェクトが取得出来ているという事ですね。
    変更したら、消えることがあくなりました。

    また、CreateGraphicsが永続性が無いという事、ありがとうございます。
    一時的な描画に使う・・・と考えれば良いでしょうか。
    そうなると、確かにPaintイベントが呼び出される際に、描画結果が失われるのは理解できます。
    (今までは、たまたま描画出来ていただけ・・・という恐ろしいコードを書いてたんですね(;'∀'))

    2020年3月8日 1:18
  • ご教授、ありがとうございます。

    まったく理解できていませんでした。
    e.GraphicsとCreateGraphicsの違いについて、今回の事で理解する事が出来たと思います。

    Paintイベントを使う場合は、CreateGraphicsは描画の保持が不安定になるので、使わない方が良いですね。

    ありがとうございます!!

    2020年3月8日 1:28
  • ありがとうございます。

    仰る通り、一つのPaintイベントハンドラーへ集約しようとしていました。(Dictionaryということで・・・)

    PictureBoxのインスタンスによって、描画する中身は、若干違います。
    Indexの値によって、描画プログラムが分岐する部分があります。

    大枠が同じなので、Indexによって、若干違う値の振り分けをできれば良いかと考えています。
    ありがとうございます。

    2020年3月8日 1:39
  • 表示するグラフは拡大したり、縮小したり、頻繁に書き換えなければいけないものでしょうか?

    そういうのでなければビットマップを作って描画し、それを Image プロパティにセットする方法もあります。

    Dim cs As Size = PictureBox1.ClientSize
    Dim bmp As New Bitmap(cs.Width, cs.Height)
    Using grp As Graphics = Graphics.FromImage(bmp)
        ' grp に対してグラフを描画
        grp.DrawString("HOGE", PictureBox1.Font, Brushes.Red, 0, 0)
    End Using
    PictureBox1.Image = bmp

    こうしておけば、Paint イベントで描画する必要はありません。

    VB6 でいう、AutoRedraw = True のような動きです。

    • 編集済み KOZ6.0 2020年3月8日 9:01
    2020年3月8日 8:59
  • Dim Index, i As Short
    For i = 0 To Panel_Draw.Count - 1
        If Panel_Draw(i).Name = t.Name Then
            Index = i
            Exit For
        End If
    Next

    PictureBoxのインスタンスによって、描画する中身は、若干違います。
    Indexの値によって、描画プログラムが分岐する部分があります。

    Count プロパティの型は As Integer ですので、それを As Short に縮小変換してはいけません。上記のコードだと、Option Strict On モードでコンパイルエラーになってしまうでしょう。(縮小変換する場合は、明示的な型変換が必要になります)

    また先の回答でも述べましたが、Dictionary からループで Index を探す必要は無いと思います。

    たとえば下記の一行を使えば、ループせずとも Index を逆引きすることができます。

    Dim index = Me.Panel_Draw.First(Function(p) p.Value Is sender).Key

    あるいはデザイン時(または実行時)に、個々の PictureBox の Tag プロパティに 0 や 1 といった数値を付与しておくという手もあります。

    Dim pic = DirectCast(sender, PictureBox)
    Dim index = CInt(pic.Tag)   'Tag プロパティに入れておいた値を Index として扱う

     

    …とはいえ、そもそもの要件がSender から Index を得て、その Index を元に分岐させること」にあるのだとしたら、Dictionary の Keyと Value を逆にして管理した方が良いのではないでしょうか?

    つまり、
    Dictionary(Of Integer, PictureBox) ’ Index をキーにして、PictureBox を取り出す
    とするのではなく、下記のようにします。

    • Dictionary(Of String, Integer) 'PictureBox の Name をキーにして、Index を取り出す
    • Dictionary(Of PictureBox, Integer) 'PictureBox そのものをキーにして、Index を取り出す
    • Dictionary(Of Object, Integer) 'sender をキーにして、Index を取り出す

    例:

    Public Class Form1
        Private Panel_Draw As Dictionary(Of PictureBox, Integer)
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Me.Panel_Draw = New Dictionary(Of PictureBox, Integer)()
            Me.Panel_Draw.Add(Me._Panel_Draw_0, 0)
            Me.Panel_Draw.Add(Me._Panel_Draw_1, 1)
            Me.Panel_Draw.Add(Me._Panel_Draw_2, 2)
            Me.Panel_Draw.Add(Me._Panel_Draw_3, 3)
            For Each pic In Me.Panel_Draw.Keys
                AddHandler pic.Paint, AddressOf Me.Panel_Draw_Paint
            Next
        End Sub
    
        Private Sub Panel_Draw_Paint(sender As Object, e As PaintEventArgs)
            Dim pic = DirectCast(sender, PictureBox)
            Dim index = Me.Panel_Draw(pic)  '★探索ループ不要:PictureBox をキーにして Index を得る
    
            Dim sf As New StringFormat() With {.Alignment = StringAlignment.Center, .LineAlignment = StringAlignment.Center}
            Dim text As String = String.Format("#{0}" & vbCrLf & "{1}" & vbCrLf & "{2:HH:mm:ss.ffff}", index, pic.Name, Now)
            e.Graphics.DrawString(text, pic.Font, Brushes.Red, pic.ClientRectangle, sf)
        End Sub
    End Class

    PictureBox に描画するには、「BackgroundImage プロパティに画像を割りあてる」「Image プロパティに画像を割りあてる」「Paint イベントの e.Graphics に描画する」「PictureBox1.CreateGraphics() に描画する」「Graphics.FromHwnd(PictureBox1.Handle) に描画する」などがありますが、それぞれ使いどころが異なります。

    自分で再描画タイミングをコントロールしたいような場合には、CreateGraphics や FromHwnd を使うこともありますが、一般的な Windows アプリではあまり使われません。

    描画内容があまり変化しない画像では、BackgroundImage や Image を使い、拡大縮小やリアルタイムグラフなど、頻繁に再描画を必要とする場合には、Paint の e.Graphics を使います。

    2020年3月9日 1:15