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));
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);
windowRect.Left -= _iFrameWidth;
windowRect.Right += _iFrameWidth;
windowRect.Top -= _iCaptionHeight;
windowRect.Bottom += _iFrameHeight;
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
:
Collapse
Copy Code
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 right ButtonOffsetY
:Caption Buttons offset from center CenterTitle
:Center the forms title in the caption bar ExcludeLeftStart
:Exclude tiling of left caption area starting position ExcludeLeftEnd
:Exclude tiling of left caption area ending position ExcludeRightStart
:Exclude tiling of right caption area starting position ExcludeRightEnd
:Exclude tiling of right caption area ending position FontRightLeading
:Use right aligned text in the caption bar ForeColor
:The caption title forecolor IconOffsetX
:Icon offset from left IconOffsetY
:Icon offset from center SupressHelpTip
:Suppress tooltip on optional help button TitleOffsetX
:Caption offset from left TitleOffsetY
:Caption offset from top center TitleFont
:The caption font
Network and programming specialist. Started in C, and have learned about 14 languages since then. Cisco programmer, and lately writing a lot of C# and WPF code, (learning Java too). If I can dream it up, I can probably put it to code. My software company, (VTDev), is on the verge of releasing a couple of very cool things.. keep you posted.