Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / C# 4.0

WinFormsX Part 1 of n

Rate me:
Please Sign up or sign in to vote.
4.84/5 (42 votes)
17 Aug 2012CPOL18 min read 67K   1.4K   91   44
A look at some of the exciting methods of the Windows Forms Extension library, like animations, graphic helpers, and a modern UI form.

The WinFormsX library - Demo application

Introduction

The Windows Forms technology is the oldest presentation layer implemented in the .NET Framework. When Microsoft announced the Avalon project, many people predicted the death of the Windows Forms technology. Nowadays both technologies, WPF (formerly known as the Avalon project) and Windows Forms, exist in a more or less friendly co-existence. Before the WinRT applications have been announced, some rumors spread that Microsoft plans to write such applications in a new way, resulting in the death of WPF.

Now that the way of creating Windows 8 applications has been shown we know the truth: The future for Modern UI applications is WPF with XAML. However, many companies still have big applications running on Windows Forms and some people plan to continue programming and / or maintaining Windows Forms applications in the (near) future.

Therefore it would be nice to have some of the cool features of WPF available in Windows Forms. Of course a direct duplicate does not make sense at all. This article will present an approach of extending the current possibilities of Windows Forms applications with helper (mostly extension) methods. The library that is presented here is open source, with the source code being available on GitHub. Participation in the project is more than welcome. If you have some ideas, a cool and helpful code snippet, or find some improvement or bug, then just push your changes, write me an email or write a comment.

The whole library is also available on NuGet, so that being up to date is an easy task. The latest stable build will be published on NuGet as soon as possible.

Background

One thing that is missing in the basic Windows Forms framework is the possibility to do animations. Of course this is not provided by the Windows API, however, the Windows API (and therefore the .NET-Framework) offers everything to perform nice animations. All we need is a timer and the possibility to change values together with a repaint of the whole control / form.

Another thing that is missing are really helpful methods in the GDI+ sections. Why is there no rectangle with rounded corners? This can be implemented quite easily and saves some time. Also other graphic helpers like finding the dominant color, drawing shadows or smoothing edges could have been implemented in the framework.

This list could be easily continued. In this (first) article we will therefore focus on the following things:

  • Animations (implementation in the library and practical examples)
  • A new Form template (implementation and examples)
  • The Graphics extension methods DrawRoundRectangle() and FillRoundRectangle() (implementation and examples)
  • The Image extension method ChangeColor() (implementation and examples)

The other extension methods and possibilities will be discussed in different articles on the WinFormsX library, which will be published in the next couple of months.

Animate

The Animate() method is an extension method of the Control class. Therefore any Control or Form or derived object in general can execute the Animate() method. The method accepts parameters specifying the properties to be changed with their final values, duration of the animation, the used easing method and callback delegates. There are several overloads:

C#
public static void Animate(this Control c, object properties);

public static void Animate(this Control c, object properties, int duration);

public static void Animate(this Control c, object properties, int duration, Easing easing);

public static void Animate(this Control c, object properties, int duration, Easing easing, Action complete);

The default duration is set to 200ms with the default easing option being linear. The properties (together with their final values) will be supplied as an anonymous object. We will discuss specific examples later on.

Implementation

Animations are rather easy to implement. We have to calculate the difference of the final value to the current value and divide it by the number of steps in between. In each step we will add the calculated value to the current value. What we have to determine right now are the following questions:

  • Which property or properties to change?
  • What is the final value?
  • What is the current value?
  • What is one step?

Most of those questions lead us to reflection. We have to perform reflection to obtain the name of the property that has to be changed. The field, i.e. the property, we are currently looking at contains the final value. Now we have to look at the property of the Control to get information about its current value. Since we cannot do comparisons (or animations) with very complex values (in general), everything here will be restricted to structs like user defined value types that sit on the stack or elementary data types like Int32 or Double.

The problem now is: Once we used reflection to have a look at the current value, we have to do that again for looking at the value a second time, or changing the value. The reason is that we need to be informed about any value changes (what is if a user changes the value during the animation?) in every step. Since we are dealing with variables that have been allocated on the stack, i.e. so called value types, we will get copies of the original version in an assignment. Therefore we can only assign a value to the original field using reflection.

The basic code of the method looks like this:

C#
public static void Animate(this Control c, object properties, int duration, Easing easing, Action complete)
{
    var t = new Timer();
    t.Interval = 30;
    var frame = 0;
    var maxframes = (int)Math.Ceiling(duration / 30.0);
    var reflection = properties.GetType();
    var target = c.GetType();
    var props = reflection.GetProperties();
    var values = new object[props.Length, 2];

    for (int i = 0; i < props.Length; i++)
    {
        var prop = props[i];
        values[i, 1] = prop.GetValue(properties, null);
        var exist = target.GetProperty(prop.Name);

        if (exist == null)
            throw new ArgumentException("Invalid property to animate. " + 
               "The given properties have to match a property of the control.");
        values[i, 0] = exist.GetValue(c, null);
    }

    t.Tick += (s, e) =>
    {
        //Here we have the logic to handle for each step!
    };

    t.Start();
}

