1
votes

Not exactly sure how to word this question, so I will go on to explain the details, and try to ask as best I can form it.

I have a project which consists of the following components

Canvas - Inherits PictureBox control

Layers - A collection of "Layer" Layer - Can contain a collection of Graphics as Images with information.

Each layer can be moved and the selection box for the layer is constrained to the portion of the layer which contains graphics at maximum bounds.

All of the above is working !!

What is not working, is when I want to save the combined result (including transparency, and alpha's), the Canvas control is empty. I know the images are being drawn in the box as it does not display anything until i do Canvas1.Invalidate() .

The code I have for the classes are as follows :

Canvas

    Imports System.Drawing
    Imports System.Drawing.Graphics

    Public Class Canvas
        Inherits PictureBox

        Private _MoveStart As Point
        Private _Layers As List(Of Layer)

        Public Sub New()
            Me.DoubleBuffered = True

            _Layers = New List(Of Layer)
        End Sub

        Public ReadOnly Property Layers() As List(Of Layer)
            Get
                Return _Layers
            End Get
        End Property

        Public Property SelectedLayer As Layer
            Get
                'Loop through all layers and return the one that is selected
                For Each l As Layer In Me.Layers
                    If l.Selected Then Return l
                Next
                Return Nothing
            End Get
            Set(ByVal value As Layer)
                'Loop through all layers and set their Selected property to True if it is the assigned layer ("value") or False if it isn't.
                For Each l As Layer In Me.Layers
                    l.Selected = (l Is value)
                Next
            End Set
        End Property

        Private Function GetLayerFromPoint(ByVal p As Point) As Layer
            ' Finds the layer that contains the point p
            For Each l As Layer In Me.Layers
                If l.Bounds.Contains(p) Then Return l
            Next
            Return Nothing
        End Function

        Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
            MyBase.OnMouseDown(e)

            If e.Button = Windows.Forms.MouseButtons.Left Then
                ' Store the previous selected layer to refresh the image there
                Dim oldSelection = Me.SelectedLayer

                ' Get the new selected layer
                Me.SelectedLayer = Me.GetLayerFromPoint(e.Location)

                'Update the picturebox
                If oldSelection IsNot Nothing Then Me.InvalidateLayer(oldSelection)
                Me.InvalidateLayer(Me.SelectedLayer)
                Me.Update()

                _MoveStart = e.Location
            End If
        End Sub

        Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
            MyBase.OnMouseMove(e)

            If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
                If Me.SelectedLayer IsNot Nothing Then

                    'Store the old bounds for refreshing
                    Dim oldBounds As Rectangle = Me.SelectedLayer.Bounds

                    'Move the selected layer
                    Me.SelectedLayer.Move(e.Location.X - _MoveStart.X, e.Location.Y - _MoveStart.Y)
                    _MoveStart = e.Location

                    'Update the picturebox
                    Me.InvalidateRectangle(oldBounds)
                    Me.InvalidateLayer(Me.SelectedLayer)
                    Me.Update()
                End If
            End If
        End Sub

        Private Sub InvalidateLayer(ByVal l As Layer)
            If l IsNot Nothing Then
                Me.InvalidateRectangle(l.Bounds)
            End If
        End Sub

        Private Sub InvalidateRectangle(ByVal r As Rectangle)
            'Inflate by 1 pixel otherwise the border isnt visible 
            r.Inflate(1, 1)
            Me.Invalidate(r)
        End Sub

        Protected Overrides Sub OnPaint(ByVal pe As System.Windows.Forms.PaintEventArgs)
            MyBase.OnPaint(pe)
            For Each l As Layer In Me.Layers
                l.Draw(pe.Graphics)
            Next
        End Sub

    End Class

Layer

    Imports System.Drawing
    Imports System.Drawing.Graphics

    Public Class Layer

        Private _Graphics As List(Of Graphic)
        Private _Name As String
        Private _Selected As Boolean

        Public Sub New(ByVal name As String)
            Me.Name = name
            Me.Selected = False
            Me.Graphics = New List(Of Graphic)
        End Sub

        Public Property Name() As String
            Get
                Return _Name
            End Get
            Set(ByVal value As String)
                _Name = value
            End Set
        End Property

        Public Property Selected() As Boolean
            Get
                Return _Selected
            End Get
            Set(ByVal value As Boolean)
                _Selected = value
            End Set
        End Property

        Public ReadOnly Property Bounds As Rectangle
            Get
                'Combine the bounds of all items
                If Me.Graphics.Count > 0 Then
                    Dim b = Me.Graphics(0).Bounds
                    For i As Integer = 1 To Me.Graphics.Count - 1
                        b = Rectangle.Union(b, Me.Graphics(i).Bounds)
                    Next
                    Return b
                End If
                Return Rectangle.Empty
            End Get
        End Property

        Public Property Graphics() As List(Of Graphic)
            Get
                Return _Graphics
            End Get
            Set(ByVal value As List(Of Graphic))
                _Graphics = value
            End Set
        End Property

        Public Sub Move(ByVal dx As Integer, ByVal dy As Integer)
            'Simply move each item 
            For Each item As Graphic In Me.Graphics
                item.Move(dx, dy)
            Next
        End Sub

        Public Sub Draw(ByVal g As System.Drawing.Graphics)
            'Draw each item
            For Each item As Graphic In Me.Graphics
                item.Draw(g)
            Next

            'Draw a selection border if selected
            If Me.Selected Then
                g.DrawRectangle(Pens.Red, Me.Bounds)
            End If
        End Sub

    End Class

Graphic

    Public Class Graphic
        Private _Image As Image
        Private _Location As Point
        Private _Size As Size

        Public Sub New(ByVal img As Image)
            Me.Bounds = Rectangle.Empty
            Me.Image = img
        End Sub

        Public Sub New(ByVal img As Image, ByVal location As Point)
            Me.New(img)
            Me.Location = location
            Me.Size = img.Size
        End Sub

        Public Sub New(ByVal img As Image, ByVal location As Point, ByVal size As Size)
            Me.New(img)
            Me.Location = location
            Me.Size = size
        End Sub

        Public Property Location() As Point
            Get
                Return _Location
            End Get
            Set(ByVal value As Point)
                _Location = value
            End Set
        End Property

        Public Property Size() As Size
            Get
                Return _Size
            End Get
            Set(ByVal value As Size)
                _Size = value
            End Set
        End Property

        Public Property Bounds() As Rectangle
            Get
                Return New Rectangle(Me.Location, Me.Size)
            End Get
            Set(ByVal value As Rectangle)
                Me.Location = value.Location
                Me.Size = value.Size
            End Set
        End Property

        Public Property Image() As Image
            Get
                Return _Image
            End Get
            Set(ByVal value As Image)
                _Image = value
            End Set
        End Property

        Public Sub Move(ByVal dx As Integer, ByVal dy As Integer)
            ' We need to store a copy of the Location, change that, and save it back,
            ' because a Point is a structure and thus a value-type!!
            Dim l = Me.Location
            l.Offset(dx, dy)
            Me.Location = l
        End Sub

        Public Sub Draw(ByVal g As Graphics)
            If Me.Image IsNot Nothing Then
                g.DrawImage(Me.Image, Me.Bounds)
            End If
        End Sub

    End Class

Example Usage

  • Create a new WinForms project (Form1)
  • Add a Canvas object to the form (will be named Canvas1)
  • Add a Button object to the form (will be named Button1)
  • Paste the following code into the Form1 source view

Form1

    Public Class Form1
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Me.DoubleBuffered = True
            Me.Show()

            Dim l As Layer

            l = New Layer("Layer 1")
            l.Graphics.Add(New Graphic(My.Resources.ActualSizeHS, New Point(10, 10)))
            Canvas1.Layers.Add(l)

            l = New Layer("Layer 2")
            l.Graphics.Add(New Graphic(My.Resources.AlignObjectsRightHS, New Point(320, 240)))
            l.Graphics.Add(New Graphic(My.Resources.AlignToGridHS, New Point(290, 140)))
            l.Graphics.Add(New Graphic(My.Resources.AlignObjectsBottomHS, New Point(320, 130)))
            Canvas1.Layers.Add(l)

            l = New Layer("Layer 3")
            l.Graphics.Add(New Graphic(My.Resources.AlignObjectsTopHS, New Point(520, 240)))
            l.Graphics.Add(New Graphic(My.Resources.AlignTableCellMiddleRightHS, New Point(390, 240)))
            l.Graphics.Add(New Graphic(My.Resources.AlignTableCellMiddleCenterHS, New Point(520, 130)))
            Canvas1.Layers.Add(l)

            Canvas1.Invalidate()
        End Sub

        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Canvas1.Image.Save("MyRenderedPicture.png", System.Drawing.Imaging.ImageFormat.Png)
        End Sub
    End Class

In the above example for Form1, replace My.Resources.* with wherever your graphics are located. This parameter is simply a System.Drawing.Image object.

The problem I am having, is when I click Button1 to save the image, the output does not contain any of the graphics that were added to the control. Please note that all of the graphics I am working with are PNG with fully transparent backgrounds, and dragging them around inside the container doesn't have the blocky effect of layering images using pictureboxes. Each image is true transparent. I wish to keep this level of transparency (and alpha blends if any exist), when i save the file -- but first ... i need to be able to save something other than a blank picturebox which clearly contains images.

Thanks in advance.

(image example of save where "shadows" are not rendering their opacity levels properly)

enter image description here

Now, if I do the following :

    Dim x As Integer = 0
    Using bmp As Bitmap = New Bitmap(Me.Width, Me.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)
        'Me.DrawToBitmap(bmp, New Rectangle(0, 0, bmp.Width, bmp.Height))
        For Each l As Layer In Me.Layers
            For Each g As Graphic In l.Graphics
                g.Image.Save("layer" & x & ".png")
                x = x + 1
            Next
        Next

        bmp.MakeTransparent(Me.BackColor)
        bmp.Save(FileName, Format)
        bmp.Dispose()
    End Using

Each layer is saved out properly -- individually. So the Graphics control is working as it should, it is when I combine them (and need to keep the position, and transparency), I think this is the routine I am looking for ---

How to merge System.Drawing.Graphics objects I am going to try and create a new Graphics object and try to "draw" onto it using the other graphics objects and their positions. Every example so far using clipping rectangles which will not do as that takes a pic of the stuff behind the Graphic which then needs to be made clear, etc etc.

2

2 Answers

1
votes

You do not assign an image to the picbox/canvas so Image is Nothing. After all, you are just using it as a canvas not an image holder. Since the helpers already know where they are, you just need to create a bitmap and draw the images/layers to it from the bottom up:

Public Function GetBitmap(format As System.Drawing.Imaging.ImageFormat) As Bitmap

    ' ToDo: add graphics settings
    Dim bmp As New Bitmap(Me.Width, Me.Height)

    Using g As Graphics = Graphics.FromImage(bmp)
        ' ToDo: draw Canvas BG / COlor to bmp to start
        ' for BMP, JPG / non Transparents
        For n As Integer = 0 To Layers.Count - 1
            Layers(n).Draw(g)
        Next

    End Using

    Return bmp

End Function

Then on the form:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    ' to do - add a literal SAve
    Using bmp As Bitmap = Canvas1.GetBitmap(Imaging.ImageFormat.Png)

        bmp.Save("C:\Temp\myImage.png", System.Drawing.Imaging.ImageFormat.Png)

    End Using

End Sub
0
votes

Are the PNGs ever being added to the Image of the Canvas? This isn't completely the same, so I apologize, but I recently made just a quick test app where I was trying to stack PNGs so thought I'd share what I did.

This loops through lstImages (which is actually a list of strings), loads the image into bmpTemp, and draws the image onto bmpBmp. That image is then used in the Image property of a PictureBox and added to the Controls collection of the form.

I just added another button to test saving and it worked fine with what I have below (after adding a name to the PictureBox).

Private Sub StackImages()

    Dim bmpBmp As New Bitmap(picStack.Width, picStack.Height)
    Dim graGraphic As Graphics = Graphics.FromImage(bmpBmp)

    For Each i As String In Me.lstImages
        Dim bmpTemp As New Bitmap(i)
        graGraphic.DrawImage(bmpTemp, 0, 0)
    Next

    Dim picTemp As New PictureBox
    picTemp.Top = picStack.Top
    picTemp.Left = picStack.Left
    picTemp.Width = picStack.Width
    picTemp.Height = picStack.Height
    picTemp.Image = bmpBmp
    picTemp.Name = "NewPictureBox"

    Me.Controls.Add(picTemp)
    picTemp.BringToFront()

End Sub

Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click

    Dim picNew As PictureBox = CType(Me.Controls.Item("NewPictureBox"), PictureBox)
    picNew.Image.Save("c:\temp\picTest.png")

End Sub