Click here to Skip to main content
15,881,715 members
Articles / Programming Languages / C#

WinForms Form Skin

Rate me:
Please Sign up or sign in to vote.
4.82/5 (23 votes)
4 Jul 2012CPOL4 min read 71.1K   9.3K   86   10
A custom, fully customizable skin for your WinForms Form

Introduction

SkinForm class allows you to create custom skin for your .NET WinForms Form object, adds objects as many as you want, and handles many events (like mouse, keyboard, and paint) for your skin like you used standard control.

Background

Lots of WinForms applications have their own skin that have better look and feel compared with standard WinForms skin. Also, there is an area of WinForms application that is less useful to our application, that is the area where the Form's text and system button is placed (in my context, I call that bar form). Besides, improving our look and feel, better use of the screen area can give a special point to us. So, why didn't we try to create it better ourselves?

Preview

Custom skin (AiSkin):

Image 1

Custom skin (AiSkin) with additional custom button that is placed before system buttons:

Image 2

Custom skin (AiSkin) with additional custom button that is placed before system buttons, and tabs:

Image 3

Main Objects

There are several classes to make this skin (under Ai.Control namespace):

  • Main Classes:
    • SkinForm Class.

      This is the main class to manage link between skin and skinned form.

    • FormHook Class.

      Inherited from NativeWindow class. Purposed to hook message processing of the skinned form through WndProc. This class is contained within SkinForm class.

    • SkinBase Class.

      Base class for skin management. This class contains variables for skin information, and functions to process messages caught by FormHook class.

    • BarFormButton Class.

      Class that represent a button that will be placed at the bar form.

    • MinimizeButton Class.

      Inherited from BarFormButton, represents the minimize button of the form.

    • MaximizeButton Class.

      Inherited from BarFormButton, represents the maximize button of the form.

    • CloseButton Class.

      Inherited from BarFormButton, represents the close button of the form.

  • Additional Classes:
    • CustomButton Class.

      Inherited from BarFormButton, represents a custom button of the form.

    • TabFormButton Class.

      Class that represents a tab button that will be placed on the bar form.
    • BarFormButtonCollection Class.

      Represents a collection of the BarFormButton object. This collection cannot contain one of the MinimizeButton, MaximizeButton, or CloseButton.

    • TabFormButtonCollection Class.

      Represents a collection of the TabFormButton object.

    • Win32API Class.

      Encapsulates structures, external functions, and constants used for win32 API calls.

  • Sample Class:
    • AiSkin Class.

      An implementation of SkinBase class that supports both tabs and custom buttons.

To create and use your own custom skin, create a class that inherits SkinBase class, create an instance of the SkinForm class and your custom skin class on a WinForms Form object, and sets both of Form and Skin properties of the SkinForm instance to the instance of your WinForms Form and the instance of your custom skin, like this:

C#
public class YourSkin : SkinBase
{
	// your implementation here
}

public class YourForm : System.Windows.Forms.Form
{
	public YourForm()
	{
		// Create an instance of SkinForm class.
		SkinForm sf = new SkinForm();
		// Create an instance of YourSkin class.
		YourSkin skin = new YourSkin();
		// Sets both Form and Skin property of sf.
		sf.Skin = skin;
		sf.Form = this;
	}
}

Brief Description

Below is short explanation of the important things to develop your custom skin.

FormHook

The purpose of this class is to hook the window message processing of the skinned form through its WndProc function. This class will deliver any events that are required for skinning process. For keyboard message processing, this class using windows raw input functionality. The skinned form will be registered to receive raw input message when the HandleCreated event of the form is fired.

C#
private class FormHook : NativeWindow
{
	// ...
	/// <summary>
	/// Called when the handle of the form is created.
	/// </summary>
	private void form_HandleCreated(object sender, EventArgs e)
	{
		AssignHandle(((Form)sender).Handle);
		// Registering form for raw input
		Win32API.RAWINPUTDEVICE[] rid = new Win32API.RAWINPUTDEVICE[1];
		rid[0].usUsagePage = 0x01;
		rid[0].usUsage = 0x06;
		rid[0].dwFlags = Win32API.RIDEV_INPUTSINK;
		rid[0].hwndTarget = ((Form)sender).Handle;
		_RIDRegistered = Win32API.RegisterRawInputDevices
                        (rid, (uint)rid.Length, (uint)Marshal.SizeOf(rid[0]));
		if (isProcessNCArea()) 
		{
			updateStyle();
			updateCaption();
		}
	}
}

