Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Alpha Blended Windows Forms

0.00/5 (No votes)
17 Sep 2010 11  
Set a 32-bit image as the background for your Windows Form

AlphaForm Example

AlphaForm

Introduction

When people want to use a 32-bit image as the background for a form, they will likely discover the WS_EX_LAYERED window style, which allows you to do just that. The problem that most people will have is that if they try to simply create their window with the layered style, they will find that they are very limited to what they can do. You cannot for example make it a child window, or place any controls on it.

This little library allows you to use that same 32-bit image as your background without having to make any compromises so you can do everything you were always able to do, plus have more professional looking UIs for custom windows and widgets.

I originally wrote this article almost three years before this update, and at the time I knew that it wasn't the best solution but it was one of the very few that would work. The idea was and still is to create an additional window with the WS_EX_LAYERED style and use that window to show the alpha blended background image. We then just need to make sure that this window is positioned behind the main form.

How Do You Make Such A Thing

You might think that positioning two windows on top of each other would be a simple task, and both times I came to look at this problem I had the same thoughts. There are however a few problems that arise, and the solutions to these problems inevitably create more problems of their own. So that you have a better understanding of how this works, and why I've done it the way I have, I'll go through the problems that I encountered and their solutions. You never know you might find out something new!

Positioning the Window

If we are going to use two windows to achieve the effect of a blended background with user controls on top of it, then the first thing that needs to be done is to position the two windows in the same place. Your first thought would probably to register for the LocationChanged event and set the background window to the same position. The problem is that one window will always be slightly ahead of the other, which may cause some flickering which won't make your hard work look particularly fantastic and it may put some users off. This difference if position between the two windows was one of the biggest problems I had in the first version of this library, which I solved by drawing all of the windows controls to the background image by calling DrawToBitmap, hiding the main window and then only moving the background. It worked very well except for the large amount of time it takes to do all of that drawing which caused the movement of the window to be a little jumpy.

Screenshot

Fig 1. The two forms don't line up while the user is dragging them

The solution I have this time comes in two parts, the first is a better way to try and ensure that both windows move at the same time and the second is to again draw the window to the background form.

For both of the windows to move together as one (or as close to as possible) you need to listen out for the WM_WINDOWPOSCHANGING message which is sent just before a form is actually moved or resized, we can then use the information that comes with this message to use DeferWindowPos which will prevent drawing of one or more windows until the entire set of windows has been moved. Since we are sending out new messages to move the window, it is necessary to cancel the movement that would have been caused by the initial WM_WINDOWPOSCHANGING message which can be done by appending the SWP_NOMOVE flag.

WndProc(ref Message m)
{
  switch(m.Msg)
  {
    case WM_WINDOWPOSCHANGING:
      //Get the structure that holds all of the movement data
      Win32.WINDOWPOS posInfo = (Win32.WINDOWPOS)Marshal.PtrToStructure
				(m.LParam, typeof(Win32.WINDOWPOS));
		
      Win32.WindowPosFlags move_size = Win32.WindowPosFlags.SWP_NOMOVE | 
				Win32.WindowPosFlags.SWP_NOSIZE;
      if ((posInfo.flags & move_size) != move_size)
      {
        //Check for my own messages, which I do by setting to hwndInsertAfter to our 
        //own window, which from what I can gather only happens when you resize your
        //window, never when you move it
        if (posInfo.hwndInsertAfter != this.Handle)
        {
          IntPtr hwdp = Win32.BeginDeferWindowPos(2);
          if (hwdp != IntPtr.Zero)
            hwdp = Win32.DeferWindowPos(hwdp, m_layeredWnd.Handle, 
			this.Handle, posInfo.x, posInfo.y, 0, 0, 
                    	(uint)(posInfo.flags | Win32.WindowPosFlags.SWP_NOSIZE | 
			Win32.WindowPosFlags.SWP_NOZORDER));
          if (hwdp != IntPtr.Zero)
            hwdp = Win32.DeferWindowPos(hwdp, this.Handle, 
			this.Handle, posInfo.x, posInfo.y, posInfo.cx, 
                    posInfo.cy, (uint)
			(posInfo.flags | Win32.WindowPosFlags.SWP_NOZORDER));
          if (hwdp != IntPtr.Zero)
            Win32.EndDeferWindowPos(hwdp);

          //Update the flags so that the form will not move with this message
          posInfo.flags |= Win32.WindowPosFlags.SWP_NOMOVE;
          Marshal.StructureToPtr(posInfo, m.LParam, true);
        }
      }
      break;
  }
}