All we did until here was reading out values and generating a two dimensional object array with initial (current) values of the control and final values that have been set by the anonymous object that have been passed as an argument. The Tick event has been handled by a lambda method. This has been done to share some calculates values of the function with the handler by a closure mechanism. The interval is set to 30ms, so that 1s of animation contains 34 frames.

The code that is executed every 30ms. A very simple form of the code is the following:

C#
frame++;

for (int i = 0; i < props.Length; i++)
{
    var start = Convert.ToDouble(values[i, 0]);
    var end = Convert.ToDouble(values[i, 1]);
    var value = easing.CalculateStep(frame, maxframes, start, end);

    if (values[i, 0] is int)
        target.GetProperty(props[i].Name).SetValue(c, (int)value, null);
    else if (values[i, 0] is int)
        target.GetProperty(props[i].Name).SetValue(c, (long)value, null);
    else if (values[i, 0] is float)
        target.GetProperty(props[i].Name).SetValue(c, (float)value, null);
    else if (values[i, 0] is decimal)
        target.GetProperty(props[i].Name).SetValue(c, (decimal)value, null);
    else
        target.GetProperty(props[i].Name).SetValue(c, value, null);
}

if (frame == maxframes)
{
    t.Stop();

    if (complete != null)
        complete();
}

This code version does not support any nested fields or properties, like Point or Size. Therefore the version in the code uses a more advanced (internal and specialized) class called ReflectionCache. This class is useful for two things:

  1. Performance benefit by avoiding massive usage of reflection (it just tries to perform it once in the beginning and then caches all the reflection objects)
  2. Managing lists of properties with fixed start and end values

The new code of the Animate() method looks like this (the two dimensional object array has been replaced with an array of more specialized objects):

C#
public static void Animate(this Control c, object properties, int duration, Easing easing, Action complete)
{
    var t = new Timer();
    t.Interval = 30;
    var frame = 0;
    var maxframes = (int)Math.Ceiling(duration / 30.0);
    var reflection = properties.GetType();
    var target = c.GetType();
    var props = reflection.GetProperties();
    var values = new ReflectionCache[props.Length];

    for (int i = 0; i < props.Length; i++)
    {
        values[i] = new ReflectionCache(target.GetProperty(props[i].Name));
        values[i].SetStart(values[i].Info.GetValue(c, null));
        values[i].SetEnd(props[i].GetValue(properties, null));
    }

    t.Tick += (s, e) =>
    {
        frame++;

        for (int i = 0; i < values.Length; i++)
        {
            values[i].Execute(c, easing, frame, maxframes);
        }

        if (frame == maxframes)
        {
            t.Stop();

            if (complete != null)
                complete();
        }
    };

    t.Start();
}

Here everything is done in the Execute() method of our ReflectionCache class.

A feature of this implementation is the possibility to create custom easing methods. This is quite straight forward: By inheriting from the abstract class Easing we implement our own easing methods. The base class has the following code:

C#
public abstract class Easing
{
    public abstract double CalculateStep(int frame, int frames, double start, double end);

    static Easing linear;
    static Easing sinus;

    /* Provides access to the standard easing methods with a Singleton pattern */
    public static Easing Linear
    {
        get
        {
            if (linear == null)
                linear = new LinearEasing();

            return linear;
        }
    }

    public static Easing Sinus
    {
        get
        {
            if (sinus == null)
                sinus = new SinusEasing();

            return sinus;
        }
    }
}

The important method here is CalculateStep(). Implementing a linear easing is as easy as writing the following code:

C#
public class LinearEasing : Easing
{
    public override double CalculateStep(int frame, int frames, double start, double end)
    {
        return start + frame * (end - start) / frames;
    }
}

More complex easings might require more code, but will in principle also do the same: Calculate a new value based on the information of the current frame index, the total number of frames, the initial and the final value of the property that is animated.

Examples

A very simple example to start with:

C#
/* Standard usings */

//Including this namespace is REALLY important
using System.Windows.FormsX;

public MyForm : Form
{
    public MyForm()
    {
        // Create a button
        Button bt = new Button();
        bt.Location = new Point(0,0);
        bt.Text = "Button!";
        bt.Width = 100;
        this.Controls.Add(bt);

        // Animate the button!
        bt.Animate(new { Width = 500 }, 2000);
    }
}

Here we create a Button control on the fly and animate the Width property from 100px to 500px in 2s. In the next examples we will leave out the Form class, constructor and other (maybe) relevant methods to concentrate on the animation context.

We can also do the same with the Form itself. In this example we apply the animation to the this object, which represents our current window:

C#
this.Animate(new { Opacity = 0.2 }, 500, Easing.Sinus);

Here we applied a sinus like animation (oscillating between -1 and 1, i.e. we will also go into the opposite direction while animation) on the opacity of the window. The animation will take 500ms and end at 20% opacity (equals 80% transparency).

