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)
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.