In most situations, this should be enough to provide clean movement of both the windows, but for some users it is still not enough which is why you have the option to draw the entire window to the background when the window is moved. To make this drawing as fast as possible, instead of calling DrawToBitmap for each control, we can just BitBlt the window to the background by getting hold of its Device Context which Windows uses to put your fully drawn window on the screen. To copy the window to the background, we also need to create a mask so that we only copy over the areas of the window that aren't transparent.

Creating the mask is simple, all we need do is draw the window onto a monochrome bitmap and any pixels on the window that are the same colour as the background colour will be drawn white, and everything else will be black. We can then use this mask with the MaskBlt function to draw the window onto the background. Using only two BitBlts, the whole operation only takes a fraction of the time that DrawToBitmap would have, which means that unless you have a particularly large background image or window, there should be no visible lag when starting to move the window.

IntPtr windowDC = Win32.GetWindowDC(this.Handle);	//Window DC that we are 
						//going to copy
IntPtr memDC = Win32.CreateCompatibleDC(windowDC);	//Temporary DC that we draw to
IntPtr BmpMask = Win32.CreateBitmap(this.ClientSize.Width, 
		this.ClientSize.Height, 1, 1, IntPtr.Zero);	//Mask bitmap
IntPtr BmpBack = backImage.GetHbitmap(Color.FromArgb(0));	//Background Image

//--Create mask
Win32.SelectObject(memDC, BmpMask);
//Set the colour that will become white
uint oldCol = Win32.SetBkColor(windowDC, 0X00FF00FF);
Win32.BitBlt(memDC, 0, 0, this.ClientSize.Width, 
	this.ClientSize.Height, windowDC, 0, 0, SRCCOPY);
Win32.SetBkColor(windowDC, oldCol);
//--

//Blit window to background image using mask
//We need to use the SPno raster operation with a white brush to combine our window
//with a black background before putting it onto the 32-bit background image, otherwise
//we end up with blending issues (source and destination colours are ANDed together)
Win32.SelectObject(memDC, BmpBack);
IntPtr brush = Win32.CreateSolidBrush(0x00FFFFFF);
Win32.SelectObject(memDC, brush);
Win32.MaskBlt(memDC, 0, 0, backImage.Width, 
	backImage.Height, windowDC, 0, 0, BmpMask, 0, 0, 0xCFAA0020);

Getting Mouse Input

Our main window will have a transparent background, and as such it will not be able to receive mouse input. The window will not receive any messages for a transparent area of the background, unless you call the Windows function GetCapture(HWND hWnd) but that generates more problems than it solves as it causes problems with regular Windows messages to the desktop and other windows, because your application is capturing all of the mouse input. Your solution may then be to get the mouse input from the background window which should catch any mouse events, so you would register for the background window's mouse events instead.

There are of course, problems. When you click on the background window, it will become the active window and be drawn above your controls. You can try to catch this and restore focus to the main window but it still causes flickering, which is exceedingly unpleasant. There is a window style that you can apply that will stop a window from gaining focus which works to some extent. The background window will not gain focus and be brought forwards but your main window will still lose its focus.

