WinForms Form Skin
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
):
Custom skin (AiSkin
) with additional custom button that is placed before system buttons:
Custom skin (AiSkin
) with additional custom button that is placed before system buttons, and tabs:
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 throughWndProc
. This class is contained withinSkinForm
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. -
Class that represents a tab button that will be placed on the bar form.TabFormButton
Class. -
BarFormButtonCollection
Class.Represents a collection of the
BarFormButton
object. This collection cannot contain one of theMinimizeButton
,MaximizeButton
, orCloseButton
. -
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:
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.
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.
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.
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.
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.
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.
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