Let's have now a look at a more complicated example. We could animate the location of a Form instance. Here we need to do it by using a struct, since the X and Y properties are read-only:

C#
this.Animate(
    new { Location = new Point(400, 300) }, /* This should be the final value */
    2000, /* Duration in ms of the animation */
    Easing.Linear, /* We use a linear easing */
    () => { MessageBox.Show("I am finished!"); } /* The method that is called after the animation finished */
);

The callback for the complete event can also be specified by using a delegate. Using a lambda expression can bring some benefits or may be required dependent on the problem. The signature of any delegate (or lambda) expression is given by:

C#
void AnimateCallback();

The specified animation method will change the location of the form in linear steps within 2 seconds. Right after those two seconds we call the given lambda method. This will show a MessageBox with the text I am finished!.

WindowX

One of the coolest features of WPF is the ability to really design windows (or let's call them forms) in a unique way. Forget about those three buttons up there or the icon on the left. Also forget about the bar on top with the text of the form. We can really adjust the space we've been given to our needs or design wishes. Of course with great power comes great responsibility, which means that the ability to render own designs has also resulted in really bad designs out there.

If we focus on an established design style we would probably want a form to look like the Zune or GitHub for Windows client. Those two applications have been built with XAML in WPF and do have a very Modern UI look. So they are no Windows 8 applications, and they do not require the full screen to run. They still run in a (now chrome-less) window, but have slick, modified design, which uses white-spaces and font to deliver content, information and services.

Totally duplicating such a style is of course not possible (we could go for it, but in the end all our effort would just result in a copy of WPF - why not use the original then?). If we go for a naive way and set the FormBorderStyle to None we might think that this would lead to the desired outcome. Unfortunately this does not work as expected. One of the reasons for that is that the window does not behave as usual. One example can be found when dragging the window to the right border of the screen. Usually Windows 7 will try to set the window to the location and size of half the screen. This will not happen with such a modified form.

The trick here is to use a normal form and modify it by calling Win32 APIs. We will call such APIs to remove all chrome and to add some shadow behind our form. Everything that can be seen on the form can be changed: The visibility and colors of the three buttons (minimize, maximize and close) and the visibility of the size grip image. Using the form is as easy as doing one of the following operations:

C#
// Plain without using the Namespace
public class MyForm : System.Windows.FormsX.WindowX
{
    /* my code */
}

// Plain with using the namespace
using System.Windows.FormsX;
public class MyForm : WindowX
{
    /* my code */
}

// Probably in combination with the designer and using the namespace
using System.Windows.FormsX;
public partial class MyForm : WindowX
{
    /* my code */
}

// Direct creation without any designer - all at runtime
var myform = new System.Windows.FormsX.WindowX();
var button = new Button();
button.Location = new Point(50, 50);
button.Text = "Click me!";
button.Size = new Size(200, 100);
button.Click += (sender, e) => { MessageBox.Show("Hi there!"); };
myform.Controls.Add(button);

The program's logo and other stuff has to be added by the user. The WindowX form does not make any assumption about the planned usage.

Implementation

The implementation makes heavy use of Win32 API calls. This is required to give the form some shadow and handle resizing as well as moving events. A future goal of the library is to pack the most important Win32 API calls into managed object oriented wrappers, i.e. meaningful classes. This would result in less work for a developer, who needs to search the API call, include it and provide correct data layouts for using it. Currently only a lot of Win32 API calls have been included in the library, however, no classes (besides static ones which just include the PInvoke statements) have been written yet.

Let's have a rough look at the implementation before we discuss it:

C#
/* Standard Namespaces */
using System.Windows.API;
using System.Runtime.InteropServices;
using System.DrawingX;

namespace System.Windows.FormsX
{
    public partial class WindowX : Form
    {
        // Windows API Constants
        // Static members
        // Class members

        public WindowX()
        {
            /* Initializes the variables */
            // Sets the handlers:
            MouseMove += new MouseEventHandler(OnMouseMove);
            MouseDown += new MouseEventHandler(OnMouseDown);
            Activated += new EventHandler(OnActivated);
            // Always redraw
            SetStyle(ControlStyles.ResizeRedraw, true);
            // Use Segoe UI as font
            Font = new Font("Segoe UI", 8f, FontStyle.Regular, GraphicsUnit.Point);
            // Standard BackColor is White
            BackColor = Color.White;
            // Use the center of the screen as standard start position
            StartPosition = FormStartPosition.CenterScreen;
            /* Other standard stuff */
        }

        /* Other standard stuff */
        
        public Color ButtonColor
        {
            get { return _buttonColor; }
            set
            {
                _buttonColor = value;
                SetButtonNormalImages();
            }
        }

        public Color HoverColor
        {
            get { return _hoverColor; }
            set
            {
                _hoverColor = value;
                SetButtonHoverImages();
            }
        }

        public new bool MinimizeBox
        {
            get { return base.MinimizeBox; }
            set
            {
                base.MinimizeBox = value;
                _min.Visible = value;
            }
        }

        public new bool MaximizeBox
        {
            get { return base.MaximizeBox; }
            set
            {
                base.MaximizeBox = value;
                _max.Visible = value;
            }
        }

        public bool CloseBox
        {
            get { return _close.Visible; }
            set
            {
                _close.Visible = value;
            }
        }

        public bool AeroEnabled
        {
            get { return _aeroEnabled; }
        }

        /* Some internal properties */
        /* Some more or less interesting methods */
        /* Lots of Win32 API calls over PInvoke */

        // Here we paint the size grip
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if(WindowState != FormWindowState.Maximized && SizeGripStyle != SizeGripStyle.Hide)
                e.Graphics.DrawImage(Images.resize, new Point(Width - 10, Height - 10));
        }

        // What happens on hovering a button (minimize, maximize and close button)
        void OnMouseHover(object sender, EventArgs e)
        {
            var bt = sender as Button;
            var t = bt.BackgroundImage;
            bt.BackgroundImage = bt.Tag as Bitmap;
            bt.Tag = t;
        }

        // Set the buttons on hover with the selected color
        void SetButtonHoverImages()
        {
            _min.Tag = Images.min.ChangeColor(startColor, _hoverColor);
            _max.Tag = Images.max.ChangeColor(startColor, _hoverColor);
            _close.Tag = Images.close.ChangeColor(startColor, _hoverColor);
        }

        // Set the normal buttons with the selected color
        void SetButtonNormalImages()
        {
            _min.BackgroundImage = Images.min.ChangeColor(startColor, _buttonColor);
            _max.BackgroundImage = Images.max.ChangeColor(startColor, _buttonColor);
            _close.BackgroundImage = Images.close.ChangeColor(startColor, _buttonColor);
        }

        /* Minimize, Maximize and Close Handlers */
    }
}

