none
PictureBoxのチラツキ及び保存方法 RRS feed

  • 質問

  • お世話になっております。

    キャプチャボードより入力した映像にDrawLineで線を重ねて表示したいのですが、線がチラついてしまいます。
    VB6では問題が無かったのですが、Visual Basic 2010では複数線を描画すると
    チラついてしまいます。

    Do

      ----------略

      ret = StretchDIBits(hdc, 0, 0, 960, 720, 0, 0, 640, 480, buffer_view, Bitinfo, 0, &HCC0020)
      g.DrawLine(Pen1, 0, 359, 959, 359)
      g.DrawLine(Pen1, 479, 0, 479, 719)

       Application.DoEvents()

    Loop

    StretchDIBitsの行が実行されると、キャプチャボードからの映像が表示され、DrawLineが消えた所にDrawLineで線を描画という流れになっています。

    ダブルバッファの設定をForm1_Loadに記入しても効果がありませんでした。    Me.SetStyle(ControlStyles.ResizeRedraw, True)
    Me.SetStyle(ControlStyles.DoubleBuffer, True)
    Me.SetStyle(ControlStyles.UserPaint, True)Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)

    あとPictureBoxをPng形式で保存したいのですが、うまくいきませんでした。
    PictureBoxに画像ファイルを読み込んで

            PictureBox1.Image.Save("C:\test.png", System.Drawing.Imaging.ImageFormat.Png)

    を実行すると保存はできるんですが、何かよい方法はないでしょうか?

    よろしくお願いします。

    2013年5月28日 8:49

回答

  • hdc って何から採っていますか?
    ここは重要な部分なので略されると状況が推測できません。
    (Win32API で直接描画してしまっているためにダブルバッファが効いていない、あるいはダブルバッファを適用するべき対象を間違えているあたりが考えられますが、推測を詰める材料が足りません)

    ところで、略されている部分は重たい処理なんでしょうか。
    そうであれば、マルチスレッド化するメリットはありますが、処理自体は軽く、全力で再描画したいだけなら、マルチスレッドは違う選択かなと思います。ただし、無限ループではなく、タイマーによる Refresh などを考えた方がよいですが。
    マルチスレッドを選択する場面としては、データの受信が重かったり、あるいは通信に時間がかかったりして、それが完了したら通知して画面に反映したいという場合ですね。

    ' 個人的には VB6 からの移行で即座にマルチスレッドはたぶんハードルが高いと思っています。
    ' 必要なら仕方ないのですが…。
    2013年5月28日 13:49
    モデレータ

