BUILDING CUSTOM CONTROLS IN C# – PART 2

 

clip_image002

ABSTRACT

This article will enable you to understand parallel axis graphs and how you might want to implement a fully functional albeit basic yet effective custom control in C#. Graphing and data visualization are hot topics in security and parallel axis graphs are very useful in representing multiple data types or multiple parameters of a system and help patterns emerge from the resulting graph line connections. Such patterns can be very helpful in detecting similarities or anomalies in newer data sets. We will make out own text based data format, our visual control from the graph, a data display using a factory control and interactivity with the data set using opacity as a main tool for demarcating particular line segments adjacent to the data set rows. Further functionality can be built once you get the idea.

THE PREMISE

clip_image004 clip_image006

[*]Both the snapshots above show the parallel axis graph in display mode and brushing mode.

[*]The left side data display box corroborates with the parallel axis display and interacts with it.

[*]The data format is text editable.

[*]The number of columns or Lines in the graph is dynamically set (no fixed size limit, but 5-6 is good enough for default size) and can position them uniformly over the entire display.

[*]The column headers are displayed on the top. Values run from the top to bottom. Each range is calibrated to fit in the line length.

[*]Brushing or data interactivity with the display involves reducing the opacity of non selected lines and coloring the selected line to teal for a better visual effect.

[*]The interface is simple with only needing the user to load a data file for display and interaction. Further features can be embellished as and when needed. The modular design of the control and the classes enable just that.

The dataset format is very simple :

clip_image008

Every line is a singular entity.

The first line always contains the number of lines/columns/fields/parameters required.

The second line contains the names of the fields separated by a space.

Thereafter, every other line till the end of the file is the dataset in the format specified above.

The config file is kept in another file in the same directory

clip_image010

The config file relates to the data types for each of the 5 fields in the preceding data set.

Each line relates to one field.

The first value is the serial number of the type/field.

The second value is the total expected subdivision ranges to be effected by the parsing logic.

The third and the 4th columns are metadata placeholders for further information if required.

1st file name is dataset2.txt and the config file is named dataset2Config.

Both files have to have the same name, with the config file having Config appended in camel case sans extension. You may change the requirements as per your needs.

Now that we have done with our data structure design, let us proceed to the control design.

We need a data parser in the back end and a graphic logic component. Finally we need a way to interact with data in both text and the corresponding graph. So that’s 3 components in all.

clip_image012

We need to use the System.Drawing/System.Drawing.Drawing2D namespaces. Let’s start with the data parser code.

DATA PARSER

[*] Initializing plot variables

string [] datasetMem;

int numberOfPlotLines = 0;

int numberOfColumns = 0;

string[] columnsHolder;

List<string> datasetHolder = new List<string>();

bool readyForDisplay = false;

string OpnedPath = String.Empty;

[*] The open file dialog code for data file input

private void openDatasetButton_Click(object sender, EventArgs e)

{

OpenFileDialog o = new OpenFileDialog();

if (o.ShowDialog() == DialogResult.OK)

{

readyForDisplay = false;

listView1.Clear();

fileReaderBackgroundWorker.RunWorkerAsync(o.FileName);

}

}

[*] Excerpt of the actual file reading code. All lines are saved in a string array

private void dataSetFileParse(string filePath) {

OpnedPath = filePath;

datasetMem = File.ReadAllLines(filePath);

}

[*] Splitting of per line as per the format. Split() function is used to extract particular fields from a complete line. In memory storing of the dataset parameters.

private void initGlobalVarFromDataset() {

//the first line contains the number of columns

numberOfPlotLines = Int32.Parse(datasetMem[0]);

//the second line contains the columns name strings

columnsHolder=datasetMem[1].Split(‘,’);

numberOfColumns = columnsHolder.Length;

//Initializing the loop variable to start from the 3rd line onwards

//read each line and save them in a string list for later data processing.

int i=2;

for (; i < datasetMem.Length ; i++) {

if (datasetMem[i]!=””){

datasetHolder.Add(datasetMem[i]); //holds the core data records

}

}

MaximumSizeDataInit(OpnedPath); //init

ListViewInit(datasetHolder,numberOfColumns,columnsHolder); //init

}

//This initializes the listView control and fills it with the data items from the data file.

public void ListViewInit(List<string> data, int nOfCol,string [] columns) {

int i=0;

for (;i<columnsHolder.Length;i++){

listView1.Columns.Add(columnsHolder[i]);

}

int j=0;int index=0;

for (; j < data.Count;j++ ) {

string[] field = data[j].Split(‘,’);

int u = 0; string [] subitems=new string[field.Length-1];

for (; u < subitems.Length; u++) {

subitems[u]=field[u+1];

}

listView1.Items.Add(data[j].Split(‘,’)[0]);

listView1.Items[index].SubItems.AddRange(subitems);

index++;

}

plotLineGenerator(); //calls the plot graph gdi code.

}

We need to assign a data structure to store the plot lines parameters and associated data. We initialize a list of type Lines, which is a struct to hold all line related visual data.