The form currently uses the same images for the minimize, maximize and close buttons as the GitHub client for Windows. In the future the images might be exchangeable. At the moment only the color of the non-hovered button (a light gray) and the hovered button (steel blue) can be changed. The ability to hide the close button (without hiding the minimize and maximize buttons) is additional to the usual Form.

Let's have a look at the implementation of the resize action. First of all we need to place some event handlers with the following methods:

C#
// Mouse down event on the form directly (either move the form or resize it)
void OnMouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        if (Width - BORDERWIDTH > e.Location.X && e.Location.X > BORDERWIDTH && e.Location.Y > BORDERWIDTH)
        {
            MoveControl(Handle);
        }
        else if (this.WindowState != FormWindowState.Maximized)
        {
            System.Diagnostics.Debug.WriteLine(e.Location.ToString());
            ResizeForm(_resizeDir);
        }
    }
}

// Mouse move event on the form in case of resize
void OnMouseMove(object sender, MouseEventArgs e)
{        
    //Calculate which direction to resize based on mouse position

    if (e.Location.X < BORDERWIDTH & e.Location.Y < BORDERWIDTH)
        ResizeDirection = ResizeDirection.TopLeft;
    else if (e.Location.X < BORDERWIDTH & e.Location.Y > Height - BORDERWIDTH)
        ResizeDirection = ResizeDirection.BottomLeft;
    else if (e.Location.X > Width - BORDERWIDTH & e.Location.Y > Height - BORDERWIDTH)
        ResizeDirection = ResizeDirection.BottomRight;
    else if (e.Location.X > Width - BORDERWIDTH & e.Location.Y < BORDERWIDTH)
        ResizeDirection = ResizeDirection.TopRight;
    else if (e.Location.X < BORDERWIDTH)
        ResizeDirection = ResizeDirection.Left;
    else if (e.Location.X > Width - BORDERWIDTH)
        ResizeDirection = ResizeDirection.Right;
    else if (e.Location.Y < BORDERWIDTH)
        ResizeDirection = ResizeDirection.Top;
    else if (e.Location.Y > Height - BORDERWIDTH)
        ResizeDirection = ResizeDirection.Bottom;
    else
        ResizeDirection = ResizeDirection.None;
}

The BORDERWIDTH is a constant that determines the area at the border, where the resize cursor appears. Basically we differentiate between a form move and a form resize event. The constant helps us to distinguish between the two events by clearly fixing the two areas. All those efforts would be useless, unless we would also extend the internal message handler:

