DOBON.NET DOBON.NETプログラミング掲示板過去ログ

PictureBoxのPaintイベントでStretchBltで描画したい

環境/言語:[VB.NET]
分類:[.NET]

画像ファイルのビューアを作っています。
フォームにPictureBoxとVScrollBar、HScrollBarを置いています。
ビットマップm_MyBmpに画像ファイルを別途ロードして
PictureBoxのPaintイベントにてPictureBoxの描画領域に相当する
矩形領域をビットマップから切り出して描画しています(DrawImageメソッド使用)。
Paintイベントでのコードは下記の通りで問題なく動作します。

Private Sub PictureBox1_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
        Dim rectD As New Rectangle(0, 0, PictureBox1.Width, PictureBox1.Height)
        Dim w As Integer
        Dim h As Integer
        Dim iHS As Integer
        Dim iVS As Integer

        If m_MyBmp Is Nothing Then
            Exit Sub
        End If

        w = PictureBox1.ClientSize.Width / m_ZoomF 'm_ZoomF:ズーム倍率変数(1,2,4,8,16の何れか)
        h = PictureBox1.ClientSize.Height / m_ZoomF
        iHS = HScrollBar1.Value
        iVS = VScrollBar1.Value
        With e.Graphics
            .PixelOffsetMode = Drawing2D.PixelOffsetMode.Half
            .InterpolationMode = Drawing2D.InterpolationMode.NearestNeighbor
            .DrawImage(m_MyBmp, rectD, iHS, iVS, w, h, GraphicsUnit.Pixel)
        End With
End Sub

ところが巨大な画像ファイル(例えば16Kx4K)を開いたとき、パフォーマンスが
急激に落ちます。
Windowsアクセサリのペイントで同じ画像を開いてもスクロールなどは高速
なので、PCの問題でなく、プログラムの作り方の問題のはずです。
ネットで調べてDrawImageは巨大画像ファイルで遅くなると知り、この部分を
StretchBltを用いた処理にて置き換えたいと考えました。色々調べてやって
いますが、うまくいきません。

Private Sub PictureBox1_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
        Dim rectD As New Rectangle(0, 0, PictureBox1.Width, PictureBox1.Height)
        Dim w As Integer
        Dim h As Integer
        Dim gS As Graphics
         Dim HDC_Src As IntPtr
        Dim HDC_Dst As IntPtr
        Dim hBmpSrcBackup As IntPtr

        If m_MyBmp Is Nothing Then
            Exit Sub
        End If

        HDC_Dst = CreateCompatibleDC(e.Graphics.GetHdc())
        gS = Graphics.FromImage(m_MyBmp)
        HDC_Src = gS.GetHdc()
        hBmpSrcBackup = SelectObject(HDC_Src, m_MyBmp.GetHbitmap())
        StretchBlt(HDC_Dst, 0, 0, w, h, HDC_Src, HScrollBar1.Value, VScrollBar1.Value, SRCCOPY)
        SelectObject(hBmpSrcBackup, m_MyBmp.GetHbitmap())
        gS.ReleaseHdc(HDC_Src)
        e.Graphics.ReleaseHdc(HDC_Dst)

End Sub

このように書きましたが、何も描画されません。
アドバイスを頂きたく、よろしくお願いします。
C#のコードですが、PINVOKE.NETにサンプルがありますので、参考になると思います。

pinvoke.net: StretchBlt (gdi32)
http://pinvoke.net/default.aspx/gdi32/StretchBlt.html
■No23845に返信(管理人さんの記事)
> C#のコードですが、PINVOKE.NETにサンプルがありますので、参考になると思います。
>
> pinvoke.net: StretchBlt (gdi32)
> http://pinvoke.net/default.aspx/gdi32/StretchBlt.html
>
参考になりそうなサンプルの紹介ありがとうございます。
検索範囲に英語も加えないとダメですね。
これを読んで、コードを直したら、ここに報告したいと思います。
紹介して頂いたサンプルを見て、下記のようにコードを修正して画像の表示は
できました。

Private Sub PictureBox1_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint 
        Dim rectD As New Rectangle(0, 0, PictureBox1.Width, PictureBox1.Height)
        Dim w As Integer
        Dim h As Integer
        Dim HDC_Src As IntPtr
        Dim HDC_Dst As IntPtr
        Dim hOrg As IntPtr
        Dim hNew As IntPtr

        If m_MyBmp Is Nothing Then
            Exit Sub
        End If

        w = PictureBox1.ClientSize.Width / m_ZoomF
        h = PictureBox1.ClientSize.Height / m_ZoomF
        HDC_Dst = e.Graphics.GetHdc()
        HDC_Src = CreateCompatibleDC(HDC_Dst)
        hOrg = SelectObject(HDC_Src, m_MyBmp.GetHbitmap())
        StretchBlt(HDC_Dst, 0, 0, PictureBox1.ClientSize.Width, PictureBox1.ClientSize.Height, HDC_Src, HScrollBar1.Value, VScrollBar1.Value, w, h, SRCCOPY)
        hNew = SelectObject(HDC_Src, hOrg)
        DeleteObject(hNew)
        DeleteDC(HDC_Src)
        e.Graphics.ReleaseHdc(HDC_Dst)
