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

RCM -Lite

Rate me:
Please Sign up or sign in to vote.
4.93/5 (12 votes)
23 Feb 2010CPOL4 min read 29.2K   1.6K   19   5
A Form skinning library in C#
main.jpg

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:

  1. Instantiate the class and add the form's handle.
  2. Add the frame and button images.
  3. 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.

menu.jpg

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.

C#
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:

C#
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;
}

caption-buttons.jpg

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:

C#
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;
C#
private void StartTimer()
{
    if (_buttonTimer > 0)
        StopTimer();
    SetTimer(ParentWnd, 66, 100, IntPtr.Zero);
}

private void StopTimer()
{
    if (_buttonTimer > 0)
    {
    KillTimer(ParentWnd, 66);
        _buttonTimer = 0;
    }
}

xbox-buttons.jpg

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:

Image 5 CollapseImage 6 Copy Code
C#
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

License

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


Written By
Network Administrator vtdev.com
Canada Canada
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.

Comments and Discussions

 
BugResize Correction Bug Fix-ish Pin
Wilksey-3710-Jun-13 4:15
Wilksey-3710-Jun-13 4:15 
Questiona bug about maximize Pin
Hi.Healthy28-Feb-11 16:53
Hi.Healthy28-Feb-11 16:53 
GeneralMDI Pin
Ivar Martinez6-Jan-11 5:07
Ivar Martinez6-Jan-11 5:07 
Cant get it to work with mdi... any direction on how to do it... thanks.
Questionhow to use helpButton Pin
bangzhu0123-Nov-10 21:56
bangzhu0123-Nov-10 21:56 
GeneralCode Correction Pin
John Underhill1-Mar-10 11:57
John Underhill1-Mar-10 11:57 

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.