The Lines struct represents the data required for each line. To construct a vertical line you would need a top point and a bottom point. Ticks are very useful for grading the data range on the line. Further a set of properties are initialized withing the struct to retrieve the data.

We also initialize a graphLinePoints data structure as a list of the same custom struct type which contains an array of the points contained in each line after the plotting is done. This is to keep a record of the number of points generated and use that to check for use interactivity and data retireval from the visual area. This done separately for better code maintenance and not included in the previous struct that keeps the entire plot related data. Thus in terms of data processing the second list of points is actually very crucial once the graph construction is done.

List<Lines> plotLinesData = new List<Lines>();

struct Lines

{

public Point TopPoint;

public Point BottomPoint;

private string Name;

//private int Ticks;

public Lines(Point a, Point b, string c)

{

TopPoint = a; BottomPoint = b; Name = c; //Ticks = d;

}

public Point GetTopPoint() { return TopPoint; }

public Point GetBottomPoint() { return BottomPoint; }

public String GetName() { return Name; }

//public int GetLineValueRange() {return Ticks;}

}

List<graphLinePoints> glp = new List<graphLinePoints>(); //###################//

struct graphLinePoints

{

PointF[] points;

public graphLinePoints(PointF [] a)

{

points = a;

}

public PointF[] GetPointArrayFromField()

{

return points;

}

}

int [] MaximumSizeForEachField;

int[] MinimumSizeForEachField;

private void MaximumSizeDataInit(string filePath) {

if (File.Exists(Path.GetDirectoryName(filePath)+”\\”+Path.GetFileNameWithoutExtension(filePath)+”Config”))

{

string[] pars = File.ReadAllLines(Path.GetDirectoryName(filePath)+”\\”+Path.GetFileNameWithoutExtension(filePath)+”Config”);

int r = 0; int p = 0; MaximumSizeForEachField = new int[numberOfColumns];

MinimumSizeForEachField=new int [numberOfColumns];

foreach (string f in pars) {

if (f.Split(‘,’)[1] != “”)

{

try

{

MaximumSizeForEachField[r] = Int32.Parse(f.Split(‘,’)[1]);

r++;

}

catch (Exception n) { }

}

else { MaximumSizeForEachField[r] = datasetHolder.Count; r++; }

if (f.Split(‘,’)[2] != “”)

{

try

{

MinimumSizeForEachField[p] = Int32.Parse(f.Split(‘,’)[2]);

p++;

}

catch (Exception m) { }

}

else {

MinimumSizeForEachField[p] = 0; p++;}

}

}

}

The code to generate the data for the lines and their ticks in the first pass is quite simple really. You iterate for the number of columns required and set the top and bottom points as per the area and aesthetics. I set the top point to always start height of 20 pixels from the graph area, this would be the Y coordinate for every such point (x,y). The x axis related starting offset is calculated from the (total width of the graph area/the number of plot lines required). This would give the division distance in pixels for every next line to start from. Thus we just add the offset to every next point generated to balance out the total graph area into equidistant lines. The same is done for the bottom lines. Next we process each line in the extracted data set list and generate the points list for per line.

private void plotLineGenerator() {

int t = this.panelPCG.Height – 40 – 20; ; // Y range holder

List<Point> fieldXPoint = new List<Point>();

List<Point[]> lineRangeHolder = new List<Point[]>();

int i = 0; int divisions = this.panelPCG.Width / numberOfPlotLines; int starter = 0;

for (; i < numberOfPlotLines; i++)

{

plotLinesData.Add(new Lines(new Point(divisions / 2 + starter, 20),

new Point(divisions / 2 + starter, this.panelPCG.Height – 40), columnsHolder[i]));

fieldXPoint.Add(new Point(divisions / 2 + starter, t));

//the Y co-ordinate in ‘fieldXPoint’ stores the distance between the top and bottom line points

starter += divisions;

}

foreach (string d in datasetHolder) { //point generator

string[] record = d.Split(‘,’);

int k=0;

PointF [] pointer=new PointF[record.Length]; //temp holder

for (;k<record.Length;k++){

if (record[k].Contains(“=”)) {

string numeric=record[k].Substring((record[k].IndexOf(“=”)+1),1);

pointer[k] = new PointF((float)fieldXPoint[k].X, (((float.Parse(numeric)-MinimumSizeForEachField[k]) / (float)MaximumSizeForEachField[k])) * (t + 20));

}

else if (k==1) {

//string hexor = record[k].Substring((record[k].IndexOf(“x”) + 1), record[k].Length);

pointer[k] = new PointF((float)fieldXPoint[k].X, ((Norm2GB(record[k]))) * (t + 20));

}

else

{

pointer[k] = new PointF((float)fieldXPoint[k].X, (((float.Parse(record[k]) – MinimumSizeForEachField[k]) / (float)MaximumSizeForEachField[k])) * (t + 20));

}

}

glp.Add(new graphLinePoints(pointer));

}

readyForDisplay = true; panelPCG.Invalidate();

}

