Themed Image Button in .NETCF

In this article I’d like to demonstrate how to create a themed image button control. I’ll be using the same techniques in my previous article, How to draw a textured rounded rectangle and Semi-Transparent Controls in .NETCF.

First, we need to define our theme. We get the start and end gradient colors from alpha blending the SystemColors.Highlight color and Color.White

class Theme
{
    public static Color AlphaBlend(Color value1, Color value2, int alpha)
    {
        int ialpha = 256 - alpha;
        return Color.FromArgb((value1.R * alpha) + (value2.R * ialpha) >> 8,
                              (value1.G * alpha) + (value2.G * ialpha) >> 8,
                              (value1.B * alpha) + (value2.B * ialpha) >> 8);
    }
 
    public static Color GradientLight
    {
        get
        {
            var color = AlphaBlend(SystemColors.Highlight, Color.White, 100);
            return AlphaBlend(Color.White, color, 50); ;
        }
    }
 
    public static Color GradientDark
    {
        get
        {
            var color = AlphaBlend(SystemColors.Highlight, Color.Black, 256);
            return AlphaBlend(Color.White, color, 50); ;
        }
    }
}

We’ll be using the GradientFill method as well in this example. For this we need to define 2 structures, TRIVERTEX and GRADIENT_RECT

struct TRIVERTEX
{
    private int x;
    private int y;
    private ushort Red;
    private ushort Green;
    private ushort Blue;
    private ushort Alpha;
 
    public TRIVERTEX(int x, int y, Color color)
        : this(x, y, color.R, color.G, color.B, color.A)
    {
    }
 
    public TRIVERTEX(
        int x, int y,
        ushort red, ushort green, ushort blue,
        ushort alpha)
    {
        this.x = x;
        this.y = y;
        Red = (ushort)(red << 8);
        Green = (ushort)(green << 8);
        Blue = (ushort)(blue << 8);
        Alpha = (ushort)(alpha << 8);
    }
}
 
struct GRADIENT_RECT
{
    private uint UpperLeft;
    private uint LowerRight;
 
    public GRADIENT_RECT(uint ul, uint lr)
    {
        UpperLeft = ul;
        LowerRight = lr;
    }
}

Using the 2 structures above we can now define our P/Invoke for GradientFill

[DllImport("coredll.dll")]
static extern bool GradientFill(
    IntPtr hdc, 
    TRIVERTEX[] pVertex, 
    int dwNumVertex, 
    GRADIENT_RECT[] pMesh, 
    int dwNumMesh, 
    int dwMode);

Let’s wrap the P/Invoke call to GradientFill in a nice method

const int GRADIENT_FILL_RECT_V = 0x00000001;
 
public static void GradientFill(
    this Graphics graphics,
    Rectangle rect,
    Color startColor,
    Color endColor)
{
    var tva = new TRIVERTEX[2];
    tva[0] = new TRIVERTEX(rect.X, rect.Y, startColor);
    tva[1] = new TRIVERTEX(rect.Right, rect.Bottom, endColor);
    var gra = new GRADIENT_RECT[] { new GRADIENT_RECT(0, 1) };
 
    var hdc = graphics.GetHdc();
    GradientFill(hdc, tva, tva.Length, gra, gra.Length, GRADIENT_FILL_RECT_V);
    graphics.ReleaseHdc(hdc);
}

In order to use the function we need to create a few GDI objects: a Pen to draw the border, and a Brush to fill the rectangle. We will mostly use P/Invoke for creating and releasing GDI objects

const int PS_SOLID = 0;
const int PS_DASH = 1;
 
[DllImport("coredll.dll")]
static extern IntPtr CreatePen(int fnPenStyle, int nWidth, uint crColor);
 
[DllImport("coredll.dll")]
static extern int SetBrushOrgEx(IntPtr hdc, int nXOrg, int nYOrg, ref Point lppt);
 
[DllImport("coredll.dll")]
static extern IntPtr CreateSolidBrush(uint color);
 
[DllImport("coredll.dll")]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobject);
 
[DllImport("coredll.dll")]
static extern bool DeleteObject(IntPtr hgdiobject);
 
[DllImport("coredll.dll")]
static extern IntPtr CreatePatternBrush(IntPtr HBITMAP);
 
