Painting/Drawing on Panel vs. PictureBox ("See it as you draw it")

Hi,

New to VB2005 and have run up against a wall, am wondering if I'm just missing a concept somewhere, would greatly appreciate any help/illumination.

THE TASK -- Graphing two functions, end result is two very curvy curves that intersect in many places. Ultimately want to manipulate the resulting picture (erase certain "intersection areas", etc.).

WHAT'S WORKING -- Currently using e.Graphics.DrawLine statements in the _paint section of a Panel. Works great, you get to "see it as you draw it" (very important), the new points & line segments appear on the screen as they're generated.

wHAT'S GIVING HEADACHES -- Discovered I'll need to use getpixel/setpixel to analyse & manipulate the resulting graph (unless there's a better way ), and that a Panel can't use getpixel/setpixel but a PictureBox can. But when I plug the same code into the _paint section of a picturebox, you lose the "see it as you draw it" quality, the picturebox is blank until it's finished graphing then the final result appears suddenly at the end.

THE BIG QUESTION -- Is there a Refresh/DoEvents/something way to get the picturebox to have the "see it as you draw it" quality as when drawing on a panel Or do I need to draw it "live" on the panel first, then do something like grab it with a screenshot (Me.DrawToBitmap ) in order to manipulate it

There's probaby a simple/elegant solution to this, but as a VB2005 newbie I can't seem to find it, am just grinding gears at the moment. Any ideas/tips would be a tremendous help. Thanks.



Answer this question

Painting/Drawing on Panel vs. PictureBox ("See it as you draw it")

  • hobbledskydiver

    You obviously didn't just have this piece of code in your back pocket jo0ls, this must have taken some time to do for the benefit of the OP. An exceptional piece of work, very clear, easy for a newbie like me to follow and understand.

    First class reply.

    Wibs

  • Himantura

    Very nice example jo0ls, thanks for posting this.
  • Gang Xiao

    I have a related problem.

    I am creating a usercontrol that displays a spectrum. I have been drawing on a PictureBox control. I've got it working pretty well and it all looks good, I can update the spectrum multiple times a second and the picturebox hardly flashes at all. Then I decided to add some keystrokes to the usercontrol and I find out that in VB 2005 they have changed it so the picturebox control doesn't have the Key events. They recommend using a Panel control instead...ok so no problem I change my usercontrol to draw on a panel control instead. Well I've found that the Panel control is very slow at displaying graphics and it flashes like crazy every time it is updated.

    So I found this post that you draw to a bitmap first then display the bitmap, so I try it out. I had reverted back to the picturebox control so I modified it to draw to a bitmap and again it worked great, it actually improved upon how I was doing. So I again convert my usercontrol to use the Panel control and find that it still flashes even when drawing to the bitmap.

    Are there any controls that I can draw on that display graphics as fast as the Picturebox control and except keystrokes Or is there a way to get the Picturebox control to except keystrokes ...Or is there a way to have the usercontrol keystroke events fire even if one of the internal controls have focus (I have 3 different picturebox controls on my usercontrol)

    -Rocks

  • vladMD

    Neat sample!!!

    james

    aka:Trucker


  • miimura

    Wow, excellent code sample (and really saved my bacon).

    Bigtime thanks, jo0ls.

    CA_VB2005er


  • abmeynaks

    Here is the example, which is huge...
    The Surface class is the clever part.
    It has a property to get the graphics for the underlying bitmap.
    Use that to draw, and then call refresh or invalidate to update the image.
    Then it has fast methods to get and set pixels. I'd recommend grabbing the whole array of pixels and modifying that rather than using the getpixel/setpixel methods as it will be faster.
    See the msdn library for bitmapdata, and bitmap.lockbits for more info (and http://www.bobpowell.net/lockingbits.htm ).

    ' 2005
    Imports System.Drawing
    Imports System.Drawing.Imaging
    Imports System.Runtime.InteropServices
     
    Public Class Form1
     
        Private canvas As Surface
        Private WithEvents t, t2 As Timer
        Private angle As Single
        Private interval As Single
        Private points As Generic.List(Of Point)
        Private radius As Single
        Private midX, midY As Single
        Private colorIndex As Integer
        Private colors() As Integer
     
        Sub New()
     
           ' This call is required by the Windows Form Designer.
           InitializeComponent()
     
           ' Add any initialization after the InitializeComponent() call.
           canvas = New Surface(Me.ClientSize.Width, Me.ClientSize.Height)
           Me.Controls.Add(canvas)
           t = New Timer
           t2 = New Timer
           t2.Interval = 50
           t.Interval = 50
           angle = 0
           points = New Generic.List(Of Point)
           interval = (2 * Math.PI) / 20   ' 20th of the way round a circle
           radius = IIf(canvas.Width > canvas.Height, canvas.Height / 2.5, canvas.Width / 2.5)
           midX = canvas.Width / 2
           midY = canvas.Height / 2
           colors = New Integer() {Color.Red.ToArgb, Color.Orange.ToArgb, Color.Yellow.ToArgb, _
           Color.Green.ToArgb, Color.Blue.ToArgb, Color.Indigo.ToArgb, Color.Violet.ToArgb}
           t.Start()
        End Sub
     
        Private Sub t_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles t.Tick
           ' calculate a point on a circle centred on the middle of the graph
           Dim p As New Point(midX + (Math.Cos(angle) * radius), midY + (Math.Sin(angle) * radius))
           points.Add(p)
           Dim p2 As Point = New Point(p.X - (radius / 20), p.Y - (radius / 20))
           Dim rec As New Rectangle(p2, New Size(radius / 10, radius / 10))
           canvas.Graphics.DrawEllipse(Pens.Navy, rec)
           ' rec needs to be a bit bigger for the invalidate call:
           rec = New Rectangle(rec.X - 1, rec.Y - 1, rec.Width + 2, rec.Height + 2)
           canvas.Invalidate(rec)
           angle += interval
           If angle > (Math.PI * 2) Then
               t.Stop()
               JoinCircles()
           End If
     
        End Sub
     
        Private Sub JoinCircles()
           For i As Integer = 0 To points.Count - 2
               For j As Integer = points.Count - 1 To i Step -1
                  ' join points
                  canvas.Graphics.DrawLine(Pens.Red, points(i), points(j))
               Next
           Next
           canvas.Refresh()
           t2.Start()
        End Sub
     
        Private Sub t2_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles t2.Tick
     
           Dim lastColor As Integer = colors(colorIndex)
           colorIndex += 1
           If colorIndex = colors.Length Then colorIndex = 0
           Dim newcolor As Integer = colors(colorIndex)
     
           ' lock the bitmap in the canvas, so the pixel array is refreshed
           canvas.LockImage()
           Dim pixels() As Integer = canvas.Pixels
           ' change all lastColor pixels to a new color
           ' things can slow here, so another thread is a good idea, then you can refresh when done.
           For i As Integer = 0 To pixels.Length - 1
               If pixels(i) = lastColor Then pixels(i) = newcolor
           Next
           ' send the array back
           canvas.Pixels = pixels
           ' update the image
           canvas.UpdateImage()
     
           canvas.Refresh()
        End Sub
     
        Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
           canvas.Dispose()
        End Sub
    End Class
     
    Friend Class Surface
        Inherits UserControl
     
        ' custom control to display a bitmap and offer fast getpixel/setpixel
     
        Private myPixels() As Integer
        Private bm As Bitmap
        Private g As Graphics
        Private locked As Boolean
     
        Sub New(ByVal width As Integer, ByVal height As Integer)
           Me.SuspendLayout()
           Me.Width = width
           Me.Height = height
           bm = New Bitmap(Me.Width, Me.Height)
           g = System.Drawing.Graphics.FromImage(bm)
           g.Clear(Color.White)
           Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)   ' windows won't erase the background
           Me.SetStyle(ControlStyles.OptimizedDoubleBuffer, True)  ' drawing will be automatically double buffered
           Me.SetStyle(ControlStyles.UserPaint, True)            ' we paint control surface
           Me.SetStyle(ControlStyles.Opaque, True)               ' we draw background
           Me.UpdateStyles()                                  ' commit changes
           Me.ResumeLayout()
        End Sub
     
        ' use to draw on the bitmap
        Public ReadOnly Property Graphics() As Graphics
           Get
               Return g
           End Get
        End Property
     
        Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
           ' this is often is msdn examples, but I'm not sure why!:
           Dim g As Graphics = e.Graphics
           ' Draw the bitmap if it exists.
           If bm IsNot Nothing Then
               g.DrawImageUnscaled(bm, New Point(0, 0))
           End If
        End Sub
     
        ' Lock the image, and transfer the pixels to the pixels array
        ' we don't want to d othis with each getpixel call!
        Public Sub LockImage()
           If locked Then Return
           If Me.Width = 0 Or Me.Height = 0 Then Return
           ' The bitmapdata object has properties to tell you about the bitmap locked in memory.
           Dim bmd As BitmapData = bm.LockBits(New Rectangle(0, 0, Me.Width, Me.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb)
           ' dimension the array to hold the pixels
           myPixels = New Integer(bm.Width * bm.Height - 1) {}
           ' copy the memory into the array
           ' scan0 is the location of the top left pixel. 
           Marshal.Copy(bmd.Scan0, myPixels, 0, myPixels.Length)
           ' unlock the bitmap
           bm.UnlockBits(bmd)
        End Sub
     
        ' GetPixel from x,y as an int32 - bytes are ARGB - alpha, red, green, blue
        ' dealing with ints is faster than dealing with system.drawing.color
        Public Function GetPixel(ByVal x As Integer, ByVal y As Integer) As Integer
           If myPixels IsNot Nothing AndAlso myPixels.Length > 0 Then
               ' position in the 1d array is:
               ' number of pixels in the rows above (x,y) which is height * number of pixels in a row
               ' PLUS number of pixels in the row leading up to (x,y)
               Return myPixels((y * bm.Width) + x)
           Else
               Throw New Exception("Pixels array is empty or nothing, call LockImage first")
           End If
     
        End Function
     
        ' Update the bitmap after altering the pixels array with setpixel, setpixelwithcolor or the pixels property
        Public Sub UpdateImage()
           ' get the bitmapdata
           Dim bmd As BitmapData = bm.LockBits(New Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb)
           ' copy the array
           Marshal.Copy(Pixels, 0, bmd.Scan0, Pixels.Length)
           ' unlock the bitmap
           bm.UnlockBits(bmd)
           ' refresh (similar to invalidate)
           Me.Refresh()
        End Sub
     
        ' getpixel as color
        Public Function GetPixelAsColor(ByVal x As Integer, ByVal y As Integer) As Color
           If myPixels IsNot Nothing AndAlso myPixels.Length > 0 Then
               Return Color.FromArgb(myPixels((y * bm.Width) + x))
           Else
               Throw New Exception("Pixels array is empty or nothing, call LockImage first")
           End If
        End Function
     
        ' just get the array...
        Public Property Pixels() As Integer()
           Get
               Return myPixels
           End Get
           Set(ByVal value As Integer())
               ' write modified array back. Call UpdateImage to copy the array to the bitmap
               myPixels = value
           End Set
        End Property
     
        ' setpixel as integer
        Public Sub SetPixel(ByVal x As Integer, ByVal y As Integer, ByVal color As Integer)
           Pixels((y * bm.Width) + x) = color
        End Sub
     
        ' setpixel as color
        Public Sub SetPixelWithColor(ByVal x As Integer, ByVal y As Integer, ByVal color As Color)
           Pixels((y * bm.Width) + x) = color.ToArgb
        End Sub
     
        ' remember to call this to free up resources
        Protected Overrides Sub Dispose(ByVal disposing As Boolean)
           If disposing Then
               bm.Dispose()
               g.Dispose()
           End If
           MyBase.Dispose(disposing)
        End Sub
     
    End Class


  • Trf_papa

    If you draw to a bitmap then it solves most problems.

    So, have a bitmap available throughout the class. Draw to it whereever you like. Then in the paint event, draw the bitmap to the surface (graphics.drawimageunscaled)
    To reduce the drawing time, use control.Invalidate(rectangle). This causes paint to fire, but it only draws the area specified by rectangle. If you don't do this then it will be very slow.

    The best option is to create a custom control to host the graph. I've got an example, just need to find it. It also provides a fast get/setpixel method - using bitmapdata objects and lockbits.


  • Painting/Drawing on Panel vs. PictureBox ("See it as you draw it")