End Sub

しかし、性能アップは果たせませんでした。DrawImageメソッドとそれほど変わらない
感じです。
タスクマネージャでスクロールさせながらCPU使用率を見ると私のプログラムでは
すぐに90%とかにアップするのに、Windowsアクセサリのペイントで同じ画像の
スクロール時は10%以下です。
ペイント並みの描画はどのようにして実現されているのでしょうか。
.NETのコントロールを使ってお手軽にプログラミングするのは無理なのでしょうか。
アドバイス頂ければ幸いです。
PictureBox の SizeMode を AutoSize にして、その親コントロールとして AutoScroll を true にした Panel を追加すればいいんじゃないですか?
■No23849に返信(Hongliangさんの記事)
> PictureBox の SizeMode を AutoSize にして、その親コントロールとして AutoScroll を true にした Panel を追加すればいいんじゃないですか?

Hongliang様
コメントありがとうございます。
実は最初に作ってみたのはご指摘の方式です。コーディングは超簡単でしたが、
スクロールバーをドラッグ中に画像が更新されず、ドラッグを止めた
位置で描画になってしまいます。
Windowsアクセサリのペイントのようにドラッグ中に画像が描き換わるように
したかったのです。
そこで、別途ScrollBarを配置してそのValueChangedイベントで
PictureBox1.Refresh()してPictureBoxのPaintイベントにて
スクロール位置に応じて画像更新してます。
よろしくお願いします。
2009/01/23(Fri) 15:08:08 編集(投稿者)

> スクロールバーをドラッグ中に画像が更新されず、ドラッグを止めた
> 位置で描画になってしまいます。
あれ、私の環境だと随時更新されたんですけど。

ScrollBar を使うなら、PictureBox を Panel の子にして、ScrollBar の Scroll で PictureBox の (Left|Top) を操作するとか。
■No23851に返信(Hongliangさんの記事)
> 2009/01/23(Fri) 15:08:08 編集(投稿者)
> 
>>スクロールバーをドラッグ中に画像が更新されず、ドラッグを止めた
>>位置で描画になってしまいます。
> あれ、私の環境だと随時更新されたんですけど。
> 

私が最初に作ったのはフォームにPanelを貼り付けて、その上にPictureBoxを
貼り付けて、ボタンをフォームにおいてコードは下記の通りです。

Public Class Form1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        PictureBox1.Load("test01.bmp")
    End Sub

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Panel1.AutoScroll = True
        PictureBox1.SizeMode = PictureBoxSizeMode.AutoSize
    End Sub
End Class

私の環境ではスクロールバーの■のドラッグ中は画像は元のままでドラッグを
やめたら1回更新されるだけです。
何か根本的なことを忘れてるのでしょうか。
私のPC(XPHome)で試したところ、
 画面のプロパティ−デザイン−効果−ドラッグ中にウィンドウの内容を表示する
のチェックを外すと、ひで46さんと同様、スクロールバーのつまみをドラッグ中は画像が更新されなくなりました。

ひで46さんのPCの設定を確認してみてください。

# もし外れていたらすみません。
■No23854に返信(H.K.R.さんの記事)
> 私のPC(XPHome)で試したところ、
>  画面のプロパティ−デザイン−効果−ドラッグ中にウィンドウの内容を表示する
> のチェックを外すと、ひで46さんと同様、スクロールバーのつまみをドラッグ中は画像が更新されなくなりました。
> 
> ひで46さんのPCの設定を確認してみてください。
> 
> # もし外れていたらすみません。

H.K.R. さま、情報ありがとうございます。
おっしゃるように画面のプロパティ−デザイン−効果−
「ドラッグ中にウィンドウの内容を表示する」のチェックを
入れたら更新されるようになりました。
ただ、速度は速くなりませんでした。
英語サイトも見なくてはと、検索して
A picture viewer class that can scroll and zoom using API
http://www.codeproject.com/KB/miscctrl/Picture_Viewer.aspx?display=PrintAll
というのがヒットしました。
ダウンロードしたら高速にスクロールしてます。
応用できるか分かりませんが、英語ですが詳しい説明が書いてあるので、
調べてみたいと思います。
2009/01/26(Mon) 19:43:17 編集(投稿者)

■No23844に返信(ひで46さんの記事)
> ところが巨大な画像ファイル(例えば16Kx4K)を開いたとき、パフォーマンスが
> 急激に落ちます。

