RCM -Lite






4.93/5 (11 votes)
A Form skinning library in C#

A New Twist
It turns out I needed a form skinning apparatus for a project I am currently working on, and rather than reinvent the wheel, I decided to pull the form skinning routines out of my RCM project and create a new library. I think you'll find that many of the glitches that haunted RCM are resolved, particularly problems that were specific to XP. The library also has no problem skinning all form types, rather than as in RCM, which was designed to skin only sizeable standard frames.
In this article, I have included some of the relevant text from the RCM article, along with the revised code snippets. I also plan to apply these changes to RCM, and update it once this is published.
Using this Library
I have designed this library to be as automated as possible, you need only compile the library and add a reference to your project, then only three commands are needed to start skinning the form:
- Instantiate the class and add the form's handle.
- Add the frame and button images.
- Call the
Start
method.
That's all there is to it.
The bitmaps used to skin the frame are formatted in the same way as most common skinning engines like Win-blinds, with literally thousands of skins to choose from. I have included the bitmaps and PNGs used in the demo to give you an idea of the format.
The form's frame has four parts: caption, left side, right side, and bottom. Each part has two states, active and inactive. The form's buttons have three states: normal, hover, and pressed. To determine the image format used, simply examine the example images included with this project.
Have DLL, Will Travel
Since this is a library, it is portable and not bound only to C# developers. That's right, it can be used in VB! I know how much you VB guys love your user controls, so I included a VB demo project. For that matter, this should be accessible to all .NET language implementations, perhaps even from VS6, (though I haven't tried it). The classes are also designed so that they can be employed independently. Most of them will require the cStoreDc
and cGraphics
classes which can be simply embedded into the parent class.