[DllImport("coredll.dll")]
static extern bool RoundRect(
    IntPtr hdc, 
    int nLeftRect, 
    int nTopRect, 
    int nRightRect, 
    int nBottomRect, 
    int nWidth, 
    int nHeight);

We’ll draw a textured rounded rectangle with a gradient fill that uses the current theme’s Highlight color as the base color.

public static void DrawThemedGradientRectangle(
    this Graphics graphics,
    Pen border,
    Rectangle area,
    Size ellipseSize)
{
    using (var texture = new Bitmap(area.Right, area.Bottom))
    {
        using (var g = Graphics.FromImage(texture))
            GradientFill(g, area, Theme.GradientLight, Theme.GradientDark);
 
        FillRoundedTexturedRectangle(graphics, border, texture, area, ellipseSize);
    }
}
 
static IntPtr CreateGdiPen(Pen pen)
{
    var style = pen.DashStyle == DashStyle.Solid ? PS_SOLID : PS_DASH;
    return CreatePen(style, (int)pen.Width, GetColorRef(pen.Color));
}
 
public static void FillRoundedTexturedRectangle(
    this Graphics graphics,
    Pen border,
    Bitmap texture,
    Rectangle rect,
    Size ellipseSize)
{
    Point old = new Point();
 
    var hdc = graphics.GetHdc();
    var hpen = CreateGdiPen(border);
    var hbitmap = texture.GetHbitmap();
    var hbrush = CreatePatternBrush(hbitmap);
 
    SetBrushOrgEx(hdc, rect.Left, rect.Top, ref old);
    SelectObject(hdc, hpen);
    SelectObject(hdc, hbrush);
 
    RoundRect(hdc, rect.Left, rect.Top, rect.Right, rect.Bottom, ellipseSize.Width, ellipseSize.Height);
 
    SetBrushOrgEx(hdc, old.Y, old.X, ref old);
    DeleteObject(hpen);
    DeleteObject(hbrush);
 
    graphics.ReleaseHdc(hdc);
}

Let’s wrap all the code above as extension methods to the Graphics class and use them in our owner drawn button control. A button control is one of the easiest owner drawn controls to create. Let’s keep it as simple as possible and only have 2 states for our button: pressed and not pressed.

class ThemedImageButton : Control
{
    bool pushed = false;
    private Bitmap image;
    private Bitmap offScreen;
 
    public Bitmap Image
    {
        get { return image; }
        set
        {
            image = value;
            Invalidate();
        }
    }
 
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
 
        if (offScreen != null)
        {
            offScreen.Dispose();
            offScreen = null;
        }
        offScreen = new Bitmap(ClientSize.Width, ClientSize.Height);
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        if (offScreen == null)
            offScreen = new Bitmap(ClientSize.Width, ClientSize.Height);
 
        using (var attributes = new ImageAttributes())
        using (var g = Graphics.FromImage(offScreen))
        {
            if (pushed)
            {
                using (var pen = new Pen(SystemColors.Highlight))
                    g.DrawThemedGradientRectangle(pen, ClientRectangle, new Size(4, 4));
            }
            else
                g.Clear(Parent.BackColor);
 
            var textSize = g.MeasureString(Text, Font);
            var textArea = new RectangleF(
                (ClientSize.Width - textSize.Width) / 2,
                (ClientSize.Height - textSize.Height),
                textSize.Width,
                textSize.Height);
 
            if (Image != null)
            {
                var imageArea = new Rectangle(
                    (ClientSize.Width - Image.Width) / 2,
                    (ClientSize.Height - Image.Height) / 2,
                    Image.Width,
                    Image.Height);
 
                var key = Image.GetPixel(0, 0);
                attributes.SetColorKey(key, key);
 
                g.DrawImage(
                    Image,
                    imageArea,
                    0, 0, Image.Width, Image.Height,
                    GraphicsUnit.Pixel,
                    attributes);
            }
 
            using (var brush = new SolidBrush(ForeColor))
                g.DrawString(Text, Font, brush, textArea);
 
            if (pushed)
            {
                var key = offScreen.GetPixel(0, 0);
                attributes.SetColorKey(key, key);
            }
            else
                attributes.ClearColorKey();
 
            e.Graphics.DrawImage(
                offScreen,
                ClientRectangle,
                0, 0, offScreen.Width, offScreen.Height,
                GraphicsUnit.Pixel,
                attributes);
        }
    }
 
    protected override void OnPaintBackground(PaintEventArgs e)
    {
    }
 
    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);
        pushed = true;
        Invalidate();
    }
 
    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);
        pushed = false;
        Invalidate();
    }
 
    protected override void OnParentChanged(EventArgs e)
    {
        base.OnParentChanged(e);
        Invalidate();
    }
 
    protected override void OnTextChanged(EventArgs e)
    {
        base.OnTextChanged(e);
        Invalidate();
    }
}