等倍表示のみ(変数 m_ZoomF を無視)にはなりますが、ここの過去ログで、
http://dobon.qp.land.to/bbs/cbbs.cgi?mode=one&namber=962&type=944&space=105&no=0
というコードがあったので、これを真似て TextureBrush で描画したところ、
16,060×4,100の 256色 Bitmap に対しても、
8,192×8192の Jpeg (http://agora.ex.nii.ac.jp/digital-typhoon/wallpaper/) に
対しても、比較的スムーズにスクロールされました。


Sub Form1_Load(…) Handles Me.Load
 m_MyBmp = New Bitmap(FileName)

 'スクロールバーの設定コード
 VScrollBar1.Maximum = m_MyBmp.Height - PictureBox1.Height
 HScrollBar1.Maximum = m_MyBmp.Width - PictureBox1.Width
 VScrollBar1.LargeChange = VScrollBar1.Maximum \ 20
 HScrollBar1.LargeChange = HScrollBar1.Maximum \ 20

 m_Texture = New TextureBrush(m_MyBmp, Drawing2D.WrapMode.Clamp)
End Sub

Sub PictureBox1_Paint(…) Handles PictureBox1.Paint
 'スクロール位置情報を取得するコード
 Dim iHS As Integer = HScrollBar1.Value
 Dim iVS As Integer = VScrollBar1.Value
 Text = "{" & String.Format("{0},{1}", iHS, iVS) & "}"

 'スクロールした分だけずらして描画
 Texture.ResetTransform()
 Texture.TranslateTransform(-H, -V)
 e.Graphics.FillRectangle(Texture, PictureBox1.ClientRectangle)
End Sub

Sub Form1_FormClosed(…) Handles Me.FormClosed
 Hide()
 If m_Texture IsNot Nothing Then m_Texture.Dispose()
 If m_MyBmp IsNot Nothing Then m_MyBmp.Dispose()
End Sub

Sub Redraw(…) Handles VScrollBar1.Scroll, HScrollBar1.Scroll
 PictureBox1.Invalidate()
End Sub

Sub Redraw(…) Handles Me.Resize, VScrollBar1.ValueChanged, HScrollBar1.ValueChanged
 PictureBox1.Invalidate()
End Sub
魔界の仮面弁士様

情報ありがとうございました。
TextureBrushというのは全く知りませんでした。
Paintイベントは下記のコードでズーミング機能を含めて十分高速な
プログラムができました。TextureBrush描画すごいです!!。
(なぜ、速くなるのか理解できていません)

'--------------------------------
'   PictureBox1のPaintイベント
'
Private Sub PictureBox1_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
       
        Dim rectD As New Rectangle(0, 0, PictureBox1.Width, PictureBox1.Height)
 
        If m_MyBmp Is Nothing Then
            Exit Sub
        End If
  
        m_Texture.ResetTransform()
        m_Texture.TranslateTransform(-HScrollBar1.Value, -VScrollBar1.Value)
        With e.Graphics
            .ScaleTransform(m_ZoomF, m_ZoomF)
            .FillRectangle(m_Texture, PictureBox1.ClientRectangle)
        End With
End Sub

1点問題がありまして、拡大表示したときに、スムージングが掛かった画像になって
しまいます。DrawImageで描画時は下記の2つのMode設定で拡大時にドットが単純に
大きくなった画像が得られていました。
今回、TextureBrushに変えたら、どこに何を設定すれば良いのか分かりません。

With e.Graphics
        .PixelOffsetMode = Drawing2D.PixelOffsetMode.Half
        .InterpolationMode = Drawing2D.InterpolationMode.NearestNeighbor
        .DrawImage(m_MyBmp, rectD, iHS, iVS, w, h, GraphicsUnit.Pixel)
End With

アドバイス頂ければ幸いです。
2009/01/27(Tue) 15:27:03 編集(投稿者)

■No23868に返信(ひで46さんの記事)
> 今回、TextureBrushに変えたら、どこに何を設定すれば良いのか分かりません。
対応していないようですね。
http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=162069

スクロール中は TextureBrush で高速描画し、スクロール完了後、
DrawImage を使って再描画するというのはどうでしょう。

# 参考資料
# http://download.microsoft.com/download/6/9/c/69c09c8a-810b-46c8-b61e-79c4339ea47e/T5-407.ppt


Private highspeed As Boolean = True

Sub Application_Idle(ByVal sender As Object, ByVal e As EventArgs)
 If highspeed Then
  highspeed = False
  PictureBox1.Invalidate()
 End If
End Sub

Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint
  :
 If highspeed Then
  'Graphics.FillRectangle(TextureBrush, Rectangle)
 Else
  'Graphics.DrawImage(Bitmap, Rectangle, Rectangle, GraphicsUnit)
 End If
End Sub

Sub Form1_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
 RemoveHandler Application.Idle, AddressOf Application_Idle
  :
End Sub

Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
 AddHandler Application.Idle, AddressOf Application_Idle
  :
End Sub

Sub RedrawByScroll(sender As Object, e As ScrollEventArgs) Handles VScrollBar1.Scroll, HScrollBar1.Scroll
 highspeed = True
 PictureBox1.Invalidate()
End Sub

Sub RedrawByChanged(sender As Object, e As EventArgs) Handles Me.Resize, VScrollBar1.ValueChanged, HScrollBar1.ValueChanged
 highspeed = True
 PictureBox1.Invalidate()
End Sub
■No23870に返信(魔界の仮面弁士さんの記事)

魔界の仮面弁士様、ありがとうございます。
お示しいただいたコードを参考に望みの動作ができました。
今回は皆さんの書き込みで色々新しいことを知るきっかけができました。
ご教授ありがとうございました。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板