Nateo Tutorial: Custom Object Drawing


 

In this tutorial we will implement the drawing of certain objects by ourselves instead of letting the NtTimeChartControl do the drawing. In order to keep things simple, we will set up a very tiny application that has only one row in its Time Chart and we will add data of only one type. For this type of data we will implement the drawing.
 
The idea is that our data may consist of floating point values ranging from 0.0 to 1.0. Each data object shall be displayed by a filled circle and the radius of the circle indicates the value of the item: The radius shall range from 0 (for value 0.0) to maxRadius (for value 1.0). Here we want to go the more professional way: Instead of simply hardcode maxRadius we want to determine it in the same way as other properties of the display like fonts and colors. For this purpose we will extend the NtDisplayDef data type in a subclass which is able to store the maxRadius.


1. Preparing the application
 
Starting with an empty application as outlined in the first tutorial Empty Time Chart, the code for the Form1_Load() member method will by now look quite common to you:
 

ntTimeChartControl.Init(10 * 24, 90, TimeDisplayMode.hours);
ntTimeChartControl.SetScrollBars(ntTimeChartControlHScrollBar, ntTimeChartControlVScrollBar);

ntHeaderControl.Init(ntTimeChartControl, Font, Font, false);
ntLeftFlankControl.Init(ntTimeChartControl, null, ntLeftFlankControl.ClientSize.Width);

ntBackPanel.SetPadding(2, 2, 2, 2);
ntBackPanel.Repos(ntLeftFlankControl, ntTimeChartControl, ntTimeChartControlHScrollBar, ntTimeChartControlVScrollBar);

ntTimeChartControl.ScrollToDt(DateTime.Now, ntTimeChartControl.ClientSize.Width / 2, 0);
ntTimeChartControl.Focus();


2. Define our own Data Type
 
To make the tutorial look more professional we choose the way of defining our own data type to store the floating point value:
 

public class MyDataObject : NtDataObject
{
public double d;
public MyDataObject(double _d, ulong _objId, DateTime _dt) { d = _d; dataItem = _d; objId = _objId; dt = dtEnd = _dt; dataTypeId = 100; }
}


3. Define our own subclass of NtDisplayDef
 
Instead of simply hard coding the maxRadius we want to take it from our configured NtDisplayDef. Therefore we create our own subclass:
 

public class MyDisplayDef : NtDisplayDef
{
public int maxRadius;
public MyDisplayDef() { dataTypeId = 100; displayMethod = 300;}
}

 
Please note two things here: First, in its constructor MyDisplayDef defines the same dataTypeId which is defined in the constructor of MyDataObject: 100. This links both of them together, because whenever the NtTimeChartControl finds a data object with this data type it knows which display definition is the right one. Second, the displayMethod is set to 300, which is greater than 100 which means 'Custom Drawing'. So when finding a data object with dataTypeId 100, the Time Chart will look for an instance of NtDisplayDef with 100. Finding one, it will read displayMethod 300 from it and then know that for this data object it has to call the interface methods of custom drawing.


4. Implement the interface NtCustomObjectPaint
 
The most important part is to implement the interface NtCustomObjectPaint. By this interface the NtTimeChartControl calls our code to perform the following tasks:

For each of these tasks there is a member function on its own defined by the interface. In our example, the data object will contain the value as a double number. The display object meanwhile will be the circle with the radius determined by the value. The NtDisplayObject always represents the bounding rectangle for the visual representation, so in our case it will be a square with the edges two times the radius of our circle. As we want the centers of all circles to be aligned on one horizontal line, we will calculate the top and bottom accordingly.
 
We will let our Form implement this interface, so we add the following member functions to our Form1 class:
 

//Custom Object Paint Interface Implementation
 
public NtDisplayObject NtCreateDisplayObject(NtTimeChartControl ntTimeChartControl, NtDataObject dataObject, NtDisplayDef displayDef, Graphics g)
{
return new NtDisplayObject();
}
 
