The .NET Compact Framework provides an excellent abstracted interface to the Graphics Device Interface (GDI) subsystem in the same manner as the full .NET Framework. However, there are still quite a few drawing methods which are not part of the CF which if included would make the design of custom controls easier (and sometimes more fun). One of the methods missing from the Compact Framework is the RoundRect GDI function. According to MSDN, "This function draws a rectangle with rounded corners. The current pen outlines the rectangle and the current brush fills it."
In this entry, the RoundRect function is used to create a new custom control called a RoundedGroupBox. The standard GroupBox control exists on the full framework and provides a panel with a rounded outline and a title. It logically groups controls into a designated area of the screen. An image of the GroupBox from the full framework is displayed in Image 1. The Compact Framework does not contain a GroupBox control, so this custom control, RoundedGroupBox, serves as a stand-in with a little more flare.
Image 1 - GroupBox from the full .NET framework
To create the RoundedGroupBox using C# and the .NET Compact Framework, aside from the RoundRect GDI function, there are three other GDI functions which need to be P/Invoked:
- CreateSolidBrush - This function is used to create colored brushes used to fill in the round rectangles.
- SelectObject - This function is used to select a brush to use when drawing.
- DeleteObject - This function deletes a drawing object and frees its associated memory.
A fourth function, GetStockObject is also contained within the source code but not used within this example. GDI contains some predefined pens, brushes and fonts. This function can retrieve any of those.
One very important point when P/Invoking these functions is that they need to be accessed at both design time and runtime. On Windows Mobile, these functions are all found in the coredll native assembly. Trying to use that assembly during design time is not going to work however, because Visual Studio will try to access a coredll.dll file on the local desktop. To work in the designer on the desktop, these functions need to be accessed in the gdi32 assembly found in the \Windows\System32 folder. A simple wrapper function has been written that checks the current environment and decides which version of the function to use -- gdi32 on the desktop and coredll on Windows Mobile. The source code for accessing these functions is listed in Code Listing 1. Image 2 shows the RoundedGroupBox control displayed in the Visual Studio designer.
Code Listing 1 - NativeMethods.cs
Image 2 - RoundedGroupBox control displayed in the designer

By default, when a P/Invoke call is made inside a .NET CF custom control, the Visual Studio designer will not attempt to draw the control properly because it believes it cannot. The attribute which controls whether Visual Studio will attempt to draw the control or not is the DesktopCompatible attribute which is placed in the DesignTimeAttributes.xmta xml file. Code Listing 2 shows the DesignTimeAttributes.xmta file. Image 3 displays two versions of the control - one with the DesktopCompatible attribute set to true in the xmta file and the other without it.
Code Listing 2 - DesignTimeAttributes.xmta
<?xml version="1.0" encoding="utf-16"?>
<Classes xmlns="http://schemas.microsoft.com/VisualStudio/2004/03/SmartDevices/XMTA.xsd">
<Class Name="Rhinomobile.ControlLibrary.BorderPanel">
<DesktopCompatible>true</DesktopCompatible>
<Property Name="BorderColor">
<Category>Appearance</Category>
<Description>The color used to draw the border of the panel.</Description>
</Property>
<Property Name="BorderWidth">
<Category>Layout</Category>
<Description>The width of the panel border.</Description>
</Property>
</Class>
<Class Name="Rhinomobile.ControlLibrary.RoundedGroupBox">
<DesktopCompatible>true</DesktopCompatible>
<Property Name="OuterColor">
<Category>Appearance</Category>
<Description>The color user to fill the background of the title of the rounded group box.</Description>
</Property>
<Property Name="RoundedGroupBoxText">
<Category>Appearance</Category>
<Description>The text displayed in the title of the group box panel.</Description>
</Property>
</Class>
</Classes>
Image 3 - Two versions of RoundedGroupBox displayed - One with the DesktopCompatible attribute set to true in the DesignTimeAttributes.xmta file, the other without