The window message hooking process occurs within WndProc function of this class.

C#
private class FormHook : NativeWindow
{
	// ...

	/// <summary>
	/// Invokes the default window procedure associated with this window.
	/// </summary>
	[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
	protected override void WndProc(ref Message m) {
		bool suppressOriginalMessage = false;
		switch (m.Msg) { 
			case Win32API.WM_STYLECHANGED:
				updateStyle();
				if (_owner._skin != null) _owner._skin.setRegion(_owner._form.Size);
				break;
			#region Form Activation
			case Win32API.WM_ACTIVATEAPP:
				if (_owner._skin != null) _owner._skin.FormIsActive = (int)m.WParam != 0;
				onNCPaint(true);
				break;
			case Win32API.WM_ACTIVATE:
				if (_owner._skin != null) _owner._skin.FormIsActive = 
                         ((int)Win32API.WA_ACTIVE == (int)m.WParam || 
                                 (int)Win32API.WA_CLICKACTIVE == (int)m.WParam);
				onNCPaint(true);
				break;
			case Win32API.WM_MDIACTIVATE:
				if (m.WParam == _owner._form.Handle) {
					if (_owner._skin != null) _owner._skin.FormIsActive = false;
				} else if (m.LParam == _owner._form.Handle) {
					if (_owner._skin != null) _owner._skin.FormIsActive = true;
				}
				onNCPaint(true);
				break;
			#endregion
			#region Mouse Events
			case Win32API.WM_NCLBUTTONDOWN:
			case Win32API.WM_NCRBUTTONDOWN:
			case Win32API.WM_NCMBUTTONDOWN:
				suppressOriginalMessage = onNCMouseDown(ref m);
				break;
			case Win32API.WM_NCLBUTTONUP:
			case Win32API.WM_NCMBUTTONUP:
			case Win32API.WM_NCRBUTTONUP:
				suppressOriginalMessage = onNCMouseUp(ref m);
				break;
			case Win32API.WM_NCMOUSEMOVE:
				suppressOriginalMessage = onNCMouseMove(ref m);
				break;
			case Win32API.WM_NCMOUSELEAVE:
			case Win32API.WM_MOUSELEAVE:
			case Win32API.WM_MOUSEHOVER:
				_owner._skin.onMouseLeave();
				break;
			case Win32API.WM_NCLBUTTONDBLCLK:
				suppressOriginalMessage = onNCDoubleClick(ref m);
				break;
			#endregion
			#region Non-client Hit Test
			case Win32API.WM_NCHITTEST:
				suppressOriginalMessage = onNCHitTest(ref m);
				break;
			#endregion
			#region Painting and sizing operation
			case Win32API.WM_NCPAINT:
				if (onNCPaint(true)) {
					m.Result = (IntPtr)1;
					suppressOriginalMessage = true;
				}
				break;
			case Win32API.WM_NCCALCSIZE:
				if (m.WParam == (IntPtr)1) {
					if (!isProcessNCArea()) break;
					Win32API.NCCALCSIZE_PARAMS p = (Win32API.NCCALCSIZE_PARAMS)m.GetLParam
                                                   (typeof(Win32API.NCCALCSIZE_PARAMS));
					if (_owner._skin != null) p = _owner._skin.calculateNonClient(p);
					Marshal.StructureToPtr(p, m.LParam, true);
					suppressOriginalMessage = true;
				}
				break;
			case Win32API.WM_SHOWWINDOW:
				if (_owner._skin != null) _owner._skin.setRegion(_owner._form.Size);
				break;
			case Win32API.WM_SIZE:
				onResize(m);
				break;
			case Win32API.WM_GETMINMAXINFO:
				suppressOriginalMessage = calculateMaximumSize(ref m);
				break;
			case Win32API.WM_WINDOWPOSCHANGING:
				Win32API.WINDOWPOS wndPos = (Win32API.WINDOWPOS)m.GetLParam
                                            (typeof(Win32API.WINDOWPOS));
				if ((wndPos.flags & Win32API.SWP_NOSIZE) == 0) {
					if (_owner._skin != null) _owner._skin.setRegion
                                        (new Size(wndPos.cx, wndPos.cy));
				}
				break;
			case Win32API.WM_WINDOWPOSCHANGED:
				if (_owner._form.WindowState == FormWindowState.Maximized) 
                                                     _owner._form.Region = null;
				Win32API.WINDOWPOS wndPos2 = (Win32API.WINDOWPOS)m.GetLParam
                                                     (typeof(Win32API.WINDOWPOS));
				if ((wndPos2.flags & (int)Win32API.SWP_NOSIZE) == 0) {
					updateCaption();
					onNCPaint(true);
				}
				break;
			#endregion
			#region Raw Input
			case Win32API.WM_INPUT:
				if (_owner._skin != null) {
					if (_owner._skin.FormIsActive) {
						uint dwSize = 0, receivedBytes;
						uint szRIHeader = 
                           (uint)Marshal.SizeOf(typeof(Win32API.RAWINPUTHEADER));
						int res = Win32API.GetRawInputData(m.LParam, 
                                  Win32API.RID_INPUT, IntPtr.Zero, ref dwSize, szRIHeader);
						if (res == 0) {
							IntPtr buffer = Marshal.AllocHGlobal((int)dwSize);
							if (buffer != IntPtr.Zero) {
								receivedBytes = (uint)Win32API.GetRawInputData
                                (m.LParam, Win32API.RID_INPUT, buffer, ref dwSize, szRIHeader);
								Win32API.RAWINPUT raw = (Win32API.RAWINPUT)
                                Marshal.PtrToStructure(buffer, typeof(Win32API.RAWINPUT));
								if (raw.header.dwType == Win32API.RIM_TYPEKEYBOARD) {
									// Process keyboard event.
									if (raw.keyboard.Message == Win32API.WM_KEYDOWN || 
                                        raw.keyboard.Message == Win32API.WM_SYSKEYDOWN) {
										ushort key = raw.keyboard.VKey;
										Keys kd = (Keys)Enum.Parse(typeof(Keys), 
                                                  Enum.GetName(typeof(Keys), key));
										if (kd != System.Windows.Forms.Control.ModifierKeys) 
                                        kd = kd | System.Windows.Forms.Control.ModifierKeys;
										// Call skin's onKeyDown function.
										KeyEventArgs ke = new KeyEventArgs(kd);
										suppressOriginalMessage = _owner._skin.onKeyDown(ke);
									}
								}
							}
						}
					}
				}
				break;
			#endregion
		}
		if(!suppressOriginalMessage) base.WndProc(ref m);
	}
}

SkinBase

This class encapsulates basic components and functions required to build your own skin.

Basic components consisting of 3 system buttons (minimize, maximize, close), rectangles for holding non-client area information.

C#
public abstract class SkinBase : IDisposable {
	// ...
	#region Protected Fields
	protected MinimizeButton _minimizeButton = new MinimizeButton();
	protected MaximizeButton _maximizeButton = new MaximizeButton();
	protected CloseButton _closeButton = new CloseButton();
	protected bool _formIsActive = true;
	#region Standard Rectangle for Non-client area
	protected Rectangle _rectClient;
	protected Rectangle _rectIcon;
	protected internal Rectangle _rectBar;
	protected Rectangle _rectBorderTop;
	protected internal Rectangle _rectBorderLeft;
	protected internal Rectangle _rectBorderBottom;
	protected internal Rectangle _rectBorderRight;
	protected Rectangle _rectBorderTopLeft;
	protected Rectangle _rectBorderTopRight;
	protected Rectangle _rectBorderBottomLeft;
	protected Rectangle _rectBorderBottomRight;
	#endregion
	#endregion
}

Basic functions for skinned form message handling, its cover activation / deactivation, hit-testing, form sizing / state changed / text changed, non-client area mouse event (mouse down, mouse up, left-button double click), and keydown. For mouse event and hit-testing, the position of the mouse pointer is relative to the top-left corner of the form, not to screen.

C#
public abstract class SkinBase : IDisposable {
	// ...
	/// <summary>
	/// Called when the text property of the form has been changed.
	/// </summary>
	protected internal abstract void onFormTextChanged();
	/// <summary>
	/// Called when the left button of the mouse is double-clicked on the 
    /// non-client area of the form.
	/// </summary>
	protected internal abstract bool onDoubleClick();
	/// <summary>
	/// Called when the mouse pointer is moved over the non-client area of the form.
	/// </summary>
	protected internal abstract bool onMouseMove(MouseEventArgs e);
	/// <summary>
	/// Called when the mouse pointer is over the non-client area of the form 
    /// and a mouse button is pressed.
	/// </summary>
	protected internal abstract bool onMouseDown(MouseEventArgs e);
	/// <summary>
	/// Called when the mouse pointer is over the non-client area of the form 
    /// and a mouse button is released.
	/// </summary>
	protected internal abstract bool onMouseUp(MouseEventArgs e);
	/// <summary>
	/// Called when the mouse pointer is leaving the non-client area of the form.
	/// </summary>
	protected internal abstract bool onMouseLeave();
	/// <summary>
	/// Called when the non-client area of the form is redrawn
	/// </summary>
	protected internal abstract bool onPaint(PaintEventArgs e);
	/// <summary>
	/// Called when one of the registered keys of the skin is pressed.
	/// </summary>
	protected internal abstract bool onKeyDown(KeyEventArgs e);
	/// <summary>
	/// Called when the form need to set its region.
	/// </summary>
	protected internal abstract bool setRegion(Size size);
	/// <summary>
	/// Called when the non-client are of the form need to be calculated.
	/// </summary>
	protected internal abstract Win32API.NCCALCSIZE_PARAMS 
                       calculateNonClient(Win32API.NCCALCSIZE_PARAMS p);
	/// <summary>
	/// Called when the bar of the form is updated.
	/// </summary>
	protected internal abstract void updateBar(Rectangle rect);
	/// <summary>
	/// Called when the hit-test is performed on the non-client area of the form.
	/// </summary>
	protected internal abstract int nonClientHitTest(Point p);
}

When creating your custom skin by inheriting SkinBase class, the important things that we need to pay attention to are calculateNonClient and nonClientHitTest functions.
The calculateNonClient function is the function where you must decide the size of non-client area of the form by modifying p.rect0 value:

