Windows Forms controls in .NET do a lot more for us than they did in previous versions of VB. One of the best things about getting all the OO features .NET brought us, is the ability to extend controls through inheritance or some other means. Some controls directly support custom drawing of their visuals, while others need to be extended to do so.
One thing I have seen asked a few times, was how to make a given control translucent, which I like to think of a little differently than transparent.
So after some investigation, I came up with a method that works decently to do this for most standard WinForm controls you would want to do this with. The biggest issues I came across, were dealing with scrolling and the custom drawing, which I am still looking at for controls that scroll. However some controls do not, like a button, and that is what we are going to take a look at today.
The idea of translucent controls really revolves around the concept that your WinForm is going to have an image as its background. It also assumes this image is set to stretch so that the image takes up the entire form background. The code in this example can be tweaked if this is not your exact scenario, as this is more or less a proof of concept on doing things this way.
So first let me explain the concept we are going for by illustrating the end result:

You can see we have a standard button just to pair against the custom button. Then a custom button that looks the same because its IsTranslucent property (one of the custom properties we add to the button) is set to false. The two bottom buttons have IsTranslucent set to true, and the left button has the default gamma setting of 1.0, while the one on the right has a frosted effect by settings its gamma to 0.15.
So now lets talk about how we actually do this.
The first thing I did was identify the common elements to a translucent control. I found that (for now) they are:
- Background Source - where are we getting the background from to paint onto the button
- Gamma - value to indicate the over/under saturation of the image to give it a frosted or darkened effect
- IsTranslucent - indicates if the control should be translucent at all. When this is false, all the custom code is skipped, and the control behaves like a normal button
- Offset - Since controls often have borders and a 3D look, this property allows an offset of the painted image so it perfectly matches with the background of the form/container. Buttons I found work well with an offset of 5/5, while other controls needed no offset, because they appear flat already.
So because all controls that I want to make translucent will use these features, I created an interface that the controls can implement.
Lets define that interface now:
Public Interface ITranslucentControl
Enum eBackgroundImageSource
Form = 0
Container = 1
End Enum
Property Gamma() As Single
Property IsTranslucent() As Boolean
Property Offset() As System.Drawing.Point
Property BackgroundImageSource() As eBackgroundImageSource
End Interface
Pretty straight forward, it has all the things I described in the bullet points above. If you are not familiar with interfaces, then all you should really know, is that our custom button will implement this interface, which means it will have all these properties in itself, however the actual code in these properties is totally up to the class to implement. Most of these properties simply access backing fields in the class though. There is no real magic going on here, I just made the interface because I know all my custom translucent controls will use these items, but inheritance from a base class was not really an option because we need to inherit from the control we want to extend. So if you are wondering "Couldn't I do this without the interface?" the answer is yes, I just decided to use one so I wouldn't forget any of the common properties I want the control to have.
Ok, so now that we have our interface, we need to make our button class.
Imports System.Windows.Forms
Imports System.Drawing
Public Class TranslucentButton
Inherits System.Windows.Forms.Button
Implements ITranslucentControl
End Class
So our class name is TranslucentButton, and it inherits from button and implement our ITranslucentControl interface.
Lets add a constructor which sets double buffering to true. This can reduce flicker where possible, which is a common issue for WinForms and graphics.
#Region "CONSTRUCTOR(S)"
Public Sub New()
'SET DOUBLE BUFFERING TO REDUCE FLICKER WHERE POSSIBLE
MyBase.DoubleBuffered = True
End Sub
#End Region
You will note I make use of code regions to keep things separated and easy to manage in the code window.
Next we have all the public properties which came from the ITranslucentControl interface, along with backing private fields to maintain the state of the object. Interfaces can't define fields, so we need to manually do that in each one.
#Region "PUBLIC PROPERTIES AND ASSOCIATED PRIVATE FIELDS"
Private _gamma As Single = 1.0
''' <summary>
''' Gets or Sets the gamma correction to
''' use for the translucent image background
''' </summary>
''' <value>Single</value>
''' <returns>Value (as single) for the gamma correction</returns>
''' <remarks>None</remarks>
Public Property Gamma() As Single _
Implements ITranslucentControl.Gamma
Get
Return _gamma
End Get
Set(ByVal value As Single)
If value < 0.01 Then value = 0.01
_gamma = value
End Set
End Property
Private _isTranslucent As Boolean = False
''' <summary>
''' Indicates if control will be drawn translucent. False by default.
''' </summary>
''' <value>Boolean</value>
''' <returns>True if control is translucent, otherwise False.</returns>
''' <remarks>None</remarks>
Public Property IsTranslucent() As Boolean _
Implements ITranslucentControl.IsTranslucent
Get
Return _isTranslucent
End Get
Set(ByVal value As Boolean)
If value <> _isTranslucent AndAlso value = False Then
Me.BackgroundImage.Dispose()
Me.BackgroundImage = Nothing
End If
_isTranslucent = value
End Set
End Property
Private _Offset As New Point(5, 5)
''' <summary>
''' Provides an offset value incase the image is
''' not correctly aligning in this control.
''' This can occur when special using a 3d border,
''' or something similar that would cause an offset
''' </summary>
''' <value>Point. Default value is 5, 5 for this control</value>
''' <returns>
''' Current offset for this controls background drawing
'''</returns>
''' <remarks>None</remarks>
Public Property Offset() _
As Point _
Implements ITranslucentControl.Offset
Get
Return _Offset
End Get
Set(ByVal value As Point)
_Offset = value
End Set
End Property
Private _backgroundImageSource As _
ITranslucentControl.eBackgroundImageSource = _
ITranslucentControl.eBackgroundImageSource.Form
''' <summary>
''' Gets/Sets the source of the background image for this control
''' </summary>
''' <value>eBackgroundImageSource enumeration value</value>
''' <returns>
''' eBackgroundImageSource.Form when the forms
''' background should be used, eBackgroundImageSource.Parent
''' when the forms immediate parent should be used
''' </returns>
''' <remarks>
''' This setting is ignored when
''' IsTranslucent is False
''' </remarks>
Public Property BackgroundImageSource() As _
ITranslucentControl.eBackgroundImageSource _
Implements ITranslucentControl.BackgroundImageSource
Get
Return _backgroundImageSource
End Get
Set(ByVal value As ITranslucentControl.eBackgroundImageSource)
_backgroundImageSource = value
End Set
End Property
#End Region
This looks like a lot of code, but all it is, is each of the implemented properties from our interface, along with a backing field. There is a decent amount of comments too. You could add some validation to the properties if you wanted, lets say, to limit the gamma value.
We use one private field here that is not part of the interface, and it is also not exposed via a public property. It is called _bypassPainting and it is just a boolean flag we set to skip the paint event of the control from firing twice, which would affect performance.
#Region "PRIVATE FIELDS"
'FLAG USED TO BYPASS REPAINTING AFTER THE BACKGROUND PICTURE IS SET
Private _bypassPainting As Boolean = False
#End Region
So that is the entire class, with the exception of the paint event which does all the hard work. Time to dig into that one and see where the "magic" happens.
So the magic that happens, is actually that the button looks at its parent control, which may be a form, or it may be another container, such as a groupbox or panel. You may want to have a button in a panel, but still have it use the forms background image for its translucency. Or maybe you do actually want the panels background image to be the image we see through the button. This is why we have the BackgroundImageSource property, which if you remember is an enum value of Form or Container. This value does not matter at all if the button is directly on the form to begin with. It only comes into play when the button is in an additional container.
The button takes the image should be seen through the button, and calculates which part of this image the button is actually covering. It then takes that region of the image, and creates a new image from that, and uses that as the buttons background image. This gives the button its transparent look, but still gives it all the standard features of a button. From there you can set gamma to make the image in the button lighter or darker if needed. I made the default offset of the button 5,5 which takes into account the buttons 3D appearance. This can be customized in the event the images are not matching perfectly between the parent image and the button image.
I won't go into too much more detail about the paint event, as it is commented pretty well, and if you follow it along, should be self explanatory.
#Region "CUSTOM PAINTING ROUTINE"
Protected Overrides Sub OnPaint( _
ByVal pevent As System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(pevent)
'IF BYPASSPAINTING FLAG WAS SET, RESET IT AND DO NOT PAINT
If _bypassPainting Then
_bypassPainting = False
Return
End If
'DON'T DO ANY CUSTOM PAINTING IF THE FEATURE IS DISABLED
If Not _isTranslucent Then Return
'GET THE CONTROL THAT THE IMAGE
'IS ON WE WANT TO USE FOR TRANSLUCENCY
Dim parentControl As Control = Nothing
If _backgroundImageSource = _
ITranslucentControl.eBackgroundImageSource.Form Then
'GET FORM
parentControl = Me.FindForm
Else
'GET PARENT (WILL BE FORM IF NO CONTAINER EXISTS)
parentControl = Me.Parent
End If
'IF THE PARENT CONTROL WE ARE REFERENCING
'HAS NO BACKGROUND IMAGE, DO NOTHING
'ALSO CHECK TO MAKE SURE STRETCH IS THE LAYOUT TYPE
If parentControl.BackgroundImage Is Nothing Then Return
If parentControl.BackgroundImageLayout <> _
ImageLayout.Stretch Then Return
'SOURCE RECTANGLE IS THE CLIPPED REGION
'OF THE FORMS BACKGROUND IMAGE
'THAT IS BEHIND THE TABPAGE. CLIPPING ALLOWS
'US TO THEN PASTE THE COVERED PART
'OF THE FORM BACKGROUND IMAGE ONTO
'THE LISTVIEW BACKGROUND TO MIMIC TRANSPARENCY
Dim srcRect As Rectangle = Nothing
If _backgroundImageSource = _
ITranslucentControl.eBackgroundImageSource.Form Then
srcRect = New Rectangle(GetPointFromForm(Me), Me.Size)
Else
srcRect = Me.Bounds
End If
'IMAGE WITH THE CURRENT CLIENT SIZE OF THE BACKGROUND
Dim mySourceImage As New Bitmap(parentControl.BackgroundImage, _
parentControl.ClientSize.Width, _
parentControl.ClientSize.Height)
'BLANK IMAGE WE WANT TO DRAW THE
'SECTION TO THAT WE WILL DISPLAY IN THE LISTVIEW
Dim myButtonImage As New Bitmap(srcRect.Width, srcRect.Height)
'CREATE A GRAPHICS OBJECT FROM THE IMAGE
Dim g As Graphics = Graphics.FromImage(myButtonImage)
'IMAGE ATTRIBUTES SO WE CAN SET GAMMA (TO MAKE IMAGE LIGHTER)
Dim image_attr As New Drawing.Imaging.ImageAttributes
image_attr.SetGamma(_gamma)
'DRAW CROPPED AND LIGHTENED IMAGE TO
'THE GRAPHICS OBJECT (WRITES TO myListViewImage OBJECT)
g.DrawImage(mySourceImage, _
Me.ClientRectangle, _
(srcRect.X + _Offset.X), _
(srcRect.Y + _Offset.Y), _
srcRect.Width, _
srcRect.Height, _
GraphicsUnit.Pixel, _
image_attr)
image_attr.Dispose()
g.Dispose()
image_attr = Nothing
g = Nothing
'BEFORE WE SET THE BACKGROUND IMAGE, SET THE BYPASS FLAG
'SO WE DONT GET STUCK IN A PAINTING LOOP
_bypassPainting = True
Me.BackgroundImageLayout = ImageLayout.None
Me.BackgroundImage = myButtonImage
End Sub
#End Region
There is one other method we use for support here, and it gets the location of the button in relation to the form when the button is not directly on the form (it is in a container or multiple containers on the form)
#Region "SUPPORT METHODS"
Private Function GetPointFromForm(ByVal C As Control) As Point
Try
Return C.FindForm.PointToClient( _
C.Parent.PointToScreen(C.Location))
Catch ex As Exception
Return New Point(0, 0)
End Try
End Function
#End Region
So that is it. Above is all the code you need to get this working, and you can find the source code attached to this post. It was written in Visual Studio 2008, however for maximum compatibility, it is targeting the .NET 2.0 framework since this doesn't use any 3.x exclusive features. That also means you could copy/paste this code into Visual Studio 2005 and it would also work.
One last note I want to touch on again, is that this is just a proof of concept, and has not been fully tested on all operating systems and under various different settings and configurations you may have on your WinForm. That being said, most likely you can modify this code to meet your needs.
Next time I will show you how to do this same thing, but with specific container controls, such as the GroupBox, Panel, and TabControl. I also have done this with the label control. The label support transparent backgrounds right out of the box, however adding my method adds the ability for the frosted effect, as well as the ability to target a parent form, instead of just the label container.
As always, send me your comments or code improvements if you happen to find any.
Posted
May 05 2008, 06:44 PM
by
Matthew Kleinwaks