public void NtPaintCustomObject(NtTimeChartControl ntTimeChartControl, NtDisplayObject displayObject, Rectangle objectRect, Rectangle paintRect, Graphics g)
{
g.FillEllipse(displayObject.Selected?Brushes.LightBlue:displayObject.DisplayDef.foregroundBrush, objectRect);
}
 
public void NtCalculateLeftAndRightEdgesTicks(NtTimeChartControl ntTimeChartControl, NtDataObject dataObject, NtDisplayDef displayDef, double ticksPerPixel, out long leftTicks, out long rightTicks)
{
int maxRadius = ((MyDisplayDef)displayDef).maxRadius;
double d = ((MyDataObject)dataObject).d; ;
int radius = (int)(d * maxRadius + 1);
long radiusInTicks = (long)(ticksPerPixel * radius);
leftTicks = dataObject.dt.Ticks - radiusInTicks;
rightTicks = dataObject.dt.Ticks + radiusInTicks;
}
 
public void NtCalculateLeftAndRightEdgesInSheet(NtTimeChartControl ntTimeChartControl, NtDisplayObject displayObject, long sheetLeftTicks, double pixelPerTick, out int left, out int right)
{
int maxRadius = ((MyDisplayDef)displayObject.DisplayDef).maxRadius;
double d = ((MyDataObject)displayObject.DataObject).d;
int radius = (int)(d * maxRadius + 1);
left = (int)((displayObject.DataObject.dt.Ticks - sheetLeftTicks) * pixelPerTick) - radius;
right = left + 2 * radius;
}
 
public void NtCalculateTopAndBottomInRow(NtTimeChartControl ntTimeChartControl, NtDisplayObject displayObject, out int top, out int bottom)
{
int maxRadius = ((MyDisplayDef)displayObject.DisplayDef).maxRadius;
double d = ((MyDataObject)displayObject.DataObject).d;
int radius = (int)(d * maxRadius + 1);
top = displayObject.DisplayDef.preferredRowHeight / 2 - radius;
bottom = top + 2 * radius;
}

 
Of course we need to declare to the NtTimeChartControl that our form implements the interface. So the header of our class shall look like this:
 

public partial class Form1 : Form, NtCustomObjectPaint
{
...
}

 
And in our Form1_Load() method we need to append:
 

ntTimeChartControl.customObjectPaint = this;


5. Create the instance of our Display Definition and a Perspective
 
In our Form1_Load() method we now add the code to instantiate the Display Definition of our own type and also create the Perspective with only one row. That row will hold one data type, only, which is our self defined one with dataTypeId 100.
 

MyDisplayDef mdd = new MyDisplayDef() { foregroundBrush = Brushes.Orange, maxRadius = 10, preferredRowHeight = 30 };
ntTimeChartControl.SetDisplayDef(mdd);

NtPerspective perspective = new NtPerspective();
NtRowPerspective rp = new NtRowPerspective() { rowHeight = 30 };
rp.dataTypeIds.Add(100);
perspective.rowPerspecives.Add(rp);
ntTimeChartControl.SetNtPerspective(perspective);


6. Create the data and put it to the Time Chart
 
Finally we create our data set and put it to the Time Chart:
 

List dataObjects = new List(10);
DateTime now = DateTime.Now;
Random random = new Random((int)(now.Ticks % 100000));
ulong objId = 1;
for (int i = 10; i-- > 0;) dataObjects.Add(new MyDataObject(random.NextDouble(), objId++, now.AddMinutes(random.Next(120 * 6) - 60 * 6)));
ntTimeChartControl.SetData(dataObjects);


Discussion
 
Starting the application you should see something like this:
 

 
In this example we even have not defined a Row Header, so we have no legend for our data. Anyway the data objects work fine in terms of tooltips, movability etc. So they act as any other objects within the NtTimeChartControl.
Please note that in the NtPaintCustomObject we use the Selected property of the NtDisplayObject to determine the color of the filled circle in order to reflect its selection status.
 
This tutorial of course is one of the more complex ones. But once you understand implementing the interface, things are pretty straight forward and you can implement the code for various custom drawn object types efficiently.



Back to Overview