My solution then is to fully disable the background window, any action on this window is then completely ignored and focus remains on our main window. The downside is that no events will ever be fired so we need to look at the window's messages. The downside again is that a disabled window only ever receives one message WM_SETCURSOR which Windows uses to allow applications to change the current cursor. Thankfully this message comes with some additional information which is the hit test code and the current action of the mouse. The mouse action is given in the form of the standard windows messages WM_MOUSEMOVE, WM_LBUTTONDOWN, etc. To use these messages, I subclass the background window so that I can intercept its messages and check for WM_SETCURSOR and then fire off the appropriate events.

m_customLayeredWindowProc = new Win32.Win32WndProc(this.LayeredWindowWndProc);
//Set our new WndProc function and store the old one
m_layeredWindowProc = Win32.SetWindowLong
	(m_layeredWnd.Handle, GWL_WNDPROC, m_customLayeredWindowProc);

private int LayeredWindowWndProc(IntPtr hWnd, int Msg, int wParam, int lParam)
{
  //Check for events
  ...
  
  //Call the original WndProc function
  return Win32.CallWindowProc(m_layeredWindowProc, hWnd, Msg, wParam, lParam);
}

Handling Semi-Transparent Windows

Because we have two separate windows if the opacity is less than 1.0, our main windows controls would be blended with the background window and then to the desktop, rather than straight to the desktop. This one has a fairly simple solution, because we know that the windows will be moving together we can cut out sections of the background image where each of the main windows controls will be, and then draw these sections to the background of our main window. The caveat is that if you place a control over an area of the background image that is not fully opaque, then the colours will be blended with the background colour of the window (which is this case will likely be Fuchsia). This is an optional feature, and can be enabled as per your requirements.

Fig 2. Top: Form without Control backgrounds drawn. Bottom: Form with Control backgrounds.

See how in the top image, your Controls are blended with the background window, and also notice that the text has been drawn onto the windows Fuchsia back colour giving it that pink outline. In the bottom image, we draw the background behind each control so the text is drawn over the actual background image removing the outline, and the button blends straight through to the desktop because we've cut out that section from the background image itself.

Overview

So with the aforementioned solutions, the final setup looks something like this:

class AlphaForm : Form
{
  private LayeredWindow m_layeredWnd;
  private Bitmap m_backgroundImage;
	
  override OnLoad(EventArgs e)
  {
    base.OnLoad(e);
    
    UpdateLayeredWindow();
    m_layeredWnd.Show();

    //Subclass window to intercept messages
    ...
  }

  override OnPainBackground(...)
  {
    //If necessary draw a portion of the background image
    //behind each control
    foreach (Control ctrl in this.Controls)
    {
      Rectangle rect = ctrl.ClientRectangle;
      e.Graphics.DrawImage(m_backgroundImage, rect, rect, GraphicsUnit.Pixel);
    }
  }
	
  private int LayeredWindowWndProc(IntPtr hWnd, int Msg, int wParam, int lParam)
  {
    if(message is WM_SETCURSOR)
    {
      Point mousePos = System.Windows.Forms.Cursor.Position;
			
      MouseEvent = lParam >> 16;
      switch(MouseEvent)
      {
        case WM_MOUSEMOVE:
          MouseEventArgs e = new MouseEventArgs(...);
          this.OnMouseMove(e);
          break;
				
        etc.					
      }
    }
  }
	
  override WndProc(ref Message m)
  {
    switch(m.Msg)
    {
      case WM_WINDOWPOSCHANGING:
        //cancel movement by setting SWP_NOMOVE flag
			
        BeginDeferWindowPos();
        DeferWindowPos( main window );
        DeferWindowPos( background );
        EndDeferWindowPos();
        break;
        
      case WM_MOUSEMOVE:
        if(left mouse button is down)
        {
          //Copy window to background image
          Win32.MaskBlt(...);
          Win32.BitBlt(...);
          Win32.UpdateLayeredWindow(...);
          
          //Tell windows we are clicking the caption so that our window will
          //be dragged
          Win32.ReleaseCapture();
          Win32.SendMessage(this.Handle, 
	    (int)Win32.Message.WM_NCLBUTTONDOWN, (int)Win32.Message.HTCAPTION, 0);
        }
        break;
    }
  }
}

