Accessing Graphics property of Canvas Deprecated - updating old Code

I am returning to code that I used many years ago to draw “stuff” in a canvas. I extensively used the Graphics property of the Canvas to do what I was trying to do.
Now I want to do things “right” and use the Paint event of the Canvas. But I am not sure about the sensible strategy for the transition.
The old code had a custom canvas control. There are about 30 methods of this canvas control. They basically allow me to draw graphs with curves and lines and dots axis etc. A basic pattern might be that I would pass some array of X and Y points to one of the methods and it would know how to draw the line defined by those points in the custom canvas controls. That code would depend on Graphics.DrawLine. So I would be doing what is now deprecated.

One idea is to create a Picture property for this custom canvas MyPicturePropertyOfCanvas that is the same size as the canvas. Then I would use all the methods that I have already written and change them to write to the Graphics of the Picture property and then send the result back to the canvas with a g.DrawPicture so it becomes visible. It seems a little round-about when i am used to my current pattern. But is this the sensible way to make the transition?

So basically I would have one line of code in the Canvas.Paint event. g.DrawPicture(MyPicturePropertyOfCanvas,0,0)

All my methods would be transformed to draw to the Graphics property of the MyPicturePropertyOfCanvas. But otherwise, would not change.

And then when I wanted to display the result, I would arrange for the Canvas.Paint event to fire off.

Is there a better approach? Is this idea stupid?

This will work fine.
It means that you only need to draw the big picture when you know the data has changed.
And only copy to the visible canvas when a Paint() happens.

Oddly, I have heard that (depending upon the complexity of the drawing), it may actually be faster to do all the drawing inside the paint event. I havent trialled that, but the idea is that doing a big copy of millions of pixels may be slower than drawing a few lines etc.

If you elect to go the ‘draw in the paint event’ then instead of coding

g = thecustomcanvas.graphics

g.drawstuff

You just put all the

g.drawstuff 

Inside the canvas paint event

Hi Robert

The solutions are mainly as you described or like Jeff said, just draw in the paint event.

If your drawing is very complex and doesn’t change much then using a picture might be the best way (try to use invalidate to trigger the redraw).
If your drawing is fast or changes often (i.e.: an animation) then directly in the paint event might be more efficient.

I think drawing directly in the paint event might be classed as faster as you are saving on the final draw picture step (and possibly the picture creation).

The general strategy is to keep the information ready to recreate the entire canvas contents at any moment.

For example, in the old code if you had a method like:

Sub SetCaption(Caption As String) Self.Graphics.ClearRect(0, 0, Self.Width, Self.Height) Self.Graphics.DrawString(Caption, 10, 20) End Sub

Your updated code my look something like

[code]Private mCaption As String

Sub SetCaption(Caption As String)
Self.mCaption = Caption
Self.Invalidate
End Sub

Event Paint(G As Graphics, Areas() As REALbasic.Rect)
G.DrawString(Self.mCaption, 10, 20)
End Event[/code]

Of course, as you’ve said, you can draw to a picture object and paint that instead which will keep your code the most similar but you will very likely take a performance hit. It also makes high resolution graphics much more complicated, and on macOS, prevents subpixel rendering. It’s the easier solution, but not necessarily the best solution. Only you can decide which route is appropriate.

I am outlining an approach that I am taking to this conversion to help anyone facing this particular issue, to encourage more elegant solutions, and invite criticism of my decision. My thanks to previous contributors to this thread. This is more complex than just doing all the drawing to a Picture Object but has the advantage of creating slightly cleaner graphics and possibly being faster. (See McGrath above)

Converting custom Canvas from old (directly address Graphics property of canvas) to the new (use Canvas Paint Event).

The original (old) project had ~ 30 methods that were part of the custom canvas (cnGraph). These methods instructed various “things” to be drawn on the canvas. For illustrative purposes, consider a simplified example: (a customCanvas with two methods). cnGraph has these two methods

Public DrawBlueLineOld() graphics.ForeColor=RGB(0,0,255) graphics.DrawLine(70,70,80,200) graphics.ForeColor=RGB(0,0,0) End Sub

Public DrawRedLineOld() graphics.ForeColor=RGB(255,0,0) graphics.DrawLine(0,0,70,70) graphics.ForeColor=RGB(0,0,0) End Sub

Imagine a window on which we have a canvas (cnTest) which is an instance of the custom canvas (cnGraph). We have a button (OldBlue) and a button (OldRed). The action events are

Sub Action() Handles Action cnTest.DrawBlueLineOld End Sub