Forms
Possibly the most problematic and downright difficult controls to draw. There are many messages that can trigger an unexpected need to repaint part or all of the non-client area of a form. There is also a lack of good examples in the wild of skinning a form properly, so it required some serious trial and error to get it right. Now make no mistake, this is not using SetWindowRgn
on a borderless form, this is painting over the frame in much the same way that window themes are painted. One problem I have seen in other examples of this, is how to resize the client area to the proper dimension, given the window type and size of the new frame parts. This is done by intercepting the WM_NCCALCSIZE
message and changing the RECT
sizes in the NCCALCSIZE_PARAMS
structure. The structure contains an array of three rectangles; the first contains the current client size; we modify these values, then copy the RECT
to the second element, which tells the form the new client dimensions.
case WM_NCCALCSIZE:
if (m.WParam != IntPtr.Zero)
{
NCCALCSIZE_PARAMS ncsize = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure
(m.LParam, typeof(NCCALCSIZE_PARAMS));
WINDOWPOS wp = (WINDOWPOS)Marshal.PtrToStructure
(ncsize.lppos, typeof(WINDOWPOS));
// store original frame sizes
if (!_bStoreSize)
{
_bStoreSize = true;
_iCaptionHeight = ncsize.rect2.Top - ncsize.rect0.Top;
_iFrameHeight = ncsize.rect0.Bottom - ncsize.rect2.Bottom;
_iFrameWidth = ncsize.rect2.Left - ncsize.rect0.Left;
}
if (!_bResetSize)
{
ncsize.rect0 = CalculateFrameSize(wp.x, wp.y, wp.cx, wp.cy);
ncsize.rect1 = ncsize.rect0;
}
Marshal.StructureToPtr(ncsize, m.LParam, false);
m.Result = (IntPtr)WVR_VALIDRECTS;
}
else
{
RECT rc = (RECT)m.GetLParam(typeof(RECT));
rc = CalculateFrameSize(rc.Left, rc.Top, rc.Right -
rc.Left, rc.Bottom - rc.Top); ;
Marshal.StructureToPtr(rc, m.LParam, true);
m.Result = MESSAGE_PROCESS;
}
base.WndProc(ref m);
break;
In order to do this properly, we first have to subtract the default window sizes from the source RECT, which varies depending on the windows style:
private RECT CalculateFrameSize(int x, int y, int cx, int cy)
{
RECT windowRect = new RECT(x, y, x + cx, y + cy);
// subtract original frame size
windowRect.Left -= _iFrameWidth;
windowRect.Right += _iFrameWidth;
windowRect.Top -= _iCaptionHeight;
windowRect.Bottom += _iFrameHeight;
// reset client area with new size
windowRect.Left += (_oLeftFrameBitmap.Width / 2);
windowRect.Right -= (_oRightFrameBitmap.Width / 2);
windowRect.Bottom -= (_oBottomFrameBitmap.Height / 2);
windowRect.Top += (_oCaptionBarBitmap.Height / 2);
return windowRect;
}
Another issue is trying to emulate the caption button behavior, for example, when the mouse is held down, then dragged away from the button and released, getting the button visual state to change correctly, as there is no NC_MOUSELEAVE
message. We do this by starting a timer that runs through the window procedure fired after a NC_MOUSEMOVE
message:
case WM_NCMOUSEMOVE:
_eLastButtonHit = HitTest();
if ((_eLastButtonHit == HIT_CONSTANTS.HTCLOSE) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMAXBUTTON) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMINBUTTON))
{
StartTimer();
InvalidateWindow();
}
base.WndProc(ref m);
break;
case WM_TIMER:
_buttonTimer += 1;
HIT_CONSTANTS hitTimer = HitTest();
if ((hitTimer == HIT_CONSTANTS.HTCLOSE) ||
(hitTimer == HIT_CONSTANTS.HTMAXBUTTON) ||
(hitTimer == HIT_CONSTANTS.HTMINBUTTON))
{
if (hitTimer != _eLastButtonHit)
{
StopTimer();
InvalidateWindow();
}
else
{
if (_buttonTimer > 500)
StopTimer();
}
}
else
{
if (!LeftKeyPressed())
{
StopTimer();
InvalidateWindow();
}
}
base.WndProc(ref m);
break;
private void StartTimer()
{
if (_buttonTimer > 0)
StopTimer();
SetTimer(ParentWnd, 66, 100, IntPtr.Zero);
}
private void StopTimer()
{
if (_buttonTimer > 0)
{
KillTimer(ParentWnd, 66);
_buttonTimer = 0;
}
}
When using buttons of different sizes from the default, we have to let the OS know so that the hit testing can trigger internal events, like showing the tooltip when hovering over a caption button. This is done by storing the button sizes, doing relative hit testing, then passing the correct HIT_CONSTANTS
flag through the result in WM_NCHITTEST
:
case WM_NCHITTEST:
_eLastWindowHit = (HIT_CONSTANTS)DefWindowProc(m.HWnd, m.Msg, m.WParam, m.LParam);
_eLastButtonHit = HitTest();
if ((_eLastButtonHit == HIT_CONSTANTS.HTCLOSE) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMAXBUTTON) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMINBUTTON))
{
m.Result = (IntPtr)_eLastButtonHit;
}
else
{
m.Result = (IntPtr)_eLastWindowHit;
base.WndProc(ref m);
}
break;
You have probably noticed the addition of a Help button to the projects capabilities. This button can either be a standard help button, or a user defined button, If using a user button, the tooltip for the help button can be suppressed via the SupressHelpTip
property. A number of other properties have been added. Here's the list:
ButtonOffsetX
:Caption buttons offset from rightButtonOffsetY
:Caption Buttons offset from centerCenterTitle
:Center the forms title in the caption barExcludeLeftStart
:Exclude tiling of left caption area starting positionExcludeLeftEnd
:Exclude tiling of left caption area ending positionExcludeRightStart
:Exclude tiling of right caption area starting positionExcludeRightEnd
:Exclude tiling of right caption area ending positionFontRightLeading
:Use right aligned text in the caption barForeColor
:The caption title forecolorIconOffsetX
:Icon offset from leftIconOffsetY
:Icon offset from centerSupressHelpTip
:Suppress tooltip on optional help buttonTitleOffsetX
:Caption offset from leftTitleOffsetY
:Caption offset from top centerTitleFont
:The caption font