The RoundedGroupBox control itself is comprised of two round rectangles drawn with different colored brushes. The top is slightly offset on the inner polygon which leaves an area for the title to be written. This control is derived from a standard .NET Compact Framework System.Windows.Forms.Panel, so out of the box it provides the logical grouping functionality inherent in a panel.
RoundedGroupBox provides two additional properties, OuterColor and RoundedGroupBoxText. OuterColor is the color used for the title area and the outline. The standard BackColor property is used for the area where controls are placed. RoundedGroupBoxText is a property which contains the text displayed in the title area.
RoundedGroupBox also overrides two inherited methods, OnPaintBackground and OnPaint. In OnPaintBackground, the background color of the parent control is used to fill the RoundedGroupBox control. Doing this fills in the gaps left by the rounded corners of the control so that seemingly the parent control is showing through. OnPaint is more complicated and a snippet of code is displayed inline below.
protected override void OnPaint(PaintEventArgs e)
{
int outerBrushColor = HelperMethods.ColorToWin32(m_outerColor);
int innerBrushColor = HelperMethods.ColorToWin32(this.BackColor);
IntPtr hdc = e.Graphics.GetHdc();
try
{
IntPtr hbrOuter = NativeMethods.CreateSolidBrush(outerBrushColor);
IntPtr hOldBrush = NativeMethods.SelectObject(hdc, hbrOuter);
NativeMethods.RoundRect(hdc, 0, 0, this.Width, this.Height, DIAMETER, DIAMETER);
IntPtr hbrInner = NativeMethods.CreateSolidBrush(innerBrushColor);
NativeMethods.SelectObject(hdc, hbrInner);
NativeMethods.RoundRect(hdc, 0, 18, this.Width, this.Height, DIAMETER, DIAMETER);
NativeMethods.SelectObject(hdc, hOldBrush);
NativeMethods.DeleteObject(hbrOuter);
NativeMethods.DeleteObject(hbrInner);
}
finally
{
e.Graphics.ReleaseHdc(hdc);
}
if (!string.IsNullOrEmpty(m_roundedGroupBoxText))
{
Font titleFont = new Font("Tahoma", 9.0F, FontStyle.Bold);
Brush titleBrush = new SolidBrush(this.BackColor);
try
{
e.Graphics.DrawString(m_roundedGroupBoxText, titleFont, titleBrush, 14.0F, 2.0F);
}
finally
{
titleFont.Dispose();
titleBrush.Dispose();
}
}
base.OnPaint(e);
}
The first two function calls are to the ColorToWin32 method. This method converts a System.Drawing.Color value into an integer used with Windows 32 functions. It is contained in the HelperMethods class. This class can be found in Code Listing 3.
Following the color conversions, a handle to the underlying GDI device context is retrieved using the GetHdc method. A solid brush of the outer color is then created using CreateSolidBrush. This brush is then selected using the SelectObject function. This is analagous to selecting a type of brush within a graphics program when drawing shapes. A handle to the previously selected brush is saved so that it can be the selected brush again when the control is finished drawing. The outer round rectangle is drawn with a call to the RoundRect function. The same steps are repeated for the inner round rectangle: creation, selection, and drawing. Both the outer and inner round rectangles are drawn using a constant value for nWidth and nHeight giving the control a circular corner. When both round rectangles have been drawn, the previously selected brush is set as the selected brush again. The two created brushes are then deleted. Finally, the handle to the device context is released. The remaining part of the OnPaint method is responsible for writing the title text in the space between the two round rectangles. The source code for RoundedGroupBox.cs is found in Code Listing 4. Image 4 shows a screenshot of a sample application which uses the RoundedGroupBox control to ask a question where a singular answer can be chosen.
Code Listing 3 - HelperMethods.cs
#region Using Directives
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
#endregion
namespace Rhinomobile.ControlLibrary
{
/// <summary>
/// Useful methods
/// </summary>
static class HelperMethods
{
/// <summary>
/// Converts a System.Drawing.Color value into a Windows 32 color value.
/// </summary>
/// <param name="color">System.Drawing.Color value</param>
/// <returns>Windows 32 integer representation of a color</returns>
public static int ColorToWin32(Color color)
{
return ((color.R | (color.G << 8)) | (color.B << 16));
}
}
}
Code Listing 4 - RoundedGroupBox.cs
#region Using Directives
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Text;
#endregion
namespace Rhinomobile.ControlLibrary
{
/// <summary>
/// A group box panel with rounded corners and a title
/// </summary>
public class RoundedGroupBox : Panel
{
#region Constants
private const int DIAMETER = 32;
#endregion
#region Fields
private Color m_outerColor = SystemColors.ActiveCaption;
private string m_roundedGroupBoxText = string.Empty;
#endregion
#region Protected Methods
protected override void OnPaintBackground(PaintEventArgs e)
{
if (this.Parent != null)
{
SolidBrush backBrush = new SolidBrush(this.Parent.BackColor);
try
{
e.Graphics.FillRectangle(backBrush, 0, 0, this.Width, this.Height);
}
finally
{
backBrush.Dispose();
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
int outerBrushColor = HelperMethods.ColorToWin32(m_outerColor);
int innerBrushColor = HelperMethods.ColorToWin32(this.BackColor);
IntPtr hdc = e.Graphics.GetHdc();
try
{
IntPtr hbrOuter = NativeMethods.CreateSolidBrush(outerBrushColor);
IntPtr hOldBrush = NativeMethods.SelectObject(hdc, hbrOuter);
NativeMethods.RoundRect(hdc, 0, 0, this.Width, this.Height, DIAMETER, DIAMETER);
IntPtr hbrInner = NativeMethods.CreateSolidBrush(innerBrushColor);
NativeMethods.SelectObject(hdc, hbrInner);
NativeMethods.RoundRect(hdc, 0, 18, this.Width, this.Height, DIAMETER, DIAMETER);
NativeMethods.SelectObject(hdc, hOldBrush);
NativeMethods.DeleteObject(hbrOuter);
NativeMethods.DeleteObject(hbrInner);
}
finally
{
e.Graphics.ReleaseHdc(hdc);
}
if (!string.IsNullOrEmpty(m_roundedGroupBoxText))
{
Font titleFont = new Font("Tahoma", 9.0F, FontStyle.Bold);
Brush titleBrush = new SolidBrush(this.BackColor);
try
{
e.Graphics.DrawString(m_roundedGroupBoxText, titleFont, titleBrush, 14.0F, 2.0F);
}
finally
{
titleFont.Dispose();
titleBrush.Dispose();
}
}
base.OnPaint(e);
}
#endregion
#region Properties
/// <summary>
/// The color user to fill the background of the title of the rounded group box.
/// </summary>
public Color OuterColor
{
get
{
return m_outerColor;
}
set
{
m_outerColor = value;
this.Invalidate();
}
}
/// <summary>
/// The text displayed in the title of the group box panel.
/// </summary>
public string RoundedGroupBoxText
{
get
{
return this.m_roundedGroupBoxText;
}
set
{
m_roundedGroupBoxText = value;
this.Invalidate();
}
}
#endregion
}
}
Image 4 - RoundedGroupBox example application










