Saturday, 19 January 2013

Dynamic Rounded Panel

Dynamic Rounded Panel (test sample)

Width, px Height, px
Radius of circle, px Border Width, px
Background Color Border Color
Apply
inner text

Dynamic Rounded Panel

14 Nov 2007 (updated 10 Jun 2008) This is another variant of the rounded panel creation (the first one you can see here), that uses images to present curved corners. To be more precise, the only image is used, the one that is build based on the specified circle radius, the background color, the border color and width. The panel appearance varies according to all these parameters and can be dynamically changed in runtime.

Picture 1. The structure of the top part of the rounded panel
a - the left part where the upper left part of the image is shown.
b - the middle that is filled with the background color and contains a block element (d). The d element imitates the image border.
c - the right part where the upper right part of the image is shown. Of course, the middle can be simpler if to use the only block that is filled with the background color and has the upper border. But different browsers interpret borders ambiguously, some of them enlarge total height of the block element. It caused a small complication of the control.
The bottom part of border is build on the same principle, only mirror-like.

The code overview

The rounded panel is inherited from the ordinary ASP.Net Panel, one property is added:
Radius - the circle radius
Besides, the control implements interface IHttpHandler and contains a code for the image creation. It has to be registered in web.config in order to process requests for the ~/roundedpanel.ashx?... url.
Generation and registration of the styles that are required for rendering the panel occur in the overriden OnPreRender method.
private string styleGroupName = null;
//the name for the styles group that is responsible for the panel representation
private string StyleGroupName
        {
            get
            {
                if (styleGroupName == null)
                {
                    string key = string.Format("{0}{1}{2}{3}", 
                                                        this.BorderColor.ToArgb(), 
                                                        this.BackColor.ToArgb(), 
                                                        this.BorderWidth, 
                                                        this.Radius);
                    byte[] bytes = System.Text.Encoding.UTF8.GetBytes(key);
                    styleGroupName = Convert.ToBase64String(bytes).Replace("=", "");
                }
                return styleGroupName;
            }
}

//query key for the circle radius
private const string radiusQueryKey = "rd";
//query key for the border width
private const string borderWidthQueryKey = "bw";
//query key for the border color
private const string borderColorQueryKey = "bc";
//query key for the background color
private const string backgroundColorQueryKey = "bg";

protected override void OnPreRender(EventArgs e)
{
    if (this.Radius > 0)
    {
        //creates url to the image showing the curved corners
        string imageUrl = string.Format(
           "~/roundedpanel.ashx?{0}={1}&{2}={3}&{4}={5},{6},{7}&{8}={9},{10},{11}",
            radiusQueryKey,
            this.Radius,
            borderWidthQueryKey,
            this.BorderWidth.Value,
            borderColorQueryKey,
            this.BorderColor.R,
            this.BorderColor.G,
            this.BorderColor.B,
            backgroundColorQueryKey,
            this.BackColor.R,
            this.BackColor.G,
            this.BackColor.B);
        //creates styles
        StringBuilder sb = new StringBuilder();
        sb.AppendFormat(
@".{0} .a, .{0} .a b, .{0} .d, .{0} .d b 
{{display:block;font-size:1px;overflow:hidden;}}
.{0} .a, .{0} .d, .{0} .b, .{0} .e 
{{background-image:url({4});background-repeat:no-repeat;height:{1}px;}}
.{0} .b, .{0} .e {{margin-left:{1}px;}}
.{0} .a {{background-position:left top;}}
.{0} .b {{background-position:right top;}}
.{0} .d {{background-position:left bottom;}}
.{0} .e {{background-position:right bottom;}}
.{0} .c, .{0} .f {{background-color:{5};height:{1}px;margin-right:{1}px;}}
.{0} .c b, .{0} .f b {{height:{2}px;background-color:{6};}}
.{0} .f b {{margin-top:{3}px;}}
.{0} .m {{background-color:{5};border-left:solid {2}px {6};border-right:solid {2}px {6};}}",
this.StyleGroupName,
this.Radius,
this.BorderWidth.Value,
this.Radius - this.BorderWidth.Value,
ResolveUrl(imageUrl),
this.BackColor.IsNamedColor ? this.BackColor.Name : "#" + this.BackColor.Name.Substring(2),
this.BorderColor.IsNamedColor ? this.BorderColor.Name : "#" + this.BorderColor.Name.Substring(2));
        //adds styles for the panel width, if it is required
        if (!this.Width.IsEmpty)
            sb.AppendLine(string.Format(".{0}{{width:{1}}}",
                this.StyleGroupName, this.Width));
        
        //adds styles for the panel height, if it is required
        if (!this.Height.IsEmpty && this.Height.Type == UnitType.Pixel)
            sb.AppendLine(string.Format(".{0} .m{{height:{1}px;overflow:visible}}",
                this.StyleGroupName, this.Height.Value - this.Radius * 2));
        //registers styles on the page
        if (!StylesController.IsStyleSheetIncludeRegistered(this.Page, "Rounded Panel"))
            StylesController.RegisterStyleSheetInclude(this.Page, "Rounded Panel", sb.ToString());
    }
    base.OnPreRender(e);
}
StylesController - a class that contains a few methods for registration of the styles created in runtime. It is described in the How to Register Stylesheet Created in Runtime article. Creation of HTML representing the control occurs in the overriden RenderControl method; the upper, bottom and middle parts are rendered separately.
public override void RenderControl(HtmlTextWriter writer)
{
    if (this.Radius > 0)
    {
        //renders the upper part of the control
        writer.Write(string.Format(
@"<div class='{0}'><b class='a'><b class='b'><b class='c'><b></b></b></b></b><div class='m'>", 
this.StyleGroupName));
        //renders inner controls
        base.RenderContents(writer);
        
        //renders the bottom part of the control
        writer.Write("</div><b class='d'><b class='e'><b class='f'><b></b></b></b></b></div>");
    }
    else
        base.RenderControl(writer);
}
The image representing curved corners is generated in the ProcessRequest method of the IHttpHandler implementation. It turns out to be the most complicated part of the work, as for me. The required image has to show a bordered circle filled with the specified background color, the rest of the image has to be transparent. It can be done by means of the transparent gif. My knowledge of GDI+ is rather superficial and examples found on the Internet are too complicated to simply copypaste. The Bob Powell's excellent resource on GDI+ and especially the article about peculiarities of the different images formats help me.
    Here is the way to draw partially transparent gif image for rounded corners:

  • First of all, you need to create a common bitmap in RGB format and draw the required image on its canvas. It is not possible to draw directly on gif-image because it contains indexed colors palette.
  • Create a gif-image of the same size that the image in RGB format.
  • Change color palette of the gif-image, it has to contain 3 colors: a transparent color, a border color and a background color.
  • Read colors pixel-by-pixel from the RGB-image and set colors for the corresponding images in the indexed image.
  • Save gif-image to the Response.