C#
protected override void WndProc(ref Message m)
{
    int WM_NCCALCSIZE = 0x83;
    int WM_NCHITTEST = 0x84;
    IntPtr result = default(IntPtr);

    int dwmHandled = Window.DwmDefWindowProc(m.HWnd, m.Msg, m.WParam, m.LParam, out result);
    
    if (dwmHandled == 1)
    {
        m.Result = result;
        return;
    }

    if (m.Msg == WM_NCCALCSIZE && m.WParam.ToInt32() == 1)
    {
        NCCALCSIZE_PARAMS nccsp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));

        // Adjust (shrink) the client rectangle to accommodate the border:
        nccsp.rect0.Top += 0;
        nccsp.rect0.Bottom += 0;
        nccsp.rect0.Left += 0;
        nccsp.rect0.Right += 0;

        if (!_marginOk)
        {
            _dwmMargins.cyTopHeight = 0;
            _dwmMargins.cxLeftWidth = 0;
            _dwmMargins.cyBottomHeight = 3;
            _dwmMargins.cxRightWidth = 0;
            _marginOk = true;
        }

        Marshal.StructureToPtr(nccsp, m.LParam, false);
        m.Result = IntPtr.Zero;
    }
    else if (m.Msg == WM_NCHITTEST && m.Result.ToInt32() == 0)
    {
        m.Result = HitTestNCA(m.HWnd, m.WParam, m.LParam);
    }
    else
    {
        base.WndProc(ref m);
    }
}

The HitTestNCA() is our method and has the purpose of finding out which part of the form has been hit. The result of this operation will change the result of the message examination, which will in the end being passed to the corresponding Win32 API. Therefore our code will do everything required to perform a (standard) form resizing operation.

Most of the credit in implementing this form goes to lipinho. He wrote a nice article here at the CodeProject. His article illustrates the basic Win32 API calls, which are necessary to create such a form.

Examples

The simplest way of using this new kind of form is displayed below:

C#
using System.Windows.FormsX;

public partial class MetroForm : WindowX
{
    public MetroForm()
    {
        InitializeComponent();
    }
}

The WindowX class can be used together with the designer. Properties are assigned to special groups, so that editing values from within the designer is easily possible. However, we can also adjust the properties within the code / at runtime. Let's view an example:

C#
using System.Drawing;
using System.Windows.Forms;
using System.Windows.FormsX;

public partial class MetroForm : WindowX
{
    public MetroForm()
    {
        Size = new Size(400, 400);
        var bt = new Button();
        bt.Location = new Point(100, 100);
        bt.Size = new Size(200, 200);
        bt.Text = "Click to change properties";
        bt.Click += new EventHandler(ButtonClicked);
        Controls.Add(bt);
    }

    void ButtonClicked(object sender, EventArgs e)
    {
        ButtonColor = Color.Red;
        HoverColor = Color.Green;
        MinimizeBox = false;
        MaximizeBox = false;
    }
}

Here we alter the colors of the close button, since we hide the minimize and the maximize button. The close button has now red as default color, while being green when the mouse is over it.

RoundRectangle

In the beginning of the Windows Forms technology the GDI+ classes have been quite popular. Most people liked the GDI wrapper, since it offered many methods out of the box and simplified many drawing operations. Manipulating images using the included GDI+ methods was a blessing and resulting in great applications like Paint.NET.

For our method we need to distinguish between drawing and filling the rounded rectangle. The first operation just draws a border (using a pen), while the second operation fills the rounded rectangle using a brush. There are several overloads for drawing a round rectangle:

C#
public static void DrawRoundRectangle(this Graphics g, Pen pen, RectangleF rectangle, params float[] radius);

public static void DrawRoundRectangle(this Graphics g, Pen pen, Rectangle rectangle, params float[] radius);

public static void DrawRoundRectangle(this Graphics g, Pen pen, PointF point, SizeF size, params float[] radius);

The radius is passed as a params array, i.e. we can basically specify as much numbers as we want. If we specify no numbers, then the method is equivalent to the DrawRectangle() method that is provided out of the box. The maximum number of significant numbers is four, since we just have four corners to round. If less than four numbers are provided then the provided numbers will be periodically retaken. This means that providing one number results in using this number for each corner. If two numbers are provided the odd and the even corners will have the same number as radius set. If three numbers are provided then we have the first three corner unique while the forth corner has again the radius of the first corner.

The next methods of interest are the ones for filling a rounded rectangle. Those methods look the same as the ones for drawing the rounded rectangle. One key difference is that those methods use a brush instead of a pen:

C#
public static void FillRoundRectangle(this Graphics g, Brush brush, Rectangle rectangle, params float[] radius);

public static void FillRoundRectangle(this Graphics g, Brush brush, RectangleF rectangle, params float[] radius);

public static void FillRoundRectangle(this Graphics g, Brush brush, PointF point, SizeF size, params float[] radius);

All those methods are internally building upon a rounded rectangle path, which is an extension of the GraphicsPath object. This extension can be also used in any code, which uses the WinFormsX library. The possible signatures for invoking this method look like:

C#
public static GraphicsPath AddRoundRectangle(this GraphicsPath gp, Rectangle rectangle, params float[] radius);

public static GraphicsPath AddRoundRectangle(this GraphicsPath gp, RectangleF rectangle, params float[] radius);

public static GraphicsPath AddRoundRectangle(this GraphicsPath gp, PointF point, SizeF size, params float[] radius);

The rounded rectangle is a nice extension to the usual methods DrawRectangle() and FillRectangle(). With the AddRoundRectangle method of the GraphicsPath we are can easily create fancy speech bubbles and other objects that require rounded corners.