  • Decrease the value of p.rect0.Top fields by the height of your bar form.
  • Decrease the value of p.rect0.Left fields by the width of your form's left-border.
  • Decrease the value of p.rect0.Right fields by the width of your form's right-border.
  • Decrease the value of p.rect0.Bottom fields by the height of your form's bottom-border.
C#
protected internal override Win32API.NCCALCSIZE_PARAMS calculateNonClient
     (Win32API.NCCALCSIZE_PARAMS p) {
	// Check if we don't need to calculate the client area.
	if (Form == null || Form.WindowState == FormWindowState.Minimized || 
		(Form.WindowState == FormWindowState.Minimized && Form.MdiParent != null)) return p;
	// Calculate the valid client area of the form here, that is stored 
    // in rect0 of the p parameter.
	p.rect0.Top += _rectBar.Height;
	_rectClient.Y = _rectBar.Height + 1;
	if (Form.WindowState == FormWindowState.Maximized) { 
		// The form is maximized, thus the borders will not be calculated 
        // and the status bar only will be calculated.
		//p.rect0.Bottom -= _rectStatus.Height;
		_rectClient.X = 0;
		_rectClient.Width = p.rect0.Right - (p.rect0.Left + 1);
		_rectClient.Height = p.rect0.Bottom - (p.rect0.Top + 1);
	} else { 
		// Deflate the left, right, and bottom of the rect0 by the left border width,
		// right border width, and sum of the status and bottom border height.
		p.rect0.Left += _rectBorderLeft.Width;
		p.rect0.Right -= _rectBorderRight.Width;
		p.rect0.Bottom -= _rectBorderBottom.Height;
		_rectClient.X = _rectBorderLeft.Width + 1;
		_rectClient.Width = p.rect0.Right - (p.rect0.Left + 2);
		_rectClient.Height = p.rect0.Bottom - (p.rect0.Top + 2);
	}
	return p;
}

The nonClientHitTest function is to tell the system which part of your non-client area pointed by the mouse pointer. The Point p parameter passed on to this function is relative to the top-left corner of the skinned form. The result of this function must be one of the hit-test result constants, constants that prefixes with HT. In my implementation, instead of returning HTMINBUTTON, HTMAXBUTTON, or HTCLOSE, I return HTOBJECT when the mouse pointer is on minimize, maximize, or close button, because I rather want to use my own tooltip than system tooltip :D.

C#
protected internal override int nonClientHitTest(Point p) {
	if (_rectClient.Contains(p)) return Win32API.HTCLIENT;
	if (_rectIcon.Contains(p)) return Win32API.HTMENU;
	// Always return HTOBJECT instead of the corresponding hittest value, 
    // to prevent the default tooltip to be shown.
	if (_minimizeButton.Enabled && _minimizeButton.Visible) {
		if (_minHost.Bounds.Contains(p)) return Win32API.HTOBJECT;
	}
	if (_maximizeButton.Enabled && _maximizeButton.Visible) {
		if (_maxHost.Bounds.Contains(p)) return Win32API.HTOBJECT;
	}
	if (_closeButton.Enabled && _closeButton.Visible) {
		if (_closeHost.Bounds.Contains(p)) return Win32API.HTOBJECT;
	}
	// Test for custom bar button, if any of them, then return the HTOBJECT
	if (Form.FormBorderStyle == FormBorderStyle.Sizable || 
        Form.FormBorderStyle == FormBorderStyle.SizableToolWindow 
		&& Form.WindowState != FormWindowState.Maximized) { 
		// Test for borders.
		// Corners
		if (_rectBorderTopLeft.Contains(p)) return Win32API.HTTOPLEFT;
		if (_rectBorderTopRight.Contains(p)) return Win32API.HTTOPRIGHT;
		if (_rectBorderBottomLeft.Contains(p)) return Win32API.HTBOTTOMLEFT;
		if (_rectBorderBottomRight.Contains(p)) return Win32API.HTBOTTOMRIGHT;
		// vertical and horizontal
		if (_rectBorderTop.Contains(p)) return Win32API.HTTOP;
		if (_rectBorderLeft.Contains(p)) return Win32API.HTLEFT;
		if (_rectBorderRight.Contains(p)) return Win32API.HTRIGHT;
		if (_rectBorderBottom.Contains(p)) return Win32API.HTBOTTOM;
	}
	if (Form.WindowState != FormWindowState.Maximized) {
		// Test for bar form.
		if (_rectBar.Contains(p)) return Win32API.HTCAPTION;
	}
	// Default return value.
	return Win32API.HTNOWHERE;
}

History