void IHttpHandler.ProcessRequest(HttpContext context)
{
    if (context.Request[radiusQueryKey] != null
        && context.Request[borderWidthQueryKey] != null
        && context.Request[borderColorQueryKey] != null
        && context.Request[backgroundColorQueryKey] != null)
    {
        int radius = int.Parse(context.Request[radiusQueryKey]);
        int borderWidth = int.Parse(context.Request[borderWidthQueryKey]);
        string[] args = context.Request[borderColorQueryKey].Split(new char[] { ',' });
        Color borderColor = Color.FromArgb(int.Parse(args[0]), int.Parse(args[1]), int.Parse(args[2]));
        args = context.Request[backgroundColorQueryKey].Split(new char[] { ',' });
        Color bgColor = Color.FromArgb(int.Parse(args[0]), int.Parse(args[1]), int.Parse(args[2]));
        //sets the transparent color
        Color transparentColor = Color.FromArgb(0, 0, 0, 0);
        //draws the image in the 32 bit RGB format
        Bitmap source = new Bitmap(radius * 2, radius * 2, PixelFormat.Format32bppRgb);
        Graphics g = Graphics.FromImage(source);
        g.FillRectangle(new SolidBrush(transparentColor), 0, 0, source.Width, source.Height);
        g.FillEllipse(new SolidBrush(bgColor), borderWidth / 2, borderWidth / 2, 
                source.Width - borderWidth, source.Height - borderWidth);
        g.DrawEllipse(new Pen(borderColor, borderWidth), 0 + borderWidth / 2, 0 + borderWidth / 2, 
                source.Width - borderWidth, source.Height - borderWidth);
        //creates the image in the indexed format
        Bitmap dest = new Bitmap(source.Width, source.Height, PixelFormat.Format8bppIndexed);
        
        //changes the color palette
        ColorPalette pal = dest.Palette;
        pal.Entries[0] = transparentColor;
        pal.Entries[1] = bgColor;
        pal.Entries[2] = borderColor;
        dest.Palette = pal;
        //sets a rectangle that occupies the whole picture area
        Rectangle rect = new Rectangle(0, 0, source.Width, source.Height);
        //locks the images in the memory
        BitmapData sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat);
        BitmapData destData = dest.LockBits(rect, ImageLockMode.WriteOnly, dest.PixelFormat);
        for (int x = 0; x < source.Width; x++)
        {
            for (int y = 0; y < source.Height; y++)
            {
                //reads color of the pixel with coordinates x,y  from the source image
                Color color = Color.FromArgb(Marshal.ReadInt32(sourceData.Scan0, sourceData.Stride * y + x * 4));
                //sets color of the corresponding pixel in the destination image
                if (color == bgColor)
                    Marshal.WriteByte(destData.Scan0, destData.Stride * y + x, 1);
                else if (color == borderColor)
                    Marshal.WriteByte(destData.Scan0, destData.Stride * y + x, 2);
                else
                    Marshal.WriteByte(destData.Scan0, destData.Stride * y + x, 0);
            }
        }
        //unlocks the images
        dest.UnlockBits(destData);
        source.UnlockBits(sourceData);
        source.Dispose();
        //writes the indexed image to the Response
        context.Response.Clear();
        context.Response.ContentType = "image/gif";
        dest.Save(context.Response.OutputStream, System.Drawing.Imaging.ImageFormat.Gif);
        dest.Dispose();
        context.Response.End();
    }
}
In the end it is required to register in web.config HttpHandlers that are responsible for drawing the image and the stylesheet registration.
<httpHandlers>
 ...
 <add verb="GET" path="roundedpanel.ashx" type="MyAssembly.MyNamespace.RoundedPanel, MyAssembly"/>
 <add verb="GET" path="stylesheet.css" type="MyAssembly.MyNamespace.StylesController, MyAssembly"/>
</httpHandlers>
Source code - 3.5 kB

No comments:

Post a Comment

Note: only a member of this blog may post a comment.