class LayeredWindow : Form
{
  void UpdateWindow(Bitmap image, byte opacity, int width = -1, int height = -1)
  {
    ...
    Win32.UpdateLayeredWindow(...);
  }
}

That's it in a nutshell, there's more code around to make it more usable but that's all you really need to make this work. The code is well commented so it might be worth your time to read through it if you want to see how everything works together in detail.

Using the Code

In the previous version, you had to pass your main form to my custom alpha form, and then show my alpha form. It was a strange thing to have to do, so in this version I've made sure to make it even simpler. You can setup your form to use background images with an alpha channel by typing out 5 characters.

//Your form with a regular window
public class Form1 : Form

//Your form with blended windows
public class Form1 : AlphaForm

Inheriting from AlphaForm will allow you to use 32bit images for your background. You will still be in charge of setting your border style to none and everything should remain exactly the same as with a standard Windows Form, including editing it in the designer (something that was missing in the previous version). Once you have chosen a background image, it will be rendered to the form in the designer so that you can position your controls, but when you run your application, the image will only be on the background window.

Methods and Properties

There are a few properties and methods that have been added to give a little more control over the background form and I'll list them here followed by an example of their usage.

AlphaForm Class Diagram
Bitmap BlendedBackground The image that is to be used as the forms background
bool EnhancedRendering If true when the form is dragged, the foreground window will be drawn to the background window and then hidden. This prevents any visual disparity between the two forms.
bool RenderControlBackground If true, a portion of the background image will be drawn behind each control on the form.
SizeModes SizeMode Changes the way that the form will behave when it is resized, available options are:
  • None: The background image will always remain its original size
  • Stretch: The background will be resized to fit the client area of the main form
  • Clip: The background image will be clipped to within the client area of the main form
void DrawControlBackground(Control ctrl, bool drawBack) When the RenderControlBackground option is set, you can use this method to control which of your Controls will have a background drawn for it. By default, all Controls are set to true
void UpdateLayeredBackground() Can be used to force an update of the forms background when you have added, removed or moved some of your Controls
void SetOpacity(double opacity) Should be used to set the opacity of your form instead of the Opacity property (Otherwise you must call UpdateLayeredBackground() yourself).

The defaults for each of the properties is false, with the SizeMode being set to None.

All of the attributes are available to be set in the designer under the AlphaForm category but here is a quick example of setting up a form with all of the features. Take note that because in the example picBox is excluded from having a background drawn, it would not blend properly with the rest of the form, similar to the top image in Figure 2.

public partial class Form1 : AlphaForm
{
  public Form1()
  {
    InitializeComponent();
  }

  private void Form1_Load(object sender, EventArgs e)
  {
    this.BlendedBackground = new Bitmap(@"C:\myImage.png");
    this.SizeMode = SizeModes.Clip;
    this.DrawControlBackrounds = true;
    this.EnhancedRendering = true;
    DrawControlBackground(this.picBox, false);
    this.SetOpacity(0.75);
  }
}

Final Word

So with this update, I hope to resolve any of the strange issues that people may have experienced, such as not being able to move the window or trying to remove the window from the taskbar, as well as improve usability by making it easier to use. Any forms using this should behave exactly the same as a standard form, and the integration between the layered background window and your own should be seamless. That's what I hope anyway.

History

  • 16-09-2010
    • Demo and source update to fix a couple of bugs
  • 12-09-2010
    • Article rewrite
    • Code updated to new version
  • 3-12-2007
    • Updated source code and demo
    • Added VB startup code
    • Added screenshots
    • Edited article text
  • 5-10-2007
    • Updated demo.zip to contain source code
    • Added information about the settings
  • 4-10-2007
    • Original article posted

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here