Sub Action() Handles Action cnTest.DrawRedLineOld End Sub

It works great but this is deprecated. So trying to move to the preferred way which is to have the code in the Paint event of the custom canvas. But how am I going to handle having ~ 30 methods to do different things all living in the Paint event? So I try this:

  1. Add an integer property to the custom canvas cnGraph named whichMethod
  2. Add two methods to the custom canvas cnGraph

Public Sub DrawBlueLineNew01(g As Graphics) g.ForeColor=RGB(0,0,255) g.DrawLine(70,70,80,200) g.ForeColor=RGB(0,0,0) End Sub

Public Sub DrawRedLineNew02() g.ForeColor=RGB(255,0,0) g.DrawLine(0,0,70,70) g.ForeColor=RGB(0,0,0) End Sub
3.Add a Paint event handler to the custom canvas cnGraph. So the paint event is just going to be a Case/Select directing the Paint event to the desired method.

Select Case whichMethod Case 1 Me.DrawBlueLineNew01 Case 2 Me.DrawRedLineNew02 End Select

Make two new buttons (NewBlue) and a button (NewRed). The action events are

Sub Action() Handles Action cnTest.whichMethod = 1 // draw blue line Self.cnTest.Invalidate // force Paint event End Sub

Sub Action() Handles Action cnTest.whichMethod = 2 // draw red line Self.cnTest.Invalidate // force Paint event End Sub
Well, this is a little more complex. We have to associate each method with an integer so the Select/Case in the Paint Event will fire off the “correct” method. But it works. We push the NewBlue button and a blue line appears. We push the NewRed button and a red line appears.

New Problem: Persistance. With the old way of directly addressing the graphics property of the canvas, we would get a blue line and then a red line and both would be visible. With the new way, when I push the NewRed button the existing blue line disappears. So somehow, we have to “remember” all the graphic methods that have been called on the canvas so when the canvas is “redrawn” all these elements are redrawn.
So lets create a property that is a stack of all the graphic methods that have been called. An property that is an integer array and we’ll call it aiDrawStack. This will “remember” all the methods that have been applied. Change the Paint event handler to run through all of these. And change the code in the NewBlue and NewRed buttons slightly.

[code]Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint
For nIndex As Integer = 0 To aiDrawStack.Ubound

Select Case aiDrawStack(nIndex)
Case 1
  Me.DrawBlueLineNew01(g)
Case 2
  Me.DrawRedLineNew02(g)
End Select

Next nIndex
End Sub[/code]

Sub Action() Handles Action Self.cnTest.aiDrawStack.Append(1) // add a blue line to the stack (Graphic Method 1) Self.cnTest.Invalidate // force Paint event End Sub

Sub Action() Handles Action Self.cnTest.aiDrawStack.Append(2) // add a red line to the stack (Graphic Method 2) Self.cnTest.Invalidate // force Paint event End Sub

New Problem: Parameters
That is all well and fine for these very simplistic Graphic Methods. But what if those Methods require parameters? We are going to have to figure out a way to “pass” those parameters and “remember” them. This gets a little convoluted. I have tried this approach.
Create a new class called ParameterStore. This has three properties and could have more if the need arises. What I personally have to keep track of is integer, double and string parameters in the ~ 30 graphic methods that I created for my custom canvas. Those three properties of ParameterStore are each arrays: dP(), iP(), sP() - array of double, array of integer, and array of string. In the custom canvas (cnGraph), create a new array property that will exist in parallel with the integer array property aiDrawStack(). I will call it aoPara(). Now when you call a Graph Method, you add its id number to the aiDrawStack() and any parameters to the aoPara().

So we will make a slightly more complex Graph Method to draw a green line with the endpoints of the line passed as parameters

Public Sub DrawGreenLineNew03(g As Graphics, X1 As Integer, Y1 As Integer, X2 As Integer, Y2 As Integer) g.ForeColor=RGB(0,255,0) // Draw a Green line g.DrawLine(X1,Y1,X2,Y2) // Parameters specify where g.ForeColor=RGB(0,0,0) End Sub

Change the Paint event code

[code]Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint
For nIndex As Integer = 0 To aiDrawStack.Ubound

Select Case aiDrawStack(nIndex)
Case 1  // blue line
  Me.DrawBlueLineNew01(g)
Case 2 // red line
  Me.DrawRedLineNew02(g)