すべての返信

  • 直接的な回答ではないですが、

    私の経験上、VB6 から VB.NET への置き換えの際には、DoEvents は使用せず、マルチスレッドを使用すると上手くいく場合が多いです。

    2013年5月28日 10:37
  • kentahoga様、早速の書き込みありがとうございます。

    マルチスレッドについて調べてみます。

    2013年5月28日 11:42
  • マルチスレッドをお勧めした後でなんですが、この場合は、以下の方法で対応可能かもしれません。

    以下のコードで試した結果、Imageへの代入とDrawLineの位置を入れ替え、その間に任意のスリープを入れると、チラつきが抑えられました。(StretchDIBits() にもよるかと思いますが・・・)

    位置を逆にしない、スリープが無い状態ですと、ラインはチラつきます。

    すみませんが、C#で試していました。コードのイメージということでお願いします。

    private void Form1_Activated(object sender, EventArgs e)
    {
        Pen pen = new Pen(Color.Black, 2);
        while (true)
        {
            Graphics g = pictureBox1.CreateGraphics();
            g.DrawLine(pen, 10, 20, 100, 200);
            g.DrawLine(pen, 10, 50, 200, 10);
            g.Dispose();
    
            System.Threading.Thread.Sleep(200); // 要調整
    
            pictureBox1.Image = System.Drawing.Image.FromFile(@"C:\koala.jpg"); // ここはStretchDIBits()
    
            Application.DoEvents();
        }
    }
    • 編集済み kentahoga 2013年5月28日 12:13 コード修正
    2013年5月28日 12:07
  • hdc って何から採っていますか?
    ここは重要な部分なので略されると状況が推測できません。
    (Win32API で直接描画してしまっているためにダブルバッファが効いていない、あるいはダブルバッファを適用するべき対象を間違えているあたりが考えられますが、推測を詰める材料が足りません)

    ところで、略されている部分は重たい処理なんでしょうか。
    そうであれば、マルチスレッド化するメリットはありますが、処理自体は軽く、全力で再描画したいだけなら、マルチスレッドは違う選択かなと思います。ただし、無限ループではなく、タイマーによる Refresh などを考えた方がよいですが。
    マルチスレッドを選択する場面としては、データの受信が重かったり、あるいは通信に時間がかかったりして、それが完了したら通知して画面に反映したいという場合ですね。

    ' 個人的には VB6 からの移行で即座にマルチスレッドはたぶんハードルが高いと思っています。
    ' 必要なら仕方ないのですが…。
    2013年5月28日 13:49
    モデレータ
  • StretchDIBitsは、Win32APIだったんですね。知らずに、キャプチャーボードからの映像を表示するとのことで少し重たい処理となり、描画に影響がでているのでは?と勘違いし、マルチスレッドをお勧めしました。フェニルアラニンさん、余計な言葉を出してしまい申し訳ありません。

    Azuleanさん、ご指摘ありがとうございます。VB6からの以降ですが場合に応じて、マルチスレッドへの変更、または(記載されている通り)タイマへの変更を行っております。

    • 編集済み kentahoga 2013年5月28日 14:17 追記
    • 回答の候補に設定 星 睦美 2013年5月30日 5:57
    2013年5月28日 14:15
  • Azulean様、回答ありがとうございます。

    hdcですが、Do Loopの前に指定してます。

     hdc = GetDC(PictureBox1.Handle.ToInt32)
      SetStretchBltMode(hdc, 4)


     Do
      ---------以下略

    となっております。

    Refreshも考えたんですが、StretchDIBitsとDrawLineが実行されると描画されるのでRefreshが利いてない状態です。


    2013年5月28日 15:02
  • kentahoga様、ご回答、ありがとうございます。
    ソース参考になりました。

    pictureBox1.Image = System.Drawing.Image.FromFile(@"C:\koala.jpg")

    とダブルバッファを使用するとチラつきが無くなるのは以前確認してました。

    これを、StretchDIBitsに入れ替えるとチラついてしまいます。
    ご指摘の通り、StretchDIBitsが原因だとおもいます。

    Sleepを入れても効果は殆ど現れませんでしたが、StretchDIBitsのしたのDrawLineを二重にすると少しチラつきが抑えられました。

    ret = StretchDIBits(hdc, 0, 0, 960, 720, 0, 0, 640, 480, buffer_view, Bitinfo, 0, &HCC0020)
      g.DrawLine(Pen1, 0, 359, 959, 359)
      g.DrawLine(Pen1, 0, 359, 959, 359)
      g.DrawLine(Pen1, 479, 0, 479, 719)
      g.DrawLine(Pen1, 479, 0, 479, 719)

    ただ、ライン数を3本以上にするとチラつきだします。


    2013年5月28日 15:13
  • キャプチャーからの映像だけを表示する分にはチラつきは無いです。


    ret = StretchDIBits(hdc, 0, 0, 960, 720, 0, 0, 640, 480, buffer_view, Bitinfo, 0, &HCC0020)
    g.DrawLine(Pen1, 0, 359, 959, 359)
    のように
    DrawLineが一つならチラつきません。
    DrawLineが2つ以上でチラついてきます。

    タイマは、後ほど試したいと思います。

    2013年5月28日 15:19
  • まず言えることは、今のコードは VB.NET と相性がよいとは言いがたいです。
    いろいろとやり直さないといけないと予想されます。

     hdc = GetDC(PictureBox1.Handle.ToInt32)
      SetStretchBltMode(hdc, 4)

    これじゃちらついても仕方ないように思います。
    理由は、ダブルバッファ(DoubleBuffered プロパティや ControlStyles.DoubleBuffer など)はあくまで .NET、正確には GDI+ を通じて描画される場合にのみ効果を発揮します。
    しかし、VB6 から移植したあなたのコードはそれらの仕組みを無視して、無理矢理画面上に描いていますので、整合性がとれていませんし、ちらつき(描画過程が見えること)は避けられないと思います。

    通常、ちらつき(描画過程が見えること)を防ぐには、一度バックバッファにすべての絵を重ねて描いてから、画面に一枚の絵に転写する、ダブルバッファという手法を利用します。
    API を使って実現するなら、API で線を描くところまで完結させつつ、メモリ DC に描くことをしないといけませんし、GDI+ を使うならそれと相容れるやり方に改めないといけません。GDI+ 路線の場合は少なくともウィンドウハンドルから HDC を採るのではなく、Graphics から採るとかそういった方向に仕向けるべきでしょうね。(それでちらつきが解決するかは判断できませんが…)

    あなたがどういう風に持って行くのが最適なのかは、あなたの技量・知識・経験・状況に左右されますので、こうすればよいとは、残念ながら一言で言いづらい状況です。
    StrechDIBits が何をするものなのか、GDI と GDI+ はどういった関係なのか、ちらつきを防ぐためのテクニックとは何かといったものを広く調べていただくところでしょうか…。

    なお、本数によってちらつく・ちらつかないが分かれると書かれていますが、環境によって左右されると予想します。
    「本数を変えればよい」と考えず、「自分のプログラムには本質的にちらつき対策ができていない(=ちらついて当たり前のコードになっている)」ととらえて、対策を考えていってください。

    ------

    効果があるかわからないけど、打つとしたら…という候補:

    • GetDC ではなく、Graphics.GetHdc/ReleaseHdc を使う。
    • Graphics を得ているコントロール、フォームに対して DoubleBuffered プロパティが True に設定されているか確認する。設定されていないときはそのコントロールを継承して設定する。
    • StrechDIBits ではなく、Bitmap クラスを使って DrawImage で描いてみる。

    ほかにもあるかもしれませんが、とりあえず思いついたものを書いてみました。やり方は調べてみてください。

    2013年5月28日 15:32
    モデレータ
  • StretchDIBitsは、Win32APIだったんですね。知らずに、キャプチャーボードからの映像を表示するとのことで少し重たい処理となり、描画に影響がでているのでは?と勘違いし、マルチスレッドをお勧めしました。

    この辺は知ってる・知ってないによって反応が分かれてしまいます。
    未知の関数に出会って、回答に迷う場合は、事前に検索してみると知られている関数なのか、そうでないのかを数秒で見極めらますので積極的に検索エンジンを使っていきましょう。

    // 業務環境でコード中の未知の関数を調べるのは下手をすると漏洩につながるので、まずはソリューション検索ですが :-P

    2013年5月28日 15:37
    モデレータ
  • 以下、他の方の回答と被る部分もありますが:

    VB6では問題が無かったのですが

    今のコードだと、GDI コードと GDI+ コードが混在していますよね。VB6 版の方で、線描画を gdiplus.dll の GdipDrawLine API でおこなった場合も、問題は発生していないのでしょうか。元のコードは、 GDI の LineTo API もしくは VB6 の Line メソッドでの描画だったのではありませんか?

    Application.DoEvents()

    Invalidate メソッドの呼び出しに切り替えた方が良いかも知れません。

    hdc = GetDC(PictureBox1.Handle.ToInt32)

    ReleaseDC の呼び出しも忘れずに。

    ところで、VB6 コードを翻訳する際に、HWND や HDC を表すための As Long(あるいは As OLE_HANDLE)を、As Integer(あるいは As Int32)に翻訳していませんか? だとすれば、それらは As IntPtr で宣言されるべきです。

    それと、デバイスコンテキストハンドルの取得のために、GetDC が使用されているようですが、VB6 での hDC プロパティ(+ Refresh)や、VB.NET での Graphics.GetHdc メソッドでは駄目でしょうか?

    SetStretchBltMode(hdc, 4)

    4というのは、STRETCH_HALFTONEのことでしょうか。マジックナンバーで指定するのではなく、Const を使って定数で指定した方が、コードの意図が分かりやすくなりますよ。

    ダブルバッファの設定をForm1_Loadに記入しても効果がありませんでした。

    ダブルバッファの効果は、Paint イベント(OnPaintメソッド)のタイミングで e.Graphics に対して描画する場合に発揮されます。現状のコードを適用させるのであれば、キャプチャデータが取り込まれるたびに Invalidate もしくは Refresh メソッドを呼び出して Paint イベントを誘発し、その中で e.Graphics に対して描画する流れにしてみては如何でしょう。

    Me.SetStyle(ControlStyles.ResizeRedraw, True)
    Me.SetStyle(ControlStyles.DoubleBuffer, True)
    Me.SetStyle(ControlStyles.UserPaint, True)
    Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)

    これでも良さそうですが、今回は VB2005以降 なので、DoubleBuffered プロパティを設定するだけで良いかと思います。これが 2003 以下だとしたら、SetStyle で指定するしか無いのですが。

    なお、既定のダブルバッファリングではなく独自のダブルバッファリングを実装したい場合には、BufferedGraphicsContext クラスを使うことが出来ます。今回はそこまで必要では無いとは思いますが、興味があれば「方法 : バッファリングされたグラフィックスを手動で管理する」を参照してみてください。

    • 回答の候補に設定 星 睦美 2013年5月30日 5:57
    2013年5月29日 1:23
  • 魔界の仮面弁士様、丁寧な回答ありがとうございます。

    Application.DoEvents()をMe.Invalidate()に入れ替えるとメニューやボタンが押せなくなってしまいます。
    使用方法が間違っているのでしょうか?

    Private Sub PictureBox1_Paint(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.PaintEventArgs) _
            Handles MyBase.Paint

    ret = StretchDIBits(hdc, 0, 0, 960, 720, 0, 0, 640, 480, buffer_view, Bitinfo, 0, &HCC0020)
      g.DrawLine(Pen1, 0, 359, 959, 359)
      g.DrawLine(Pen1, 479, 0, 479, 719)

    でも駄目でした。

    PictureBox1を見えない位置に配置し、描画させてからPictureBox2にコピーすればと思っていますが、コピー方法がわかりません。

    Dim g As Graphics = Graphics.FromImage(bmp)
    g.CopyFromScreen(New Point(Me.Location.X + 12, Me.Location.Y + 60), New Point(0, 0), New Size(970, 720))

    のようにコピーしても画像がずれてしまいます。
    今、数値を変更して色々と試している所です。

    2013年5月29日 7:09
  • まずは、ビジネスであればと言う観点で書きます。

    本来的には Windows 開発の経験が豊富な方と一緒に取り組むべき状況です。
    VB6 時代の制約を受けて低レベルの API を駆使して実現したプログラムであるため、VB.NET 時代の高レベルなフレームワークにうまくなじまない状況であるため、小手先では直りません。
    Web ベースのコミュニケーションには限界があるので、できれば先輩・同僚と協力して取り組んでください。頼れる相手がいないとしても、上司にトラブルを抱えていること、今後も予想よりも時間がかかることを報告しておいてください。

    -----

    技術的には以下のことは書いておきたいと思います。

    • GetDC という API を使うコードはやめて、Graphics クラスの GetHdc メソッドと ReleaseHdc メソッドのペアに置き換えてください。
      GDI と GDI+ の世界を協調動作させるための第一歩として必要です。
    • 基本的に、無限ループの処理中は、ユーザーからの操作に対する応答を実現することはできません。
      Application.DoEvents は例外で、ループ中でも操作を可能にするためのおまじないになりますが、よい面と悪い面があります。
      今回の事例で無限ループをやめるのは難しいかもしれませんが、どういった弊害を持つのかは今後調べてみてください。
    • スクリーンからコピーするというアプローチは一見してうまく動くかもしれませんが、ウィンドウが他のウィンドウに重なっている、見えない範囲に置かれている場合、コピーされない可能性があります。
    • 回答の候補に設定 星 睦美 2013年5月30日 5:57
    2013年5月29日 13:17
    モデレータ
  • Application.DoEvents()をMe.Invalidate()に入れ替えるとメニューやボタンが押せなくなってしまいます。

    それはチラつきの件とは別問題で、UI スレッドでループによるポーリングを行っていることが原因かと。

    「何もしない」でいれば、ボタンの操作もできるわけですから、ループ処理が不用意に多すぎるのが要因です。それを DoEvents でどうにか駆動させている状況かと思いますが……そもそも DoEvents は諸刃の剣であり、本来はあまり頻繁に呼ぶようなものでは無かったりします。

    ループ処理を廃して、イベントベースの処理に置き換えるとか(Timer.Tick とか Application.Idle とか Control.Paint とか)、先に案の出ているワーカースレッドを使うとか、PeekMessage、MsgWaitForMultipleObjects 等々を併用するなど、他の作り方を模索した方が良い気がしますが、既存資産からの移行だと、大幅な改修は難しいのかも知れませんね。

    PictureBox1を見えない位置に配置し、描画させてからPictureBox2にコピーすればと思っていますが、コピー方法がわかりません。

    アンマネージの世界だと、CreateCompatibleDC + BitBlt で描画バッファを作成することがあります。

    ところで、画像入力部はどのような仕組みになっているのでしょうか。Video for Windows なのか、DirectShow での制御なのか。あるいは仕組みは不明で、とにかく PictureBox1 に無条件描画される仕組みなのか…。

    Private Sub PictureBox1_Paint(ByVal sender As Object, _
             ByVal e As System.Windows.Forms.PaintEventArgs) _
             Handles MyBase.Paint
    
     ret = StretchDIBits(hdc, 0, 0, 960, 720, 0, 0, 640, 480, buffer_view, Bitinfo, 0, &HCC0020)
       g.DrawLine(Pen1, 0, 359, 959, 359)
       g.DrawLine(Pen1, 479, 0, 479, 719)

    でも駄目でした。

    上記で描画先に指定している hdc は、どこで取得したものでしょうか。

    イベント引数 e.Graphics から生成したものでないのなら、それはダブルバッファリングの対象にならないと思います。Graphics.GetHdc/ReleaseHdc を試してみてください。

    ただし、描画のたびに Graphics から HDC を生み出していてはパフォーマンス面で問題が出るかも知れません。DrawLine 自体もさほど高速ではないですしね(かといって、数回程度の描画なら問題にはなりませんが)。

    ここはいっそ、直線描画も GDI32.DLL の API に担当させてみるのはどうでしょうか。VB6 からのGDI描画で問題が無かったのであれば、.NET でも GDI 描画に頼ってみようという後ろ向きな案ですけれども(うまくいくかどうかは分かりません)。

    あとPictureBoxをPng形式で保存したいのですが、うまくいきませんでした。

    Dim bmp As New Bitmap(x, y) で描画先を作っておき、Graphics.FromImage(bmp) で得た Graphics を使って描画。最後に bmp.Save(target, ImageFormat.Png) というのが基本的な流れです。

    描画先が PictureBox か Bitmap かの違いだけなので、結局、再度描画する必要があります。既に描画した結果を撮影するとなると、BitBlt API や CopyScreen メソッドなどを頼ることになるでしょう。あるいは、描画結果を後で再利用できるよう、PictureBox ではなく初めから Bitmap オブジェクトに描画しておくとか(リアルタイム描画には向きませんが)。

    PictureBox1.Image.Save("C:\test.png", System.Drawing.Imaging.ImageFormat.Png)

    これだと、Image に割り当てられている画像部のみが保存されます。Paint イベントで描画された内容や、CreateGraphics メソッドやGetDC APIで得たキャンパスに描かれた内容は保存されません。

    http://dobon.net/vb/dotnet/graphics/pictureboximageanddrawimage.html

    2013年5月29日 14:11
  • Azulean様、回答ありがとうございます。

    GetDCは、キャプチャーボードメーカー推奨らしく、これ以外の方法は用意していないとの事でした。
    一段落したら、勉強を兼ねてGetHdcにチャレンジしてみたいと思います。

    ボタンが押されるまで、描画を繰り返すのでDo ~ Loopを使用しているんですが、Timer割込みでもいいのかなとも思いながらも変更せずにいます。

    ウインドウが重なった場合は、想定していませんでした。
    コピー前にウインドウを最前面にするとか別な方法を考えるべきですね。

    色々と考えさせられる貴重な意見、ありがとうございました。

    2013年5月30日 0:31
  • 皆様、色々と教えていただき、ありがとうございました。
    とりあえず、現状はと言いますとチラつきに関してはうまく行きました。

    PictureBoxの透過について、試していたところ偶然に出来ていたって事です。
    Form1のTransparencyKeyを指定。
    今回は、多分使われないだろう色”Thistle”を指定してみました。
    次に、PictureBox1のBackColorにTransparentを指定。

    これだけで、チラつきが無くなりました。
    何故チラつかなくなったかの理由は判らないのが気がかりです。

    他にもう一つチラつかない方法がありまして...
    g.DrawLine(Pen1, 0, 359, 959, 359)の代わりにPictureBoxを使います。
    pictureBox2.Location = New Point(0,380)
    pictureBox2.Size = New Point(960,1)といった感じで1ピクセル幅のPictureBoxを作り、最前面に表示。

    この方法を見つけたときを思わず笑ってしまいましたが、ラインの色は変えられますが、点線や斜め線が書けないといった欠点により却下しました。

    魔界の仮面弁士様の指定されたサイトのを試してみましたが保存されませんでした。
    ありがとうございました。

    2013年5月30日 9:29