I hope you found this useful. If you’re interested in the full source code then you can grab it here

Semi-Transparent Controls in .NETCF

Some time ago, I wrote an article called Transparent Controls in .NETCF which describes how to accomplish transparency in the .NET Compact Framework. In this article I’d like to discuss how to control the opacity levels of transparent controls. This is made possible by alpha blending the parent controls background image with the child controls background color or image.

Here is an example of semi-transparent controls.

The screen shots above contain a semi-transparent PictureBox, Label, and Button controls. The button control is very Vista inspired, having 2 vertical fading gradient fills and then alpha blending it with the background image

Implementing Semi-Transparent Controls

The container or parent control will be the same code I used in my previous article.

Windows Mobile 5 and higher offers 2 ways of alpha blending, one through the function AlphaBlend (GDI) and through the Imaging API (COM). In this example let’s use the AlphaBlend function.

To begin with we need to define the BLENDFUNCTION structure that we’ll use as a parameter to AlphaBlend.

struct BLENDFUNCTION
{
    public byte BlendOp;
    public byte BlendFlags;
    public byte SourceConstantAlpha;
    public byte AlphaFormat;
}
 
enum BlendOperation : byte
{
    AC_SRC_OVER = 0x00
}
 
enum BlendFlags : byte
{
    Zero = 0x00
}
 
enum SourceConstantAlpha : byte
{
    Transparent = 0x00,
    Opaque = 0xFF
}
 
enum AlphaFormat : byte
{
    AC_SRC_ALPHA = 0x01
}

Next step is to define our P/Invoke declaration for AlphaBlend

[DllImport("coredll.dll")]
static extern bool AlphaBlend(
    IntPtr hdcDest,
    int xDest,
    int yDest,
    int cxDest,
    int cyDest,
    IntPtr hdcSrc,
    int xSrc,
    int ySrc,
    int cxSrc,
    int cySrc,
    BLENDFUNCTION blendfunction);

We’ll be using the GradientFill method as well in this example. For this we need to define 2 structures, TRIVERTEX and GRADIENT_RECT

struct TRIVERTEX
{
    private int x;
    private int y;
    private ushort Red;
    private ushort Green;
    private ushort Blue;
    private ushort Alpha;
 
    public TRIVERTEX(int x, int y, Color color)
        : this(x, y, color.R, color.G, color.B, color.A)
    {
    }
 
    public TRIVERTEX(
        int x, int y,
        ushort red, ushort green, ushort blue,
        ushort alpha)
    {
        this.x = x;
        this.y = y;
        Red = (ushort)(red << 8);
        Green = (ushort)(green << 8);
        Blue = (ushort)(blue << 8);
        Alpha = (ushort)(alpha << 8);
    }
}
 
struct GRADIENT_RECT
{
    private uint UpperLeft;
    private uint LowerRight;
 
    public GRADIENT_RECT(uint ul, uint lr)
    {
        UpperLeft = ul;
        LowerRight = lr;
    }
}

Using the 2 structures above we can now define our P/Invoke for GradientFill

[DllImport("coredll.dll")]
static extern bool GradientFill(
    IntPtr hdc,
    TRIVERTEX[] pVertex,
    uint dwNumVertex,
    GRADIENT_RECT[] pMesh,
    uint dwNumMesh,
    uint dwMode);

And then lets wrap those neatly in some extension methods to the Graphics class

public static class GraphicsExtension
{
    public static void AlphaBlend(this Graphics graphics, Image image, byte opacity)
    {
        AlphaBlend(graphics, image, opacity, Point.Empty);
    }
 