Case 3 // green line
  Dim X1, Y1, X2, Y2 As Integer
  X1 = Self.aoPara(nIndex).iP(0)
  Y1 = Self.aoPara(nIndex).iP(1)
  X2 = Self.aoPara(nIndex).iP(2)
  Y2 = Self.aoPara(nIndex).iP(3)
  Me.DrawGreenLineNew03(g, X1, Y1, X2, Y2)
End Select

Next nIndex
End Sub[/code]

Change the code in the buttons that initiate the drawing

Sub Action() Handles Action Dim oPara As New ParameterStore Self.cnTest.aiDrawStack.Append(1) // add a blue line to the stack (Graphic Method 1) Self.cnTest.aoPara.Append(oPara) // have to append even if there are no parameters to keep two arrays in sync Self.cnTest.Invalidate // force Paint event End Sub

Sub Action() Handles Action Dim oPara As New ParameterStore Self.cnTest.aiDrawStack.Append(2) // add a red line to the stack (Graphic Method 2) Self.cnTest.aoPara.Append(oPara) // have to append even if there are no parameters to keep two arrays in sync Self.cnTest.Invalidate // force Paint event End Sub

[code]Sub Action() Handles Action
Dim whichDraw As Integer = 3 // means green line
Dim oPara As New ParameterStore // 70, 70, 200, 220 are the parameters we want to pass
oPara.iP.Append(70)
oPara.iP.Append(70)
oPara.iP.Append(200)
oPara.iP.Append(220)

Self.cnTest.aiDrawStack.Append(whichDraw) // whichDraw is 3 which means green
Self.cnTest.aoPara.Append(oPara)

Self.cnTest.Invalidate // force Paint event
End Sub[/code]

Finally, to “clear” the graphic you need only empty the aiDrawStack and aoPara arrays
A button the clear the graphic would contain the following code

Sub Action() Handles Action ReDim Self.cnTest.aiDrawStack(-1) ReDim Self.cnTest.aoPara(-1) Self.cnTest.Invalidate // force Paint event End Sub

And now that you have this “memory” of all the graphic methods you have applied to the canvas, you can do cute things like removing just a particular line by removing its representation in the aiDrawStack and aoPara arrays. You could paint on top of or under elements by changing the “order” of these parallel arrays.

This is undoubtedly more complex because the code is keeping track of more things and when I create a new Graphic Method for my custom canvas, I have to assign it an integer and add it to the Case/Select of the PaintEvent.

You might look into Object 2D objects
They scale nice
Are vector based
etc

Basically all the same kinds of things you’ve invented

Norm, thanks.
I can (do) use Object2D code in some of the ~30 Graphic Methods that are part of cnGraph. But those methods, when chosen, still end up being part of the aiDrawStack so as I would understand it Object2D does not fundamentally change the general organization described above. I still have to have a Case/Select in my Paint Event containing many methods and those include the methods that incorporate Object2D.

What I’m suggesting is that instead of passing a"code" to know what to draw + params in a separate object you just use object2d directly
You’ve basically reinvented Object2d

I would leave all the methods in place and just change them to update the internal stack, or create object2d’s. That way the rest of your code can remain unchanged, you’re just changing the internals of the canvas subclass. And if for whatever reason you decide not to use object2d, do create your own object and have a single array/stack of that object. Encapsulate all the logic inside that class.

I might be confused. Do you have a property of the canvas subclass that is an array of Object2Ds? So the Paint Event just works its way down that stack? And your methods are just making additions to that stack when called?

For Example: ao2D_Stack is just an array of Object2Ds that is a property of the canvas subclass. Any of the ~ 30 graphic methods of this class just add an appropriate object to this array.

And then in the Paint Event

[code]Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint
For nIndex As Integer = 0 To ao2D_Stack.Ubound

g.DrawObject(ao2D_Stack(nIndex))

Next nIndex
End Sub[/code]

Is that the direction you are pushing me?

I suspect the confusion is that while Norm is saying Object2D, he may be referring to Group2D

A Group2D object is a collection of Object2D objects (rather than an array)

If you draw the Group2D, all the child objects are drawn ‘by’ it.
But you can scale , transpose, or rotate the entire group, which makes it useful for drawing a map (for example).
Zooming in is literally change the scale and refresh the canvas.

Ahhh! That would make sense to me.

Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint g.DrawObject(g2d_Draw) End Sub

You create g2d_Draw as a Group2D property of the custom canvas.
The various methods that you create for the custom canvas act by adding Object2D’s to this group.
And the Paint event remains very simple.

If that is the direction I should be going, I think I can see a light that way.

Either way is good. An array of object2d might provide more flexibility in manipulating individual objects, but a group2d provides greater simplicity.