October 18, 2012

Transparent picturebox overlay in winforms

I stumbled into an annoying problem when doing some graphics coding in a windows forms environment. The task seemed rather simple: Have a panel with a background image, drag images -in my case as listview items- onto that panel where a picturebox is shown to display the dragged image. When done, merge the picturebox images with the background to store the final picture as a single image.

My problem

When I drag an image (listview item) onto the panel a picturebox is created at the dragged location and the image is displayed. However, troubles arise when two pictureboxes overlap. Here's the catch: Setting transparency on your picturebox will make the background reflect the picturebox control background!
So the result lookst like this:
(I colored the second picturebox transparent area a little 'red-ish' to show the problem!)

Problem Solved?

I did some research on the net, but to no avail. So, I improvised:
  1. We know the picturebox transparent area will reflect the picturebox parent image/foreground
  2. I do need the complete picture anyway
  3. There's no "free painting" involved, just 'reorganizing' pictures
  4. Speed is not an issue, since by my (educated) guess, in my app there will be dragged less than well... lets say 50 images to the panel.
  5. Considering point 4, I just might 'get away' with creating the final picture every time a picturebox is created/dragged/transformed...
  6. Control tags can contain Objects (other controls!)

 Problem solved!

Now consider this layout:
  1. A panel containing the background image (background)
  2. A Panel containing the imaged where background and picturebox images are merged (canvas) and the parent of my pictureboxes
  3. The pictureboxes dragged onto the canvas

The trick now is to 'reconstruct' the panel canvas background image every tima a picturebox is changed/added.
  • We first create the background panel with the (clean) background image (pnlBackground).
  • Next we create the canvas panel where pnlCanvas.Tag = pnlBackground. The canvas is a child control of the background!
  • Every time a new picturebox is added, the picturbox.Tag = pnlBackground. The pictureboxes are children of the canvas!
Do you see where I'm going here?
Now on every event changing the pictureboxes on the canvas panel we call a function to reconstruct the canvas panel background image. Events like dragdrop on the canvas panel (adding a new picturebox) and events of the picturebox itself (dragging, rotating, etc)

The merging function should accept a picturebox and a panel (overload it). When called with a picturebox we have:
Panel pnlCanvas = sender.Tag as Panel (sender == picturebox)
Panel pnlBackground = pnlCanvas.Tag as Panel

Now, take the pnlBackground image and merge each picturebox image with it. The resulting image is your new pnlCanvas image.

The end result will look like this:

Each picturebox transparent area reflects the parent control foreground, which is the merged picture of pnlCanvas! Since the pictureboxes are still there (overlaying the pnlCanvas) they are still available for editing by the user (dragging, rotating, etc)

 There's lots of code available about merging images, so no code in this post. Just the basic idea on how I solved the problem. It might not be the best solution, but it works for me. As mentioned above: My 'canvas' will not contain over 50 pictureboxes so speed is not an issue to me, but 'reconstructing' (merging) the canvas picture will cost you...

October 11, 2012

Custom Cursor Hotspot - HowTo...

When I recently implemented some drag and drop functionality I wanted to create a nice "drag cursor". A little different from the default. A quick "Google" showed me how to use a bitmap as a cursor.

My Problem

When creating a cursor from a bitmap the new cursor hotspot is the center point of that cursor image.

Though I had a nice cursor now, using it posed a challenge for my users. People -rightfully- assume the "fingertip" to be the point where they are pointing at not the center of their cursor.

Problem solved?!

Another Google search brought me the solution. To create my custom cursor (assuming the image is one of my resources) I had this code:

 Cursor.Current = New Cursor(My.Resources.addCursor.GetHicon())  

To create a custom cursor with a custom hotspot I changed it to:

 Cursor.Current = FormHelper.CreateCursor(My.Resources.addCursor, New Point(5, 5))  

The voodoo happens in the CreateCursor -static- method. Though you're probably not interested, I copied the source anyway:

#Region "Create Custom Cursor" 
   Private Structure IconInfo  
     Public fIcon As Boolean  
     Public xHotspot As Int32  
     Public yHotspot As Int32  
     Public hbmMask As IntPtr  
     Public hbmColor As IntPtr  
   End Structure  

   <DllImport("user32.dll", EntryPoint:="CreateIconIndirect")> _  
   Private Shared Function CreateIconIndirect(ByVal iconInfo As IntPtr) As IntPtr  
   End Function  

   <DllImport("user32.dll", CharSet:=CharSet.Auto)> _  
   Public Shared Function DestroyIcon(ByVal handle As IntPtr) As Boolean  
   End Function  

   <DllImport("gdi32.dll")> _  
   Public Shared Function DeleteObject(ByVal hObject As IntPtr) As Boolean  
   End Function  

   ''' <summary>  
   ''' Create a custom winforms cursor with a designated hotspot  
   ''' </summary>  
   ''' <param name="bmp">Image to be used as cursor</param>  
   ''' <param name="hotspot">Hotspot location of the cursor</param>  
   ''' <returns></returns>  
   ''' <remarks>  
   ''' Requires imports:  
   ''' Imports System.Drawing  
   ''' Imports System.Runtime.InteropServices  
   ''' Imports System.Windows.Forms  
   Public Shared Function CreateCursor(ByVal bmp As Bitmap, ByVal hotspot As Point) As Cursor  
     'Setup the Cursors IconInfo  
     Dim tmp As New IconInfo  
     tmp.xHotspot = hotspot.X  
     tmp.yHotspot = hotspot.Y  
     tmp.fIcon = False  
     tmp.hbmMask = bmp.GetHbitmap()  
     tmp.hbmColor = bmp.GetHbitmap()  
     'Create the Pointer for the Cursor Icon  
     Dim pnt As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(tmp))  
     Marshal.StructureToPtr(tmp, pnt, True)  
     Dim curPtr As IntPtr = CreateIconIndirect(pnt)  
     'Clean Up  
     Return New Cursor(curPtr)  
   End Function  

#End Region  

Don't forget the required imports! (In case you it did interest you)