    public static void AlphaBlend(this Graphics graphics, Image image, byte opacity, Point location)
    {
        using (var imageSurface = Graphics.FromImage(image))
        {
            var hdcDst = graphics.GetHdc();
            var hdcSrc = imageSurface.GetHdc();
 
            try
            {
                var blendFunction = new BLENDFUNCTION
                {
                    BlendOp = ((byte)BlendOperation.AC_SRC_OVER),
                    BlendFlags = ((byte)BlendFlags.Zero),
                    SourceConstantAlpha = opacity,
                    AlphaFormat = 0
                };
                AlphaBlend(
                    hdcDst,
                    location.X == 0 ? 0 : -location.X,
                    location.Y == 0 ? 0 : -location.Y,
                    image.Width,
                    image.Height,
                    hdcSrc,
                    0,
                    0,
                    image.Width,
                    image.Height,
                    blendFunction);
            }
            finally
            {
                graphics.ReleaseHdc(hdcDst);
                imageSurface.ReleaseHdc(hdcSrc);
            }
        }
    }
 
    public static void GradientFill(
        this Graphics graphics,
        Rectangle rectangle,
        Color startColor,
        Color endColor,
        GradientFillDirection direction)
    {
        var tva = new TRIVERTEX[2];
        tva[0] = new TRIVERTEX(rectangle.Right, rectangle.Bottom, endColor);
        tva[1] = new TRIVERTEX(rectangle.X, rectangle.Y, startColor);
        var gra = new[] { new GRADIENT_RECT(0, 1) };
 
        var hdc = graphics.GetHdc();
        try
        {
            GradientFill(
                hdc,
                tva,
                (uint)tva.Length,
                gra,
                (uint)gra.Length,
                (uint)direction);
        }
        finally
        {
            graphics.ReleaseHdc(hdc);
        }
    }
 
    public enum GradientFillDirection
    {
        Horizontal = 0x00000000,
        Vertical = 0x00000001
    }
}

Now that we can call the AlphaBlend API we can start designing a simple UI. Let’s create a container to for the semi-transparent control that exposes its background image allowing the child control to alpha blend part of the background image. Before we create our alpha button control, let’s take a quick review on How to implement transparency in .NETCF.

In my old article I expose the background image through an interface that a container control must implement, I called it IControlBackground

interface IControlBackground
{
    Image BackgroundImage { get; }
}

Now that it’s possible to retrieve the parent controls background image through a child control we can create our alpha button control. This control will be the usual button control where we will use a boolean flag to store the pushed state of the control, this state is changed during mouse events. The control is painted using 2 gradient fills using shades of black and the text is drawn in the center. Since the entire control is painted in the OnPaint event I override OnPaintBackground with blank code.

class AlphaButton : Control
{
    private bool pushed;
 
    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);
        pushed = true;
        Invalidate();
    }
 
    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);
        pushed = false;
        Invalidate();
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        using (var backdrop = new Bitmap(Width, Height))
        {
            using (var gxOff = Graphics.FromImage(backdrop))
            {
                var topRect = new Rectangle(0, 0, ClientSize.Width, ClientSize.Height / 2);
                var bottomRect = new Rectangle(0, topRect.Height, ClientSize.Width, ClientSize.Height / 2);
 
                if (!pushed)
                {
                    gxOff.GradientFill(
                        bottomRect,
                        Color.FromArgb(0, 0, 11),
                        Color.FromArgb(32, 32, 32),
                        GraphicsExtension.GradientFillDirection.Vertical);
 
                    gxOff.GradientFill(
                        topRect,
                        Color.FromArgb(176, 176, 176),
                        Color.FromArgb(32, 32, 32),
                        GraphicsExtension.GradientFillDirection.Vertical);
                }
                else
                {
                    gxOff.GradientFill(
                        topRect,
                        Color.FromArgb(0, 0, 11),
                        Color.FromArgb(32, 32, 32),
                        GraphicsExtension.GradientFillDirection.Vertical);
 
                    gxOff.GradientFill(
                        bottomRect,
                        Color.FromArgb(176, 176, 176),
                        Color.FromArgb(32, 32, 32),
                        GraphicsExtension.GradientFillDirection.Vertical);
                }
 
                using (var border = new Pen(Color.White))
                    gxOff.DrawRectangle(border, 0, 0, ClientSize.Width - 1, ClientSize.Height - 1);
 
                if (!string.IsNullOrEmpty(Text))
                {
                    var size = gxOff.MeasureString(Text, Font);
                    using (var text = new SolidBrush(Color.White))
                        gxOff.DrawString(
                            Text,
                            Font,
                            text,
                            (ClientSize.Width - size.Width) / 2,
                            (ClientSize.Height - size.Height) / 2);
                }
 
                try
                {
                    var bgOwner = Parent as IControlBackground;
                    if (bgOwner != null && bgOwner.BackgroundImage != null)
                        gxOff.AlphaBlend(bgOwner.BackgroundImage, 70, Location);
                }
                catch (MissingMethodException ex)
                {
                    throw new PlatformNotSupportedException(
                        "AlphaBlend is not a supported GDI feature on this device", 
                        ex);
                }
            }
 
            e.Graphics.DrawImage(backdrop, 0, 0);
        }
    }
 
    protected override void OnPaintBackground(PaintEventArgs e)
    {
    }
}