private float Norm2GB(string EP)

{

int y = Convert.ToInt32(EP, 16);

float rangeMaxDefPageStartAddr = 209409; //in divisions of 10 pages

float rank;

if (y <= 209409)

{

rank = (float)(y – 4096) / rangeMaxDefPageStartAddr;

}

else { rank = (float)(y – 4096) / (rangeMaxDefPageStartAddr + (y – rangeMaxDefPageStartAddr)); }

return rank;

}

Finally, the actual drawing code is done in the paint loop. Here a flag ‘readyForDisplay’ is set so that the graph would not draw itself before the datasets are ready and the lines/points are calculated.

We set the smoothing mode to Antialias as the lines have to crisp. This feature comes from the System.Drawing.Drawing2D class. Thereafter we iterate each data structure for both the plot lines ‘Lines’ and the points container ‘graphLinePoints’.

The syntax for DrawLine() method is simple as well.

It takes a pen instance, start point and end point as arguments. The pen class abstracts a virtual pen and thus lines are best drawn with a thin nib.

DrawString() is used to name the header of each line. It takes the input string, font, color and the position.

To draw the ticks for each of the data in each line another function called DrawLines() is used. This takes a pen instance and a points array. This would draw lines connecting each point from every other line for a particular dataset row. Like a join the dots kind of thing. This simplifies setting and working on extensive number of points. Hence you can now see the additional benefit of separating the graph code and the points data structure. Also debugging would be easy.

private void plotPointsGenerator() {

}

private bool fieldClicked=false;

private void panelPCG_Paint(object sender, PaintEventArgs e)

{

if (readyForDisplay)

{

e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

foreach (Lines l in plotLinesData)

{

e.Graphics.DrawLine(new Pen(new SolidBrush(Color.White), 2), l.GetTopPoint(), l.GetBottomPoint());

SizeF s = e.Graphics.MeasureString(l.GetName(), new Font(“Arial”, 8));

e.Graphics.DrawString(l.GetName(), new Font(“Arial”, 8), new SolidBrush(SystemColors.Control), new PointF(((float)l.GetTopPoint().X) – s.Width / 2, 3));

}

//foreach (graphLinePoints j in glp){

if (fieldClicked)

{

for (int k = 0; k < glp.Count; k++)

{

if (k != listView1.SelectedIndices[0])

{

e.Graphics.DrawLines(new Pen(new SolidBrush(Color.FromArgb(50, 255, 255, 255)), 0.5f), glp[k].GetPointArrayFromField());

}

else

{

e.Graphics.DrawLines(new Pen(new SolidBrush(Color.Teal), 1), glp[k].GetPointArrayFromField());

}

}

}

else

{

foreach (graphLinePoints g in glp)

{

e.Graphics.DrawLines(new Pen(new SolidBrush(Color.White), 1), g.GetPointArrayFromField());

}

}

//} //end of foreach

}

}

At this point the listView that displays the textual data in a table is set to refresh the graph with a fieldClicked boolean parameter and the invalidate() method of the graph area is invoked.

private void listView1_Click(object sender, EventArgs e)

{

if (listView1.SelectedItems.Count > 0)

{

fieldClicked = true;

panelPCG.Invalidate();

}

}

The logic for changing the graph lines color and opacity is very simple as well. This is the same excerpt of the code above – here we check for if fieldClicked = true. If true then we run the segment below first else the ‘default’ case. The listView itself keeps a record of the selected index. I disable multi selections for this example. Thus we simply check for the selected index in the listView and accrodingly set the opacity and the color of that field connecting lines. The SolidBrush class takes the Color value. The color value is set to Color.FromArgb(50,255,255,255) – meaning opacity of 50 (runs from 1 to 255) and a color of white (255,255,255). The selected line is set to full opacity and a Color.Teal shade. This makes it stick out from the rest.

for (int k = 0; k < glp.Count; k++)

{

if (k != listView1.SelectedIndices[0])

{

e.Graphics.DrawLines(new Pen(new SolidBrush(Color.FromArgb(50, 255, 255, 255)), 0.5f), glp[k].GetPointArrayFromField());

}

else

{

e.Graphics.DrawLines(new Pen(new SolidBrush(Color.Teal), 1), glp[k].GetPointArrayFromField());

}

The same feature can be enabled in the graph area as well, wherein if the user hovers around the line at the particular data set regions the value would pop up in the location making it more interactive. The use could also move the lines around and accordingly the graph should update itself. Large data ranges have to be accommodated. And finally support for graph zoom. Can you think of other interesting features you can code for your custom graph control that processes and displays your own data set? In terms of data sets – you could code for standard issue XML or SQL queries as well. Customise as you want it.

CONCLUSION

You have taken a good look at the approach and the process from start to finish of how to make your own custom controls and the kind of design and coding effort that goes into it. Simplicity is the key word in C#. The GDI+ library in .NET has made graphics coding very intuitive and efficient for data processing and visualization needs.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s