Introduction
In most of the projects related to developing Windows Mobile applications, 99% there will be the need for developing at least one custom control to provide a really good user experience and for presenting results in such a way that the standard controls are never enough for fulfilling it.
During a Windows Mobile project that I was involved in, I was faced with the need to present to the user 2D Curves detailing some statistics that the application is supposed to collect. Hence, the emergence of the need to create a customizable Curve
control. I supposed that for sure other developers will face such a problem/need. So, I decided to present through this article the concept of custom controls and the Curve
custom control that I'm providing.
Background
For those of you who are new to controls development in .NET, there used to be a big confusion between what a User Control is and what a Custom Control is.
A User Control is basically a UI component, which is made of other controls which can also be User Controls, and that has a design-time experience very similar to the Form’s one. A User Control is built based on composition. Its goal is helping you to reuse a set of UI elements on more than one Form providing a consistent look & feel and usability from both the development and user experience point of view.
In contrast, a Custom Control is a single control. A .NET control which is developed from scratch. It’s basically a class that inherits from System.Windows.Forms.Control
and overrides some of the Control
’s methods, like OnPaint
, to provide the expected user experience.
While a User Control is easier to develop and support, a Custom Control is lighter and faster at runtime, and it gives you the highest flexibility you can get in a .NET control because you are free to draw whatever you want, even animations, and to handle input messages like keyboard and touch events.
Using the Code
As attachments, I provide both the Curve
custom control as well as the demo Visual Studio 2008 projects. You will be able to: either use directly the Curve
control as I'm providing it or you can even customize it, re-compile it and use yours.
So, in the following paragraphs, I'll detail both the steps to follow in order to directly plug and use the Curve
control within your project and the steps to follow in order to customize the Curve
control and use yours.
Using the Curve Custom Control
First of all, you have to download the CurveControl
project attached to this article. Once done, you have to add the Curve
control to your project. You can do that by right clicking on your project toolbox and clicking on the "Choose Items ..." (see picture below). Then, you browse and add the "CurveControl.dll" file available within the release directory of "CurveControl
" Visual Studio 2008 attached to this article.
Then, you will be able to drop the Curve
control from the toolbox to your project Form
.
Once done, you will need to edit the Curve
Paint
event in order to customize it and to supply it with the list of Point
objects that you want to plot. The following code snippet describes an example where I start by creating the list of point(s) to plot on the 2D Curve
, then I setup the titles of the X and Y axis(s) as well as the colors and font(s) associated to these titles.
Note that through this Curve
control, it is also possible to configure it in such a way that it plots the 2D Curve
as well as the coordinates' values of all the points. You just need to make a call to the ShowPointCoordinates(true)
method. It is also possible to easily customize the font and color to be used for plotting the coordinates' values.
private void curve1_Paint(object sender, PaintEventArgs e)
{
List<Point> values = new List<Point>();
values.Add(new Point(20, 30));
values.Add(new Point(100, 70));
values.Add(new Point(40, 50));
values.Add(new Point(150, 40));
values.Add(new Point(70, 20));
curve1.SetValuesList(values);
curve1.SetXAxisTitle("Time(s)");
curve1.SetYAxisTitle("Avg Rate");
curve1.ShowPointCoordinates(true);
curve1.SetSorting(true);
curve1.SetBackgroundColor(Color.White);
curve1.SetXYAxisColor(Color.Black);
curve1.SetTitlesColor(Color.Red);
curve1.SetXYTitlesFont(new Font("Calibri", 8, FontStyle.Bold));
curve1.SetCoordinatesColor(Color.Blue);
curve1.SetCoordinatesFont(new Font("Calibri", 8, FontStyle.Bold));
}
Customizing the Curve Control
Below, I've tried to comment as much as needed the source code of the Curve
control. So, you will be able to easily customize it. As I've explained later, any control that you would start from scratch should inherit from the Control
class and override the needed methods. In the case of the Curve
control, I've overridden the OnPaint
method within which I make a call to the PlotCurve
method. As described in the following code snippet, the later method takes in charge resizing then plotting the different components of the Curve
. Namely, plotting the axis(s) and their titles by calling the PlotXYAxis
method. Then, the PlotCurve
method makes a call to the AdaptValuesToXYAxisLenght
method in order to adjust the coordinates of the Point(s) to plot according to the current Curve
control size. Once done, the PlotCurve
method proceeds to plotting the different Point(s) as well as their coordinates if asked. It is also important to not forget to implement the method void Curve_Resize(object sender, EventArgs e)
in order to catch the Resize
event. Indeed, Once the Resize
event is fired, either on runtime or during the design time, we need to call back the PlotCurve
method which will take into consideration the new Curve
control size and re-size its components.
Note also that the following code snippet provides all the comments and details related to the methods that you can use to customize the final appearance of the Curve
. Namely, the components’ fonts and colors. For example, I provided the methods SetXAxisTitle
, SetYAxisTitle
, SetXYAxisColor
, SetTitlesColor
and SetXYTitlesFont
for editing the axis(s) titles, their font and colors. However, the positioning of the titles is done implicitly according to the curve width and height. It is also possible to change the background color of the Curve
using the method SetBackgroundColor
.
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace CurveControl
{
public partial class Curve : Control
{
private List<Point> valuesList;
private List<Point> convertedList;
private string xAxisTitle = "X Title";
private string yAxisTitle = "Y Title";
private int curveXPosition = 0;
private int curveYPosition = 0;
private int curveWidth = 0;
private int curveHeight = 0;
private int xOrigin = 0;
private int yOrigin = 0;
private int xAxisTitleHeight = 0;
private int yAxisTitleWidth = 0;
private int yAxisTitleHeight = 0;
private bool showCoordinates;
private Graphics g = null;
private bool sorting;
Font xyTitlesFont = null;
Font coordinatesFont = null;
Color xyTitlesColor = Color.Red;
Color coordinatesColor = Color.Blue;
Color xyAxisColor = Color.Black;
public Curve()
{
valuesList = new List<Point>();
convertedList = new List<Point>();
showCoordinates = false;
sorting = true;
xyTitlesFont = new Font("Calibri", 8, FontStyle.Bold);
coordinatesFont = new Font("Calibri", 8, FontStyle.Bold);
InitializeComponent();
}
private Graphics GetGraphics
{
get
{
if (g == null)
g = CreateGraphics();
return g;
}
}
public void SetXAxisTitle(string title)
{
xAxisTitle = title;
}
public void ShowPointCoordinates(bool show)
{
showCoordinates = show;
}
public void SetSorting(bool value)
{
sorting = value;
}
public void SetYAxisTitle(string title)
{
yAxisTitle = title;
}
public void SetXYTitlesFont(Font f)
{
xyTitlesFont = f;
}
public void SetXYAxisColor(Color c)
{
xyAxisColor = c;
}
public void SetTitlesColor(Color c)
{
xyTitlesColor = c;
}
public void SetCoordinatesColor(Color c)
{
coordinatesColor = c;
}
public void SetCoordinatesFont(Font f)
{
coordinatesFont = f;
}
public void SetValuesList(List<Point> values)
{
valuesList = values;
}
private int GetMaxX(ref List<Point> list)
{
int max = 0;
foreach (Point p in list)
{
if (p.X > max)
max = p.X;
}
return max;
}
int GetMaxY(ref List<Point> list)
{
int max = 0;
foreach (Point p in list)
{
if (p.Y > max)
max = p.Y;
}
return max;
}
private void AdaptValuesToXYAxisLenght(int xAxisLength, int yAxisLength)
{
int maxX = GetMaxX(ref valuesList);
int maxY = GetMaxY(ref valuesList);
foreach (Point p in valuesList)
{
convertedList.Add(new Point((p.X * xAxisLength) /
maxX, (p.Y * yAxisLength) / maxY));
}
}
private int ComparePoints(Point a, Point b)
{
if (a.X > b.X)
return -1;
else if (a.X < a.Y) return 1;
else return 0;
}
public void SetBackgroundColor(Color color)
{
Rectangle rect = new Rectangle(0, 0,
curveWidth - curveXPosition, curveHeight - curveYPosition);
GetGraphics.FillRectangle(new SolidBrush(color), rect);
}
private void PlotXYAxis(ref int xAxisLenght, ref int yAxisLenght)
{
GetGraphics.DrawLine(new Pen(xyAxisColor, 4),
xOrigin, yOrigin, xOrigin+xAxisLenght, yOrigin);
GetGraphics.DrawLine(new Pen(xyAxisColor, 2),
xOrigin + xAxisLenght, yOrigin, xOrigin + xAxisLenght - 6, yOrigin
+ 6 + 2);
GetGraphics.DrawLine(new Pen(xyAxisColor, 2),
xOrigin + xAxisLenght, yOrigin, xOrigin + xAxisLenght - 6, yOrigin
- 6 - 2);
GetGraphics.DrawLine(new Pen(xyAxisColor, 4),
xOrigin, yOrigin, xOrigin, yOrigin - yAxisLenght);
GetGraphics.DrawLine(new Pen(xyAxisColor, 2),
xOrigin, yOrigin - yAxisLenght - 2, xOrigin + 6, yOrigin -
yAxisLenght + 6 - 2);
GetGraphics.DrawLine(new Pen(xyAxisColor, 2),
xOrigin, yOrigin - yAxisLenght - 2, xOrigin - 6, yOrigin -
yAxisLenght + 6 - 2);
GetGraphics.DrawString(xAxisTitle,
xyTitlesFont, new SolidBrush(xyTitlesColor), (curveWidth -
xAxisTitle.Length)/ 2, yOrigin);
GetGraphics.DrawString(yAxisTitle, xyTitlesFont,
new SolidBrush(xyTitlesColor), curveXPosition, curveYPosition -
yAxisTitleHeight - yAxisTitleHeight/2);
}
public void PlotCurve()
{
this.curveXPosition = this.Location.X;
this.curveYPosition = this.Location.Y;
this.curveWidth = this.Size.Width;
this.curveHeight = this.Size.Height;
xOrigin = curveXPosition + yAxisTitleWidth / 2;
yOrigin = curveHeight - xAxisTitleHeight;
yAxisTitleWidth = (int)Math.Floor((double)
(GetGraphics.MeasureString(xAxisTitle, xyTitlesFont).Width));
xAxisTitleHeight = (int)Math.Floor((double)
(GetGraphics.MeasureString(xAxisTitle, xyTitlesFont).Height));
yAxisTitleHeight = (int)Math.Floor((double)
(GetGraphics.MeasureString(yAxisTitle, xyTitlesFont).Height));
int xAxisLength = curveWidth - xOrigin;
int yAxisLength = yOrigin - curveYPosition;
PlotXYAxis(ref xAxisLength, ref yAxisLength);
this.AdaptValuesToXYAxisLenght(xAxisLength, yAxisLength);
int maxX = GetMaxX(ref convertedList);
int maxY = GetMaxY(ref convertedList);
if (convertedList.Count > 0)
{
if (sorting)
{
convertedList.Sort(new Comparison<Point>(ComparePoints));
}
Point last = convertedList[0];
bool direction = false;
int xHeight = (int)Math.Floor((double)
(GetGraphics.MeasureString("X",coordinatesFont).Height));
int yWidth = (int)Math.Floor((double)
(GetGraphics.MeasureString("X", coordinatesFont).Width));
foreach (Point p in convertedList)
{
if (last != p)
{
GetGraphics.DrawLine(new Pen(Color.Black, 2),
xOrigin + last.X, yOrigin - last.Y, xOrigin + p.X,
yOrigin - p.Y);
if (p.Y > last.Y)
direction = true;
else direction = false;
}
GetGraphics.DrawString("X", coordinatesFont,
new SolidBrush(coordinatesColor), xOrigin + p.X - yWidth /
2, yOrigin - p.Y - xHeight/2);
if (showCoordinates)
{
string coordinates = "x: " + p.X.ToString() +
"\n" + "y: " + p.Y.ToString();
int coordinatesHeight = (int)Math.Floor((double)
(GetGraphics.MeasureString(coordinates,
coordinatesFont).Height));
int coordinatesWidth = (int)Math.Floor((double)
(GetGraphics.MeasureString(coordinates,
coordinatesFont).Width));
if (direction)
{
if (p.X == maxX )
{
GetGraphics.DrawString(coordinates,
coordinatesFont, new SolidBrush(coordinatesColor),
xOrigin + p.X - coordinatesWidth,
yOrigin - p.Y - coordinatesHeight / 2 - xHeight / 2);
}
else if (p.Y == maxY)
{
GetGraphics.DrawString(coordinates,
coordinatesFont, new SolidBrush(coordinatesColor),
xOrigin + p.X - coordinatesWidth -
coordinatesWidth/2, yOrigin - p.Y -
coordinatesHeight / 2);
}
else
{
GetGraphics.DrawString(coordinates,
coordinatesFont, new SolidBrush(coordinatesColor),
xOrigin + p.X - coordinatesWidth / 2,
yOrigin - p.Y - coordinatesHeight - xHeight / 2);
}
}
else
{
if (p.X == maxX )
{
GetGraphics.DrawString(coordinates,
coordinatesFont, new SolidBrush(coordinatesColor),
xOrigin + p.X - coordinatesWidth ,
yOrigin - p.Y + xHeight / 2);
}
else
{
GetGraphics.DrawString(coordinates,
coordinatesFont, new SolidBrush(coordinatesColor),
xOrigin + p.X - coordinatesWidth / 2,
yOrigin - p.Y + xHeight / 2);
}
}
}
last = p;
}
}
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
PlotCurve();
}
private void Curve_Resize(object sender, EventArgs e)
{
PlotCurve();
}
}
}
Depending on your needs and the semantic of the curve you want to plot, you can choose either to enable Curve
control to implicitly sort the list of Point
(s) according to the X axis or not. You can do that using the SetSorting
method. And finally, I've introduced some controls within the PlotCurve
method in order to take into account the direction of the curve when plotting the Point
(s)' coordinates. Indeed, the Curve
control changes the position of the coordinates to plot depending on the future curve direction in order to avoid that curve hides the coordinates.
History
- First version: March 2010