Implementation

The implementation of the round rectangle is based on a GraphicsPath. This allows us to re-use most of the code. The only thing we still need to do is to either draw the path using DrawPath() or fill the path using FillPath(). Let's have a look at the DrawRoundRectangle() method:

C#
public static void DrawRoundRectangle(this Graphics g, Pen pen, PointF point, SizeF size, params float[] radius)
{
    var gp = new GraphicsPath().AddRoundRectangle(point, size, radius);
    g.DrawPath(pen, gp);
}

Therefore the most important method is the AddRoundRectangle() extension to the GraphicsPath object. Here we specify the radius values for each corner, the location of the top left corner and the size of the rectangle. The code is shown below:

C#
public static GraphicsPath AddRoundRectangle(this GraphicsPath gp, PointF point, SizeF size, params float[] radius)
{
    var r = new float[4];
    var d = new float[4];
    var bottom = point.Y + size.Height;
    var right = point.X + size.Width;

    for (int i = 0; i < r.Length; i++)
    {
        r[i] = radius.Length > 0 ? radius[i % radius.Length] : 0f;
        d[i] = 2f * r[i];
    }

    if (r[0] > 0f)
        gp.AddArc(new RectangleF(point.X, point.Y, d[0], d[0]), 180, 90);

    gp.AddLine(new PointF(point.X + r[0], point.Y), new PointF(right - r[1], point.Y));

    if (r[1] > 0f)
        gp.AddArc(new RectangleF(right - d[1], point.Y, d[1], d[1]), 270, 90);

    gp.AddLine(new PointF(right, point.Y + r[1]), new PointF(right, bottom - r[2]));

    if (r[2] > 0f)
        gp.AddArc(new RectangleF(right - d[2], bottom - d[2], d[2], d[2]), 0, 90);

    gp.AddLine(new PointF(right - r[2], bottom), new PointF(point.X + r[3], bottom));

    if (r[3] > 0f)
        gp.AddArc(new RectangleF(point.X, bottom - d[3], d[3], d[3]), 90, 90);

    gp.AddLine(new PointF(point.X, bottom - r[3]), new PointF(point.X, point.Y + r[0]));
    gp.CloseFigure();
    return gp;
}

We have to check each radius value if it is greater than zero. If we have a zero value we just connect the the specific corner to the next corner.

Examples

Using the Graphics extensions is as easy as having an instance of a Graphics object with the referenced WinFormsX library and the namespace System.DrawingX. Let's have a look at a very basic example:

C#
Bitmap bmp = new Bitmap(640, 480);
using(Graphics g = Graphics.FromImage(bmp))
{
    g.DrawRoundRectangle(Pens.Black, new Rectangle(100, 100, 200, 200), 10f);
}
bmp.Save(@"C:\Example.bmp");

Here we have created a new bitmap, which is then used to draw a round rectangle in the middle. Finally we save the image in a file Example.bmp. Let's see a more complicated example:

C#
protected override void OnPaint(PaintEventArgs e)
{
    Graphics g = e.Graphics;
    // Set the rectangle
    var rect = new Rectangle(100, 100, 200, 200);
    // Set the radius values for the corners - results in 10 5 10 5 for top-left, top-right, bottom-right, bottom-left
    var radius = new [] { 10f, 5f };

    // Fill the rectangle
    g.FillRoundRectangle(SolidBrushes.ForestGreen, rect, radius);
    // Draw the rectangle
    g.DrawRoundRectangle(Pens.Black, rect, radius);
}

Here we override the OnPaint() method of a Form derived class. As with the usual Rectangle methods we first fill the drawing object, before we draw it. This is done to prevent hiding of the previously drawn border.

ChangeColor

A useful extension method for any Bitmap instance is the ChangeColor() method, which searches for a specific color in the image and replaces the color with a given color.

There is only one signature available. The signature of the method reads:

C#
public static Bitmap ChangeColor(this Bitmap bitmap, Color source, Color destination);

So the method call is really short and simple. We just have to specify a source color, which has to be replaced and a destination color, which will be replaced.

Implementation

In order to stay competitive we either have to use some unsafe code or perform a Copy() of the unmanaged memory block with the static Marshal class. Finally we iterate through that byte array with the bytes per pixel value. If the current color is the color to be replaced we have to replace it. Finally we copy the managed byte array back to the original unmanaged memory source.

C#
public static Bitmap ChangeColor(this Bitmap bitmap, Color source, Color destination)
{
    var newBitmap = new Bitmap(bitmap);
    var data = newBitmap.LockBits(new Rectangle(Point.Empty, newBitmap.Size), ImageLockMode.ReadWrite, newBitmap.PixelFormat);
    var ptr = data.Scan0;

    // Declare an array to hold the bytes of the bitmap.
    var bytes = Math.Abs(data.Stride) * newBitmap.Height;
    var values = new byte[bytes];
    var bpp = data.Stride / data.Width;

    // Copy the RGB values into the array.
    Marshal.Copy(ptr, values, 0, bytes);

    for (var i = 0; i < bytes; i += bpp)
    {
        var color = Color.FromArgb(
            bpp == 4 ? values[i + 3] : 255, //Alpha
            values[i + 2], //Red
            values[i + 1], //Green
            values[i + 0]);//Blue

        if (color == source)
        {
            if (bpp == 4)
                values[i + 3] = destination.A;

            values[i + 2] = destination.R;
            values[i + 1] = destination.G;
            values[i + 0] = destination.B;
        }
    }

    Marshal.Copy(values, 0, ptr, bytes);
    newBitmap.UnlockBits(data);
    return newBitmap;
}