I implement the IControlBackground interface in a simple Form class and draw my background image on the Form. I can use this directly as a container or create inherited classes and re-use the code in several occasions.

class FormBase : Form, IControlBackground
{
    Bitmap background;
 
    public FormBase()
    {
        background = new Bitmap(
            Assembly.GetExecutingAssembly().GetManifestResourceStream(
            "SemiTransparentSample.background.jpg"));
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.DrawImage(background, 0, 0);
    }
 
    public Image BackgroundImage
    {
        get { return background; }
    }
}

To finish it up let’s create a Form that will contain the alpha buttons.

class MainForm : FormBase
{
    public MainForm()
    {
        Controls.Add(new AlphaButton
        {
            Font = new Font("Arial", 16f, FontStyle.Bold),
            ForeColor = Color.White,
            Text = "Alpha Button 1",
            Bounds = new Rectangle(20, 20, 200, 50)
        });
        Controls.Add(new AlphaButton
        {
            Font = new Font("Arial", 16f, FontStyle.Bold),
            ForeColor = Color.White,
            Text = "Alpha Button 2",
            Bounds = new Rectangle(20, 90, 200, 50)
        });
        Controls.Add(new AlphaButton
        {
            Font = new Font("Arial", 16f, FontStyle.Bold),
            ForeColor = Color.White,
            Text = "Alpha Button 3",
            Bounds = new Rectangle(20, 160, 200, 50)
        });
        Controls.Add(new AlphaButton
        {
            Font = new Font("Arial", 16f, FontStyle.Bold),
            ForeColor = Color.White,
            Text = "Alpha Button 4",
            Bounds = new Rectangle(20, 230, 200, 50)
        });
    }
}

I hope you found this interesting and insightful. If you’re interested in the Visual Studio solution then you can download it here.

Transparent Controls in .NETCF

I work a lot with a graphic artist for developing solutions. The better the graphic artist you work with is, the harder it is to implement their designs to your application. One thing that my solutions have in common is that they all require transparent controls. My graphic artist loves having a image buttons on top of fancy background.

Here’s some screen shots of what I’ve made with my graphic artist:

In these screen shots I heavily use a transparent label control over a Form that has a background image. I normally set my controls to be designer visible so I can drag and drop while designing. Visual Studio 2005 and 2008 will automatically load all Custom Controls and UserControls

Implementing Transparent Controls

For creating transparent controls, I need the following:

1) IControlBackground interface – contains BackgroundImage { get; }
2) TransparentControlBase – draws the BackgroundImage of an IControlBackground form
3) Transparent Control – Inherits from TransparentControlBase
4) FormBase Form – implements IControlBackground and draws the background image to the form

Let’s start off with the IControlBackground interface. Like I mentioned above, it only contains a property called BackgroundImage.

public interface IControlBackground
{
  Image BackgroundImage { get; }
}