  • 4th July, 2012: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Ai Software
Indonesia Indonesia
Keep moving ...
Learn the different ...
Get the advantages ...

And ... knowing everything ...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Аslam Iqbal25-Nov-20 23:28
professionalАslam Iqbal25-Nov-20 23:28 
GeneralMy vote of 5 Pin
Аslam Iqbal7-May-20 4:39
professionalАslam Iqbal7-May-20 4:39 
Questiondo you have any idea how to achieve same result in MFC/C++ Pin
Huzifa Terkawi30-Aug-14 8:56
Huzifa Terkawi30-Aug-14 8:56 
GeneralMy vote of 5 Pin
Renju Vinod9-Dec-13 1:54
professionalRenju Vinod9-Dec-13 1:54 
Nice
Questionmy vote 5 Pin
Abhishek.pachpadra9-Nov-12 12:23
Abhishek.pachpadra9-Nov-12 12:23 
BugRTE When using .NET 4.0 Pin
onelopez17-Jul-12 9:45
onelopez17-Jul-12 9:45 
AnswerRe: RTE When using .NET 4.0 Pin
Lee.W.Spencer15-Sep-12 7:14
Lee.W.Spencer15-Sep-12 7:14 
GeneralMy vote of 5 Pin
Burak Ozdiken4-Jul-12 15:26
Burak Ozdiken4-Jul-12 15:26 
Generalwhat is the difference with this way? Pin
Southmountain4-Jul-12 11:44
Southmountain4-Jul-12 11:44 
GeneralRe: what is the difference with this way? Pin
red_moon4-Jul-12 14:24
red_moon4-Jul-12 14:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.