This kind of code is also used for finding the dominant color in a Bitmap object.

Examples

In this case there is no easy or complicated usage. Let's have a look at one example method call:

C#
Bitmap newBitmap = oldBitmap.ChangeColor(Color.Red, Color.Blue);

This example changes the red color (255, 0, 0, 0) to blue (0, 0, 255, 0). Since we are obtaining a new image, we could also store the returned instance of the new image in the old variable. This would look like this:

C#
bitmap = bitmap.ChangeColor(Color.Red, Color.Blue);

This has the advantage that the old resources should be cleaned up by the Garbage Collector some time in the future.

FindDominantColor

A quite similar method is the FindDominantColor() extension method of a Bitmap object. This method only has one signature without any arguments:

C#
public static Color FindDominantColor(this Bitmap bitmap);

The implementation is quite similar to the one before, however, there is one major difference.

Implementation

In order to stay competitive we either have to use some unsafe code or perform a Copy() of the unmanaged memory block with the static Marshal class. Finally we iterate through that byte array with the bytes per pixel value. If the current color is transparent we skip it. Otherwise we increment the current value in a Hashtable, which has Color values as keys.

C#
public static Color FindDominantColor(this Bitmap bitmap)
{
    var colorCount = new Hashtable();
    var maxCount = 0;
    var dominantColor = Color.White;
    var data = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadOnly, bitmap.PixelFormat);
    var ptr = data.Scan0;

    // Declare an array to hold the bytes of the bitmap.
    var bytes = Math.Abs(data.Stride) * bitmap.Height;
    var values = new byte[bytes];
    var bpp = data.Stride / data.Width;

    // Copy the RGB values into the array.
    Marshal.Copy(ptr, values, 0, bytes);

    for (var i = 0; i < bytes; i += bpp)
    {
        var color = Color.FromArgb(
            //Check required (there could be 3 bytes per pixel (no alpha) or 4) --- all other ones will be excluded...
            bpp == 4 ? values[i + 3] : 255, //Alpha
            values[i + 2], //Red
            values[i + 1], //Green
            values[i + 0]);//Blue

        // ignore fully transparent ones
        if (color.A == 0)
            continue;

        // ignore white
        if (color.Equals(Color.White))
            continue;

        var count = 1;

        if (colorCount.Contains(color))
        {
            count = (int)colorCount[color] + 1;
            colorCount[color] = count;
        }
        else
            colorCount.Add(color, count);

        // keep track of the color that appears the most times
        if (count > maxCount)
        {
            maxCount = count;
            dominantColor = color;
        }
    }

    bitmap.UnlockBits(data);
    return dominantColor;
}

In the end we release the resources to give power back to the memory manager (GC).

Examples

There is just one possible usage. A sample code could look like this:

C#
Bitmap bmp = Bitmap.FromFile(@"C:\Sample.jpg");
Color c = bmp.FindDominantColor();
MessageBox.Show(c.ToString());

This example loads the bitmap from a file called sample.jpg in the root directory of the drive C and finds its dominant color. Finally the values of the color are shown in a message box displayed on the screen.

Using the code

Using the WinFormsX library is possible in many ways. One possibility is by using the NuGet package manager. After installing the WinFormsX library to the solution, where we want to use it, we can include the required namespaces in any class or code file that we want to. Right now we just have three main namespaces:

C#
using System.WebX;           /* For Uri extensions */
using System.DrawingX;       /* For Bitmap, Image, GraphicsPath and Graphics extensions */
using System.Windows.FormsX; /* For WindowX and Animate extension */

Another possibility is by downloading the WinFormsX.dll and referencing it in the corresponding project. This is the less preferable option, since we have to search for updates ourselves. The NuGet package manager contains a lot of helpers for keeping our packages up to date. Therefore we can always work with the latest version of WinFormsX.

About the demo project

The demo has been built to show some of the features of WinFormsX. The main focus of the project does not lie in the demo itself, so just use it as it was intended: have a look at the code and play around with it! I will try to enhance the demo version(s) in the future, but until I find some time for those details, I cannot promise a better demo version. If you built a better demo, then just push it into the Git repository. I would be more than happy to accept those commits.

Points of Interest