Next we will need to create the TransparentControlBase. Let’s create a class that inherits from Control. We then need to override the OnPaintBackground() event to draw the IControlBackground.BackgroundImage of the Parent control. To do this, we create an instance of IControlBackground from the Parent. Once we have the BackgroundImage, we draw part of the BackgroundImage where the transparent control is lying on.

We also override the OnTextChanged() and OnParentChanged() events to force a re-draw whenever the text or parent of the control is changed.

public class TransparentControlBase : Control
{
  protected bool HasBackground = false;

  protected override void OnPaintBackground(PaintEventArgs e)
  {
    IControlBackground form = Parent as IControlBackground;
    if (form == null) {
      base.OnPaintBackground(e);
      return;
    } else {
      HasBackground = true;
    }

    e.Graphics.DrawImage(
      form.BackgroundImage,
      0,
      0,
      Bounds,
      GraphicsUnit.Pixel);
  }

  protected override void OnTextChanged(EventArgs e)
  {
    base.OnTextChanged(e);
    Invalidate();
  }

  protected override void OnParentChanged(EventArgs e)
  {
    base.OnParentChanged(e);
    Invalidate();
  }
}

Now we need to create a control that inherits from TransparentControlBase. I’ll create a simple TransparentLabel control for this example. The control will have the same behavior as the standard Label control, except that it can be transparent when used over a form or control that implements IControlBackground.

public class TransparentLabel : TransparentControlBase
{
  ContentAlignment alignment = ContentAlignment.TopLeft;
  StringFormat format = null;
  Bitmap off_screen = null;

  public TransparentLabel()
  {
    format = new StringFormat();
  }

  public ContentAlignment TextAlign
  {
    get { return alignment; }
    set
    {
      alignment = value;
      switch (alignment) {
        case ContentAlignment.TopCenter:
          format.Alignment = StringAlignment.Center;
          format.LineAlignment = StringAlignment.Center;
          break;
        case ContentAlignment.TopLeft:
          format.Alignment = StringAlignment.Near;
          format.LineAlignment = StringAlignment.Near;
          break;
        case ContentAlignment.TopRight:
          format.Alignment = StringAlignment.Far;
          format.LineAlignment = StringAlignment.Far;
          break;
      }
    }
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    if (!base.HasBackground) {
      if (off_screen == null) {
        off_screen = new Bitmap(ClientSize.Width, ClientSize.Height);
      }
      using (Graphics g = Graphics.FromImage(off_screen)) {
        using (SolidBrush brush = new SolidBrush(Parent.BackColor)) {
          g.Clear(BackColor);
          g.FillRectangle(brush, ClientRectangle);
        }
      }
    } else {
      using (SolidBrush brush = new SolidBrush(ForeColor)) {
        e.Graphics.DrawString(
          Text,
          Font,
          brush,
          new Rectangle(0, 0, Width, Height),
          format);
      }
    }
  }
}

Now that we have our transparent controls, we need to create a Form that will contain these controls. First we need to create a base class that will implement IControlBackground and inherit from Form.

In this example, I added a background image to the solution and as an embedded resource. My default namespace is called TransparentSample and my background image is located at the root folder with the filename background.jpg

public class FormBase : Form, IControlBackground
{
  Bitmap background;

  public FormBase()
  {
    background = new Bitmap(
      Assembly.GetExecutingAssembly().GetManifestResourceStream(
      “TransparentSample.background.jpg”));
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    e.Graphics.DrawImage(background, 0, 0);
  }

  public Image BackgroundImage
  {
    get { return background; }
  }
}

For the last step, we need to create a Form that will contain these transparent controls. To start, let’s add a new Form to our project and let it inherit from FormBase instead of Form.

Now we can add our transparent controls to the main form.

public class MainForm : FormBase
{
  TransparentLabel label;

  public MainForm()
  {
    label = new TransparentLabel();
    label.Font = new Font(“Arial”, 16f, FontStyle.Bold);
    label.ForeColor = Color.White;
    label.Text = “Transparent Label”;
    label.Bounds = new Rectangle(20, 60, 200, 50);
    Controls.Add(label);
  }
}

That wasn’t very complicated, was it? Having a nice and intuitive UI offers a very good user experience. Being creative, imaginative, and learning to work with a graphic artist can really pay off.

Scroll to top