none
Help with graphic techniques RRS feed

  • Question

  • Hi I am writing an app and am relatively new to vb.net but have been working with it for a while. I want to know what you think is the best way to achieve the following types of graphic things I want to work with. Please understand I don't want to use existing apps etc but prefer to do my own things. What is the best and least complicated way of drawing lines at particular points say from A to B?? Placing characters at any point on the screen I want it to be?? Keeping track of the length of strings I work with etc. I know there are so many ways of working with graphics but would like to hear it from people that are familiar with the types of graphics. Hopefully I have made it clear about what I am looking for but if you have additional questions please feel free to ask me.

    THank you in advance,

    Les

    Tuesday, March 27, 2018 4:01 AM

Answers

  • What is the best and least complicated way of drawing lines at particular points say from A to B?? Placing characters at any point on the screen I want it to be?? Keeping track of the length of strings I work with etc.

    Use the Paint event to draw the lines on the graphics object that is provided in the arguments to that event.   Doing it like that avoids all the issues associated with obtaining and persisting the graphics object.  The drawing that you do in the paint event might be low-level (individual lines, circles and points) or it might simply be displaying an image that has been prepared earlier, and which might or might not get updated throughout the life of the application.   

    It probably seems there are many ways to do graphics because people have come up with all sorts of inventive methods of dealing with the problems they run into if they don't do the drawing like that.   But they are all workarounds.  See:  https://vbdotnetblog.wordpress.com/graphics/graphics-methodology/

    If you are drawing a series of graphical elements then the usual procedure is to create a collection of objects and iterate over those objects in the paint event, drawing each one using the information in the object.  For instance, if you are drawing lines then you might create a collection of objects where each one includes start position, end position, width and colour - or whatever other information you need to draw the graphic.  When something gets updated you alter an existing item in the collection, or add or delete an item in the collection, and call the Invalidate method.  Everything in the collection then gets drawn in the Paint event as that code works through the collection of objects.

    • Marked as answer by Les2011 Friday, March 30, 2018 4:52 PM
    Tuesday, March 27, 2018 4:43 AM
  • Hi Les,

    Acamar and Tommy have provided some good general advice.  One thing I would note is that while the Paint event is the most common place to perform most general graphics operations, it is not the be-all-end-all of drawing on a control.  There is also the BufferedGraphicsManager, which is what the control is using under-the-hood.  You can create your own BufferedGraphics instance for a control and use it to perform all of the drawing without necessarily interacting with the Paint event (depending on what code is telling the BufferedGrahpics instance to render).

    While the basics of drawing an object to a control surface are straight-forward, there can be some subtle complications such as scrolling the drawing surface within the control, or handling scaling and rotation of objects.

    To handle scrolling you generally need to define a "viewport" which defines the visible area of the drawing.  The world-space for the drawing (let's call it a "canvas") is infinite.  The viewport serves as a window into the canvas.  The viewport's size will be the same as the control and its location will indicate the scroll position.

    Scaling and rotation are handled through instances of the Matrix class, however, there are different ways to go about it depending on what you are trying to achieve and determining the correct series of operations for a desired result can be a bit tricky, especially when you first start using the class.

    To get you started, and help with some of these initial hurdles, I've created the following example.  This code represents a very simplistic, yet highly functional, design and can be used as the basis for more complex solutions.

    First you'll want to define a "RenderObject" which represents any general object that can be displayed on the canvas.  This object will need to store information about position, scaling and rotation (properties that all rendered objects would have).  It also needs to provide the bounds for the object and the means to draw it to the canvas.  There are a number of ways to provide the RenderObject with means to draw itself and this example will demonstrate two; one is to have an overridable method that an inherited class can override to provide its own code and the other is through delegates (a third would be through Events which, while not shown, would be easy to add).

    RenderObject

    'define an object which can be rendered to the canvas
    Public Class RenderObject
        'the center position of the object
        Public Property Position As PointF
        'the code to perform the actual drawing of the object
        Public Property RenderAction As Action(Of Graphics, Rectangle)
        'the object's rotation in degrees
        Public Property Rotation As Single
        'the object's horizontal and vertical scaling factor
        Public Property Scale As New SizeF(1, 1)
        'the object's original size
        Public Property Size As SizeF
        'the object's depth or drawing order
        Public Property ZOrder As Double
    
        'get the bounding region for the object in world-space (after scaling and rotation)
        Public Function GetWorldBounds() As Region
            'get the local bounds in a region
            Dim result = New Region(GetLocalBounds())
            'scale and rotate the region
            Using m As New Drawing2D.Matrix
                m.Translate(Position.X, Position.Y)
                m.Scale(Scale.Width, Scale.Height)
                m.Rotate(Rotation)
                m.Translate(-Position.X, -Position.Y)
                result.Transform(m)
            End Using
            Return result
        End Function
    
        'get the bounding rectangle for the object in local-space (before scaling and rotation)
        Public Function GetLocalBounds() As RectangleF
            Dim w = Size.Width
            Dim h = Size.Height
            'get a rectangle centered around the object's position
            Return New RectangleF(Position.X - w / 2, Position.Y - h / 2, w, h)
        End Function
    
        'perform the actual drawing in either an overload of this method or in the attached Action(Of T)
        Protected Overridable Sub OnRender(g As Graphics, bounds As Rectangle)
            RenderAction?.Invoke(g, bounds)
        End Sub
    
        'method called by the canvas to initiate drawing of the object
        Protected Friend Sub Render(g As Graphics, bounds As Rectangle)
            'move the drawing origin to the top-left of the drawing bounds
            g.TranslateTransform(bounds.Left, bounds.Top)
            'move the drawing origin to the object's position
            g.TranslateTransform(Position.X, Position.Y)
            'scale the canvas to the object's scale
            g.ScaleTransform(Scale.Width, Scale.Height)
            'rotate the canvas to the object's rotation
            g.RotateTransform(Rotation)
            'move the drawing origin back to the top-left of the bounds
            g.TranslateTransform(-Position.X, -Position.Y)
            'call the drawing code
            OnRender(g, bounds)
            'reset the graphics transform
            g.ResetTransform()
        End Sub
    End Class

    With the RenderObject defined we can then create the RenderCanvas control.  For this example I'm using a single code file inheriting from Control.  In practice you would normally add a new Custom Control to your project and the template would create a couple of code files along with the supporting code for the component model.  You can use any base control class (eg UserControl) that you want, but generally the basic "Custom Control" template will suffice.  To reiterate, this example control lacks the component model code for brevity.

    Also note that the control uses overrides of the "On[Event]" methods of the class instead of handling the events.  These methods are what raise the events in the control class so we can execute our custom code in the method that raises the event rather than through the extra layer of event handlers.

    RenderCanvas

    Public Class RenderCanvas
        Inherits Control
    
        'declare a list to hold all of the objects to draw
        Public ReadOnly Property RenderObjects As New List(Of RenderObject)
        'declare a varaible to hold the currently selected object
        Public ReadOnly Property SelectedObject As RenderObject
    
        'track the mouse movement distance
        Private mouseDelta As Point
        'define the view port for the canvas
        Private viewPort As Rectangle
    
        Public Sub New()
            'set the control to double-buffer to eliminate flicker during rapid redraws
            DoubleBuffered = True
        End Sub
    
        Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
            MyBase.OnMouseDown(e)
            'store the mouse location
            mouseDelta = e.Location
            'get the topmost object under the mouse pointer, if any
            _SelectedObject = (From r In RenderObjects Where r.GetWorldBounds().IsVisible(e.Location - viewPort.Location) Order By r.ZOrder Descending).FirstOrDefault
            'redraw the canvas
            Invalidate()
        End Sub
    
        Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
            MyBase.OnMouseMove(e)
            'use mouse to drag objects and scroll the view port
            If e.Button = MouseButtons.Right Then
                'if right-click and drag, move view port by mouse move distance
                viewPort.Location += e.Location - mouseDelta
            ElseIf e.Button = MouseButtons.Left AndAlso _SelectedObject IsNot Nothing Then
                'if left-click and drag, move the selected object by mouse move distance
                _SelectedObject.Position += e.Location - mouseDelta
            End If
            'store the new mouse position
            mouseDelta = e.Location
            'redraw the canvas
            Invalidate()
        End Sub
    
        Protected Overrides Sub OnPaint(e As PaintEventArgs)
            MyBase.OnPaint(e)
            'clear the canvas to the control's back color
            e.Graphics.Clear(BackColor)
            'set drawing quality options as desired
            e.Graphics.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
            e.Graphics.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic
            e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
            'loop through each render object starting with the lowest ZOrder value
            For Each renderObj In (From r In RenderObjects Order By r.ZOrder Select r)
                'get the object's world bounds
                Dim bounds = renderObj.GetWorldBounds()
                'adjust for the view port location
                bounds.Translate(viewPort.Left, viewPort.Top)
                'get rectangle around world bounds to test against view port rectangle
                Dim r = Rectangle.Truncate(bounds.GetBounds(e.Graphics))
                'ensure the object is within the view port (ensure the object is visible on-screen)
                If viewPort.IntersectsWith(r) Then
                    'tell the objec to draw itself to the canvas
                    renderObj.Render(e.Graphics, viewPort)
                    'if this is the selected object, draw a highlight over it
                    If renderObj Is SelectedObject Then
                        Using brsh As New SolidBrush(Color.FromArgb(128, SystemColors.Highlight))
                            e.Graphics.FillRegion(brsh, bounds)
                        End Using
                    End If
                End If
            Next
        End Sub
    
        Protected Overrides Sub OnSizeChanged(e As EventArgs)
            MyBase.OnSizeChanged(e)
            'when the canvas control's size changes, update the size of the view port
            viewPort.Width = ClientSize.Width
            viewPort.Height = ClientSize.Height
            'redraw the canvas
            Invalidate()
        End Sub
    End Class

    The RenderCanvas will draw all of the RenderObjects added to it, will let you drag objects with left click, and will let you scroll the canvas with right-click and drag.

    Here is an example form using the control along with three examples of using a RenderObject (shapes, text, and images).

    Example Form

    Public Class Form1
        'create the RenderCanvas instance
        Friend WithEvents Canvas As New RenderCanvas() With {.Dock = DockStyle.Fill}
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            'add the canvas to the form
            Controls.Add(Canvas)
    
            'create an example of a shape object
            Dim shapeObj As New RenderObject With {
                .Size = New Size(50, 50),
                .Position = New PointF(Width / 2, 50)
            }
            shapeObj.RenderAction = Sub(g As Graphics, bounds As Rectangle)
                                        g.FillRectangle(Brushes.RoyalBlue, shapeObj.GetLocalBounds)
                                    End Sub
    
            'create an example of a text object
            Dim textObj As New RenderObject With {
                .Size = New Size(100, 24),
                .Position = New PointF(Width / 2, Height / 2)
            }
            Dim txtFormat As New StringFormat With {
                .Alignment = StringAlignment.Center,
                .LineAlignment = StringAlignment.Center
            }
            textObj.RenderAction = Sub(g As Graphics, bounds As Rectangle)
                                       Dim txtbounds = textObj.GetLocalBounds
                                       g.DrawString("This is a test", Font, SystemBrushes.WindowText, txtbounds, txtFormat)
                                   End Sub
    
    
            '<unrelated code to get an image from the web>
            Dim client As New Net.WebClient
            Dim picturestream As New IO.MemoryStream(client.DownloadData("http://www.pngpix.com/wp-content/uploads/2016/02/Dog-PNG-Image-1.png"))
            client.Dispose()
            Dim picture As Bitmap = Bitmap.FromStream(picturestream)
            '</unrelated code>
    
            'create an example of an image object
            Dim imageObj As New RenderObject With {
                .Size = New Size(240, 240 * (picture.Height / picture.Width))
            }
            imageObj.Position = New PointF(Width / 2, Height - imageObj.Size.Height)
            imageObj.RenderAction = Sub(g As Graphics, bounds As Rectangle)
                                        Dim imgbounds = Rectangle.Truncate(imageObj.GetLocalBounds)
                                        g.DrawImage(picture, imgbounds, 0, 0, picture.Width, picture.Height, GraphicsUnit.Pixel)
                                    End Sub
    
            'add example objects to the canvas
            Canvas.RenderObjects.Add(shapeObj)
            Canvas.RenderObjects.Add(textObj)
            Canvas.RenderObjects.Add(imageObj)
        End Sub
    
        'demonstrate rotation, scaling and zorder
        Private Sub Canvas_KeyUp(sender As Object, e As KeyEventArgs) Handles Canvas.KeyUp
            If Canvas.SelectedObject Is Nothing Then Exit Sub
            Select Case e.KeyCode
                'rotate selected object clockwise
                Case Keys.D
                    Canvas.SelectedObject.Rotation += 5
                    If Canvas.SelectedObject.Rotation = 360 Then Canvas.SelectedObject.Rotation = 0
    
                'rotate selected object counter-clockwise
                Case Keys.A
                    Canvas.SelectedObject.Rotation -= 5
                    If Canvas.SelectedObject.Rotation = -5 Then Canvas.SelectedObject.Rotation = 355
    
                'scale selected object up
                Case Keys.W
                    Canvas.SelectedObject.Scale += New SizeF(0.1, 0.1)
    
                'scale selected object down
                Case Keys.S
                    Canvas.SelectedObject.Scale -= New SizeF(0.1, 0.1)
    
                'move selected object down the z-order (toward bottom)
                Case Keys.Q
                    Canvas.SelectedObject.ZOrder -= 1
    
                'move selected object up the z-order (toward top)
                Case Keys.E
                    Canvas.SelectedObject.ZOrder += 1
            End Select
    
            'invalidate the canvas to show the changes
            Canvas.Invalidate()
        End Sub
    End Class

    This is one of the most simplistic designs for a program which only needs to process changes to the graphics based on user input.  That is to say, the displayed graphics only change when the user performs some action.  If you needed to modify the graphics on a regular basis, based on program execution rather than user interaction (e.g. animated or otherwise "active" render objects) then you would likely use a slightly different design due to continuous rendering of the canvas.

    Note that while you can define RenderObjects in a general fashion as shown in the example, you can also create derived custom classes that can be used repeatedly.  For example, a TextRenderObject for strings might look like:

    Example Custom RenderObject

    Public Class TextRenderObject
        Inherits RenderObject
    
        Public Property Color As Color = SystemColors.WindowText
        Public Property Font As New Font("Arial", 11.0!)
        Public Property Text As String
    
        Private txtFormat As New StringFormat With {
                .Alignment = StringAlignment.Center,
                .LineAlignment = StringAlignment.Center
            }
    
        Protected Overrides Sub OnRender(g As Graphics, bounds As Rectangle)
            MyBase.OnRender(g, bounds)
            Using brsh As New SolidBrush(Color)
                g.DrawString(Text, Font, brsh, GetLocalBounds(), txtFormat)
            End Using
        End Sub
    End Class

    Hopefully this gives you a foundation to build from.  You can paste this code as-is into a new project to test it out.  But as mentioned, when you create your own project you should add a new Custom Control to the project to generate the component model for the control and then paste in the class contents from the example.

    If you have any questions about the example feel free to ask.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    • Marked as answer by Les2011 Friday, March 30, 2018 4:51 PM
    Thursday, March 29, 2018 1:26 PM
    Moderator
  • Drawing on the screen is different than drawing on a control...

    Drawing to the screen can be done by creating a Graphics instance with a zero pointer.  GDI draws to a graphics context.  A graphics context can be made over a control surface, the screen, a printer, or anything else someone might dream up and build a context over.  It doesn't matter what the context is, using GDI is the same.

    TextRenderer can measure text and works better than graphics measuring text. It can be used with Graphics to draw text also...

    TextRenderer can be useful, but if you measure text with TextRenderer then you must also draw it with TextRender.  You cannot measure with one and draw with the other.  As for which is "better" it depends on what you are doing.  If you are making your own multiline textbox then TextRenderer would be useful; if you just displaying some short text with other graphics then TextRenderer may not be necessary.

    If you need to keep the length of strings you work with then it's probable you would need a List(Of T) that has properties for the string...

    No matter what you draw you are likely to need additional information.  That is the point of the "RenderObject" class.  Both Acamar and I have already explained that the objects to draw go into a collection.

    Buffered graphics would not be necessary ... but some or most [controls] probably do not have a double buffered property  ...

    The DoubleBuffered property is a member of Control, so all controls have it.  When you set the property to true the control uses the BufferedGraphicsManager behind the scenes. In most cases you'll want doublebuffering on a control.  Even if the code does not repaint often, resizing the form could induce flicker.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    • Marked as answer by Les2011 Friday, March 30, 2018 4:52 PM
    Friday, March 30, 2018 12:27 PM
    Moderator

All replies

  • What is the best and least complicated way of drawing lines at particular points say from A to B?? Placing characters at any point on the screen I want it to be?? Keeping track of the length of strings I work with etc.

    Use the Paint event to draw the lines on the graphics object that is provided in the arguments to that event.   Doing it like that avoids all the issues associated with obtaining and persisting the graphics object.  The drawing that you do in the paint event might be low-level (individual lines, circles and points) or it might simply be displaying an image that has been prepared earlier, and which might or might not get updated throughout the life of the application.   

    It probably seems there are many ways to do graphics because people have come up with all sorts of inventive methods of dealing with the problems they run into if they don't do the drawing like that.   But they are all workarounds.  See:  https://vbdotnetblog.wordpress.com/graphics/graphics-methodology/

    If you are drawing a series of graphical elements then the usual procedure is to create a collection of objects and iterate over those objects in the paint event, drawing each one using the information in the object.  For instance, if you are drawing lines then you might create a collection of objects where each one includes start position, end position, width and colour - or whatever other information you need to draw the graphic.  When something gets updated you alter an existing item in the collection, or add or delete an item in the collection, and call the Invalidate method.  Everything in the collection then gets drawn in the Paint event as that code works through the collection of objects.

    • Marked as answer by Les2011 Friday, March 30, 2018 4:52 PM
    Tuesday, March 27, 2018 4:43 AM
  • Hi Acamar,

    I thank you for your quick response.  I will take a look at your ideas and link and see how things go.  I will provide feedback to you or the link sometime over the weekend when I can play with it. thx

    Tuesday, March 27, 2018 4:51 AM
  • Exactly the best way depends on what you are drawing and your programming skills.

    It also depends on what you are drawing and how fast it needs to be drawn.

    The basic techniques Acamar gives will do for most drawing. Then you can add more complicated data structures and drawing methods depending on what you are doing. But drawing in the paint event is basically the same for all unless you have special needs that require something else.

    Here is a discussion you may enjoy:

    https://social.msdn.microsoft.com/Forums/vstudio/en-US/ffa3f84c-e459-48e3-96a6-92d758c0ee82/what-should-a-class-for-drawing-graphics-look-like?forum=vbgeneral

    Tuesday, March 27, 2018 9:36 AM
  • As for doing things like drawing lines, there are functions that exist that usually do everything we need. So it is better to not try to do your own version of those, at least not in the beginning.

    Also, you can do all the drawing in the paint event as has been said. Beginners tend to think that is a waste of time but it is the way that Windows works and you will have problems if you ignore it. However it is possible to create a bitmap outside the paint event and then just show the bitmap in the paint event. Options like that are possible.

    Another issue is updating graphics. You don't mention that but there will be a time when you will want to do that. There are techniques that can make it easy to remove parts of drawings. For example if you want to change the size and/or location of a box then you need to first remove the previous box. I am unfamiliar with the details but you should find articles and/or books that help with that.



    Sam Hobbs
    SimpleSamples.Info

    Tuesday, March 27, 2018 8:37 PM
  • Hi Sam

    Good thought, I do in fact, I think, want to be able to use scroll bars in the event a long equation I generate is to long for the object it is drawn on.  I remember reading that there are now certain objects you can draw on that will automatically engage the scroll bars when needed.

    Thank you for your thoughts

    Les

    Tuesday, March 27, 2018 10:16 PM
  • I am sure there are multiple solutions when scrolling is appropriate but one of them would be to use a control for showing an image and then to generate the image as I say.


    Sam Hobbs
    SimpleSamples.Info

    Tuesday, March 27, 2018 10:23 PM
  • Hi Les,

    Acamar and Tommy have provided some good general advice.  One thing I would note is that while the Paint event is the most common place to perform most general graphics operations, it is not the be-all-end-all of drawing on a control.  There is also the BufferedGraphicsManager, which is what the control is using under-the-hood.  You can create your own BufferedGraphics instance for a control and use it to perform all of the drawing without necessarily interacting with the Paint event (depending on what code is telling the BufferedGrahpics instance to render).

    While the basics of drawing an object to a control surface are straight-forward, there can be some subtle complications such as scrolling the drawing surface within the control, or handling scaling and rotation of objects.

    To handle scrolling you generally need to define a "viewport" which defines the visible area of the drawing.  The world-space for the drawing (let's call it a "canvas") is infinite.  The viewport serves as a window into the canvas.  The viewport's size will be the same as the control and its location will indicate the scroll position.

    Scaling and rotation are handled through instances of the Matrix class, however, there are different ways to go about it depending on what you are trying to achieve and determining the correct series of operations for a desired result can be a bit tricky, especially when you first start using the class.

    To get you started, and help with some of these initial hurdles, I've created the following example.  This code represents a very simplistic, yet highly functional, design and can be used as the basis for more complex solutions.

    First you'll want to define a "RenderObject" which represents any general object that can be displayed on the canvas.  This object will need to store information about position, scaling and rotation (properties that all rendered objects would have).  It also needs to provide the bounds for the object and the means to draw it to the canvas.  There are a number of ways to provide the RenderObject with means to draw itself and this example will demonstrate two; one is to have an overridable method that an inherited class can override to provide its own code and the other is through delegates (a third would be through Events which, while not shown, would be easy to add).

    RenderObject

    'define an object which can be rendered to the canvas
    Public Class RenderObject
        'the center position of the object
        Public Property Position As PointF
        'the code to perform the actual drawing of the object
        Public Property RenderAction As Action(Of Graphics, Rectangle)
        'the object's rotation in degrees
        Public Property Rotation As Single
        'the object's horizontal and vertical scaling factor
        Public Property Scale As New SizeF(1, 1)
        'the object's original size
        Public Property Size As SizeF
        'the object's depth or drawing order
        Public Property ZOrder As Double
    
        'get the bounding region for the object in world-space (after scaling and rotation)
        Public Function GetWorldBounds() As Region
            'get the local bounds in a region
            Dim result = New Region(GetLocalBounds())
            'scale and rotate the region
            Using m As New Drawing2D.Matrix
                m.Translate(Position.X, Position.Y)
                m.Scale(Scale.Width, Scale.Height)
                m.Rotate(Rotation)
                m.Translate(-Position.X, -Position.Y)
                result.Transform(m)
            End Using
            Return result
        End Function
    
        'get the bounding rectangle for the object in local-space (before scaling and rotation)
        Public Function GetLocalBounds() As RectangleF
            Dim w = Size.Width
            Dim h = Size.Height
            'get a rectangle centered around the object's position
            Return New RectangleF(Position.X - w / 2, Position.Y - h / 2, w, h)
        End Function
    
        'perform the actual drawing in either an overload of this method or in the attached Action(Of T)
        Protected Overridable Sub OnRender(g As Graphics, bounds As Rectangle)
            RenderAction?.Invoke(g, bounds)
        End Sub
    
        'method called by the canvas to initiate drawing of the object
        Protected Friend Sub Render(g As Graphics, bounds As Rectangle)
            'move the drawing origin to the top-left of the drawing bounds
            g.TranslateTransform(bounds.Left, bounds.Top)
            'move the drawing origin to the object's position
            g.TranslateTransform(Position.X, Position.Y)
            'scale the canvas to the object's scale
            g.ScaleTransform(Scale.Width, Scale.Height)
            'rotate the canvas to the object's rotation
            g.RotateTransform(Rotation)
            'move the drawing origin back to the top-left of the bounds
            g.TranslateTransform(-Position.X, -Position.Y)
            'call the drawing code
            OnRender(g, bounds)
            'reset the graphics transform
            g.ResetTransform()
        End Sub
    End Class

    With the RenderObject defined we can then create the RenderCanvas control.  For this example I'm using a single code file inheriting from Control.  In practice you would normally add a new Custom Control to your project and the template would create a couple of code files along with the supporting code for the component model.  You can use any base control class (eg UserControl) that you want, but generally the basic "Custom Control" template will suffice.  To reiterate, this example control lacks the component model code for brevity.

    Also note that the control uses overrides of the "On[Event]" methods of the class instead of handling the events.  These methods are what raise the events in the control class so we can execute our custom code in the method that raises the event rather than through the extra layer of event handlers.

    RenderCanvas

    Public Class RenderCanvas
        Inherits Control
    
        'declare a list to hold all of the objects to draw
        Public ReadOnly Property RenderObjects As New List(Of RenderObject)
        'declare a varaible to hold the currently selected object
        Public ReadOnly Property SelectedObject As RenderObject
    
        'track the mouse movement distance
        Private mouseDelta As Point
        'define the view port for the canvas
        Private viewPort As Rectangle
    
        Public Sub New()
            'set the control to double-buffer to eliminate flicker during rapid redraws
            DoubleBuffered = True
        End Sub
    
        Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
            MyBase.OnMouseDown(e)
            'store the mouse location
            mouseDelta = e.Location
            'get the topmost object under the mouse pointer, if any
            _SelectedObject = (From r In RenderObjects Where r.GetWorldBounds().IsVisible(e.Location - viewPort.Location) Order By r.ZOrder Descending).FirstOrDefault
            'redraw the canvas
            Invalidate()
        End Sub
    
        Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
            MyBase.OnMouseMove(e)
            'use mouse to drag objects and scroll the view port
            If e.Button = MouseButtons.Right Then
                'if right-click and drag, move view port by mouse move distance
                viewPort.Location += e.Location - mouseDelta
            ElseIf e.Button = MouseButtons.Left AndAlso _SelectedObject IsNot Nothing Then
                'if left-click and drag, move the selected object by mouse move distance
                _SelectedObject.Position += e.Location - mouseDelta
            End If
            'store the new mouse position
            mouseDelta = e.Location
            'redraw the canvas
            Invalidate()
        End Sub
    
        Protected Overrides Sub OnPaint(e As PaintEventArgs)
            MyBase.OnPaint(e)
            'clear the canvas to the control's back color
            e.Graphics.Clear(BackColor)
            'set drawing quality options as desired
            e.Graphics.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
            e.Graphics.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic
            e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
            'loop through each render object starting with the lowest ZOrder value
            For Each renderObj In (From r In RenderObjects Order By r.ZOrder Select r)
                'get the object's world bounds
                Dim bounds = renderObj.GetWorldBounds()
                'adjust for the view port location
                bounds.Translate(viewPort.Left, viewPort.Top)
                'get rectangle around world bounds to test against view port rectangle
                Dim r = Rectangle.Truncate(bounds.GetBounds(e.Graphics))
                'ensure the object is within the view port (ensure the object is visible on-screen)
                If viewPort.IntersectsWith(r) Then
                    'tell the objec to draw itself to the canvas
                    renderObj.Render(e.Graphics, viewPort)
                    'if this is the selected object, draw a highlight over it
                    If renderObj Is SelectedObject Then
                        Using brsh As New SolidBrush(Color.FromArgb(128, SystemColors.Highlight))
                            e.Graphics.FillRegion(brsh, bounds)
                        End Using
                    End If
                End If
            Next
        End Sub
    
        Protected Overrides Sub OnSizeChanged(e As EventArgs)
            MyBase.OnSizeChanged(e)
            'when the canvas control's size changes, update the size of the view port
            viewPort.Width = ClientSize.Width
            viewPort.Height = ClientSize.Height
            'redraw the canvas
            Invalidate()
        End Sub
    End Class

    The RenderCanvas will draw all of the RenderObjects added to it, will let you drag objects with left click, and will let you scroll the canvas with right-click and drag.

    Here is an example form using the control along with three examples of using a RenderObject (shapes, text, and images).

    Example Form

    Public Class Form1
        'create the RenderCanvas instance
        Friend WithEvents Canvas As New RenderCanvas() With {.Dock = DockStyle.Fill}
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            'add the canvas to the form
            Controls.Add(Canvas)
    
            'create an example of a shape object
            Dim shapeObj As New RenderObject With {
                .Size = New Size(50, 50),
                .Position = New PointF(Width / 2, 50)
            }
            shapeObj.RenderAction = Sub(g As Graphics, bounds As Rectangle)
                                        g.FillRectangle(Brushes.RoyalBlue, shapeObj.GetLocalBounds)
                                    End Sub
    
            'create an example of a text object
            Dim textObj As New RenderObject With {
                .Size = New Size(100, 24),
                .Position = New PointF(Width / 2, Height / 2)
            }
            Dim txtFormat As New StringFormat With {
                .Alignment = StringAlignment.Center,
                .LineAlignment = StringAlignment.Center
            }
            textObj.RenderAction = Sub(g As Graphics, bounds As Rectangle)
                                       Dim txtbounds = textObj.GetLocalBounds
                                       g.DrawString("This is a test", Font, SystemBrushes.WindowText, txtbounds, txtFormat)
                                   End Sub
    
    
            '<unrelated code to get an image from the web>
            Dim client As New Net.WebClient
            Dim picturestream As New IO.MemoryStream(client.DownloadData("http://www.pngpix.com/wp-content/uploads/2016/02/Dog-PNG-Image-1.png"))
            client.Dispose()
            Dim picture As Bitmap = Bitmap.FromStream(picturestream)
            '</unrelated code>
    
            'create an example of an image object
            Dim imageObj As New RenderObject With {
                .Size = New Size(240, 240 * (picture.Height / picture.Width))
            }
            imageObj.Position = New PointF(Width / 2, Height - imageObj.Size.Height)
            imageObj.RenderAction = Sub(g As Graphics, bounds As Rectangle)
                                        Dim imgbounds = Rectangle.Truncate(imageObj.GetLocalBounds)
                                        g.DrawImage(picture, imgbounds, 0, 0, picture.Width, picture.Height, GraphicsUnit.Pixel)
                                    End Sub
    
            'add example objects to the canvas
            Canvas.RenderObjects.Add(shapeObj)
            Canvas.RenderObjects.Add(textObj)
            Canvas.RenderObjects.Add(imageObj)
        End Sub
    
        'demonstrate rotation, scaling and zorder
        Private Sub Canvas_KeyUp(sender As Object, e As KeyEventArgs) Handles Canvas.KeyUp
            If Canvas.SelectedObject Is Nothing Then Exit Sub
            Select Case e.KeyCode
                'rotate selected object clockwise
                Case Keys.D
                    Canvas.SelectedObject.Rotation += 5
                    If Canvas.SelectedObject.Rotation = 360 Then Canvas.SelectedObject.Rotation = 0
    
                'rotate selected object counter-clockwise
                Case Keys.A
                    Canvas.SelectedObject.Rotation -= 5
                    If Canvas.SelectedObject.Rotation = -5 Then Canvas.SelectedObject.Rotation = 355
    
                'scale selected object up
                Case Keys.W
                    Canvas.SelectedObject.Scale += New SizeF(0.1, 0.1)
    
                'scale selected object down
                Case Keys.S
                    Canvas.SelectedObject.Scale -= New SizeF(0.1, 0.1)
    
                'move selected object down the z-order (toward bottom)
                Case Keys.Q
                    Canvas.SelectedObject.ZOrder -= 1
    
                'move selected object up the z-order (toward top)
                Case Keys.E
                    Canvas.SelectedObject.ZOrder += 1
            End Select
    
            'invalidate the canvas to show the changes
            Canvas.Invalidate()
        End Sub
    End Class

    This is one of the most simplistic designs for a program which only needs to process changes to the graphics based on user input.  That is to say, the displayed graphics only change when the user performs some action.  If you needed to modify the graphics on a regular basis, based on program execution rather than user interaction (e.g. animated or otherwise "active" render objects) then you would likely use a slightly different design due to continuous rendering of the canvas.

    Note that while you can define RenderObjects in a general fashion as shown in the example, you can also create derived custom classes that can be used repeatedly.  For example, a TextRenderObject for strings might look like:

    Example Custom RenderObject

    Public Class TextRenderObject
        Inherits RenderObject
    
        Public Property Color As Color = SystemColors.WindowText
        Public Property Font As New Font("Arial", 11.0!)
        Public Property Text As String
    
        Private txtFormat As New StringFormat With {
                .Alignment = StringAlignment.Center,
                .LineAlignment = StringAlignment.Center
            }
    
        Protected Overrides Sub OnRender(g As Graphics, bounds As Rectangle)
            MyBase.OnRender(g, bounds)
            Using brsh As New SolidBrush(Color)
                g.DrawString(Text, Font, brsh, GetLocalBounds(), txtFormat)
            End Using
        End Sub
    End Class

    Hopefully this gives you a foundation to build from.  You can paste this code as-is into a new project to test it out.  But as mentioned, when you create your own project you should add a new Custom Control to the project to generate the component model for the control and then paste in the class contents from the example.

    If you have any questions about the example feel free to ask.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    • Marked as answer by Les2011 Friday, March 30, 2018 4:51 PM
    Thursday, March 29, 2018 1:26 PM
    Moderator
  • Drawing on the screen is different than drawing on a control. Drawing on the screen requires double buffered graphics and the use of some pinvoke functions in my experience (for clearing the screen of the drawing and releasing pointers if necessary). As well there is no paint event for the screen so you need to constantly draw in a timer. And updating the screen (refreshing the screen) causes all apps to refresh (redraw) so if constantly updating the drawing on the screen you will not like the effect.

    TextRenderer can measure text and works better than graphics measuring text. It can be used with Graphics to draw text also.

    https://msdn.microsoft.com/en-us/library/system.windows.forms.textrenderer(v=vs.110).aspx

    If you need to keep the length of strings you work with then it's probable you would need a List(Of T) that has properties for the string, the length of the string and height of the string or size of the string. Such that you can add and remove from the List(Of T) when necessary. Even then you may also need a location property for the string for drawing purposes.

    Buffered graphics would not be necessary if you are using a paint event to draw graphics on a Form or in a Control such as a PictureBox. Although a PictureBox is double buffered and a Form needs to have its double buffered property set although not required for painting on it. It all depends on how often the painting is changed (speed wise) as to whether the double buffered property needs to be set. Other controls such as a Panel can be drawn on also but some or most probably do not have a double buffered property so painting on them could become an issue if constant updates to the painting were required in say 50 ms time periods or less.


    La vida loca

    Friday, March 30, 2018 12:00 PM
  • Drawing on the screen is different than drawing on a control...

    Drawing to the screen can be done by creating a Graphics instance with a zero pointer.  GDI draws to a graphics context.  A graphics context can be made over a control surface, the screen, a printer, or anything else someone might dream up and build a context over.  It doesn't matter what the context is, using GDI is the same.

    TextRenderer can measure text and works better than graphics measuring text. It can be used with Graphics to draw text also...

    TextRenderer can be useful, but if you measure text with TextRenderer then you must also draw it with TextRender.  You cannot measure with one and draw with the other.  As for which is "better" it depends on what you are doing.  If you are making your own multiline textbox then TextRenderer would be useful; if you just displaying some short text with other graphics then TextRenderer may not be necessary.

    If you need to keep the length of strings you work with then it's probable you would need a List(Of T) that has properties for the string...

    No matter what you draw you are likely to need additional information.  That is the point of the "RenderObject" class.  Both Acamar and I have already explained that the objects to draw go into a collection.

    Buffered graphics would not be necessary ... but some or most [controls] probably do not have a double buffered property  ...

    The DoubleBuffered property is a member of Control, so all controls have it.  When you set the property to true the control uses the BufferedGraphicsManager behind the scenes. In most cases you'll want doublebuffering on a control.  Even if the code does not repaint often, resizing the form could induce flicker.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    • Marked as answer by Les2011 Friday, March 30, 2018 4:52 PM
    Friday, March 30, 2018 12:27 PM
    Moderator
  • Drawing on the screen is different than drawing on a control...

    Drawing to the screen can be done by creating a Graphics instance with a zero pointer.  GDI draws to a graphics context.  A graphics context can be made over a control surface, the screen, a printer, or anything else someone might dream up and build a context over.  It doesn't matter what the context is, using GDI is the same.

    TextRenderer can measure text and works better than graphics measuring text. It can be used with Graphics to draw text also...

    TextRenderer can be useful, but if you measure text with TextRenderer then you must also draw it with TextRender.  You cannot measure with one and draw with the other.  As for which is "better" it depends on what you are doing.  If you are making your own multiline textbox then TextRenderer would be useful; if you just displaying some short text with other graphics then TextRenderer may not be necessary.

    If you need to keep the length of strings you work with then it's probable you would need a List(Of T) that has properties for the string...

    No matter what you draw you are likely to need additional information.  That is the point of the "RenderObject" class.  Both Acamar and I have already explained that the objects to draw go into a collection.

    Buffered graphics would not be necessary ... but some or most [controls] probably do not have a double buffered property  ...

    The DoubleBuffered property is a member of Control, so all controls have it.  When you set the property to true the control uses the BufferedGraphicsManager behind the scenes. In most cases you'll want doublebuffering on a control.  Even if the code does not repaint often, resizing the form could induce flicker.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    I guess you told me huh? LOL. Now try refreshing the screen. What does that require? I won't bother with the rest of your test(y) Reed. And I'm not even a consultant or MCC (anymore) or MVP, 76,095 to 65,238 must really rally the cause. ;)

    BTW have you voted IronRazer and tommytwotrain as MVP's yet? After all Razerz is already a moderator at Dream In Code (DIC) and tommytwotrain is by far excellent with Graphics and the Chart Control which I remember was like a red headed step child in this forum for years although the Chart Control Forum had no real assistance but this Forum always did except for those that told peeps to go elsewhere to get no help.

    These two peeps should be MVP's regardless of their point levels. But hey who rules the roost? Apparently nobody that watches who's really who. Or cares apparently.


    La vida loca



    Saturday, March 31, 2018 2:26 AM