This is the first article on the WinFormsX library. My goal was to present possibilities and give an outlook on the development of the library. The library is currently in an incomplete alpha state, i.e. there are still many parts missing or incomplete. Participation in the project would be greatly appreciated and can be given in any form (direct code contributions, comments, votes, downloading the library, emails and such). The main point is to get ideas going and implement them to build a library, which is really useful for writing Windows Forms applications.

History

  • v1.0.0 | Initial release | 14.08.2012.
  • v1.0.1 | Fixed some typos and corrected some statements | 15.08.2012.
  • v1.1.0 | Added information about FindDominantColor() | 15.08.2012.
  • v1.1.1 | Renamed the article (consistency issue) | 16.08.2012.
  • v1.1.2 | Fixed some typos | 18.08.2012.

License

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


Written By
Chief Technology Officer
Germany Germany
Florian lives in Munich, Germany. He started his programming career with Perl. After programming C/C++ for some years he discovered his favorite programming language C#. He did work at Siemens as a programmer until he decided to study Physics.

During his studies he worked as an IT consultant for various companies. After graduating with a PhD in theoretical particle Physics he is working as a senior technical consultant in the field of home automation and IoT.

Florian has been giving lectures in C#, HTML5 with CSS3 and JavaScript, software design, and other topics. He is regularly giving talks at user groups, conferences, and companies. He is actively contributing to open-source projects. Florian is the maintainer of AngleSharp, a completely managed browser engine.

Comments and Discussions

 
GeneralMy Vote 5 Pin
Shemeemsha (ഷെമീംഷ)7-Oct-14 19:02
Shemeemsha (ഷെമീംഷ)7-Oct-14 19:02 
GeneralRe: My Vote 5 Pin
Florian Rappl7-Oct-14 20:03
professionalFlorian Rappl7-Oct-14 20:03 
GeneralMy vote of 5 Pin
Amir Mohammad Nasrollahi28-Jul-13 4:01
professionalAmir Mohammad Nasrollahi28-Jul-13 4:01 
GeneralRe: My vote of 5 Pin
Florian Rappl28-Jul-13 9:24
professionalFlorian Rappl28-Jul-13 9:24 
GeneralMy vote of 5 Pin
DotNetMastermind24-Nov-12 13:07
DotNetMastermind24-Nov-12 13:07 
GeneralRe: My vote of 5 Pin
Florian Rappl24-Nov-12 23:07
professionalFlorian Rappl24-Nov-12 23:07 
QuestionCan't compile Pin
Niksa Lovrinic2-Nov-12 5:54
Niksa Lovrinic2-Nov-12 5:54 
AnswerRe: Can't compile Pin
Florian Rappl2-Nov-12 11:01
professionalFlorian Rappl2-Nov-12 11:01 
GeneralMy vote of 5 Pin
Mohammad A Rahman21-Oct-12 17:31
Mohammad A Rahman21-Oct-12 17:31 
GeneralRe: My vote of 5 Pin
Florian Rappl21-Oct-12 20:09
professionalFlorian Rappl21-Oct-12 20:09 
QuestionThank you Pin
shuiwuhen2121-Sep-12 3:51
shuiwuhen2121-Sep-12 3:51 
AnswerRe: Thank you Pin
Florian Rappl21-Sep-12 5:20
professionalFlorian Rappl21-Sep-12 5:20 
GeneralMy vote of 5 Pin
Md. Marufuzzaman14-Sep-12 2:03
professionalMd. Marufuzzaman14-Sep-12 2:03 
GeneralRe: My vote of 5 Pin
Florian Rappl14-Sep-12 10:40
professionalFlorian Rappl14-Sep-12 10:40 
QuestionThank you Pin
Albarhami13-Sep-12 4:09
Albarhami13-Sep-12 4:09 
AnswerRe: Thank you Pin
Florian Rappl13-Sep-12 4:31
professionalFlorian Rappl13-Sep-12 4:31 
GeneralMy vote of 5 Pin
Albarhami13-Sep-12 4:08
Albarhami13-Sep-12 4:08 
GeneralRe: My vote of 5 Pin
Florian Rappl13-Sep-12 4:31
professionalFlorian Rappl13-Sep-12 4:31 
GeneralMy vote of 5 Pin
javedsmart11-Sep-12 21:40
javedsmart11-Sep-12 21:40 
GeneralRe: My vote of 5 Pin
Florian Rappl12-Sep-12 2:06
professionalFlorian Rappl12-Sep-12 2:06 
QuestionDwmapi.dll is Missing Pin
ahsan19971522-Aug-12 14:44
ahsan19971522-Aug-12 14:44 
AnswerRe: Dwmapi.dll is Missing Pin
Florian Rappl22-Aug-12 19:44
professionalFlorian Rappl22-Aug-12 19:44 
QuestionNice Pin
BillW3316-Aug-12 7:36
professionalBillW3316-Aug-12 7:36 
AnswerRe: Nice Pin
Florian Rappl16-Aug-12 7:47
professionalFlorian Rappl16-Aug-12 7:47 
GeneralMy vote of 5 Pin
sam.hill15-Aug-12 17:17
sam.hill15-Aug-12 17:17 

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.