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 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:
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
{
}
public class YourForm : System.Windows.Forms.Form
{
public YourForm()
{
SkinForm sf = new SkinForm();
YourSkin skin = new YourSkin();
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
{
private void form_HandleCreated(object sender, EventArgs e)
{
AssignHandle(((Form)sender).Handle);
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
{
[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) {
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;
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 {
protected internal abstract void onFormTextChanged();
protected internal abstract bool onDoubleClick();
protected internal abstract bool onMouseMove(MouseEventArgs e);
protected internal abstract bool onMouseDown(MouseEventArgs e);
protected internal abstract bool onMouseUp(MouseEventArgs e);
protected internal abstract bool onMouseLeave();
protected internal abstract bool onPaint(PaintEventArgs e);
protected internal abstract bool onKeyDown(KeyEventArgs e);
protected internal abstract bool setRegion(Size size);
protected internal abstract Win32API.NCCALCSIZE_PARAMS
calculateNonClient(Win32API.NCCALCSIZE_PARAMS p);
protected internal abstract void updateBar(Rectangle rect);
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) {
if (Form == null || Form.WindowState == FormWindowState.Minimized ||
(Form.WindowState == FormWindowState.Minimized && Form.MdiParent != null)) return p;
p.rect0.Top += _rectBar.Height;
_rectClient.Y = _rectBar.Height + 1;
if (Form.WindowState == FormWindowState.Maximized) {
_rectClient.X = 0;
_rectClient.Width = p.rect0.Right - (p.rect0.Left + 1);
_rectClient.Height = p.rect0.Bottom - (p.rect0.Top + 1);
} else {
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;
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;
}
if (Form.FormBorderStyle == FormBorderStyle.Sizable ||
Form.FormBorderStyle == FormBorderStyle.SizableToolWindow
&& Form.WindowState != FormWindowState.Maximized) {
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;
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) {
if (_rectBar.Contains(p)) return Win32API.HTCAPTION;
}
return Win32API.HTNOWHERE;
}
History
- 4th July, 2012: Initial version
Keep moving ...
Learn the different ...
Get the advantages ...
And ... knowing everything ...