Using SharpDX for Custom Chart Rendering
NinjaTrader Chart objects (such as Indicators, Strategies, DrawingTools, ChartStyles) implement an OnRender() method aimed to render custom lines, shapes, and text to the chart. To achieve the level of performance required to keep up with market data events, NinjaTrader uses a 3rd-party open-source .NET library named SharpDX. This 3rd party library provides a C# wrapper for the powerful Microsoft DirectX API used for graphics processing and known for its hardware-accelerated performance, including 2D vector and text layout graphics used for NinjaTrader Chart Rendering. The SharpDX/DirectX library is extensive, although NinjaTrader only uses a handful of namespaces and classes, which are documented as a guide in this reference. In addition to this educational resource, we have also compiled a more focused collection of SharpDX SDK Reference resources to help you learn the SharpDX concepts used in NinjaTrader Chart Rendering.
There are three main SharpDX namespaces you need to be familiar with:
Contains basic objects used by SharpDX.
Contains objects used for rendering for 2D geometry, bitmaps, and text.
Contains objects used for text rendering
The rest of this page will help you navigate the fundamental concepts needed to achieve custom rendering to your charts.
Understanding the SharpDX.Vector2
SharpDX Draw methods use a SharpDX.Vector2 object which describes where to render a command relative to the chart panel. These Vector2 objects can be thought as a two-dimensional point in the chart panels X and Y axis. Since the chart canvas used to draw on consists of the full panel of the chart, a vector using a value of 0 for both the X and Y coordinates would be located in the top left corner of the chart:
Vector2 objects contain X and Y properties helpful to recalculate new properties based on the initial vector:
Additionally, you can recalculate a new vector from existing vector objects:
It is also helpful to know that Vector2 objects are similar to the Windows Point structure and these two types can be used interchangeably. Depending on the mechanism used to obtain user input or other application values, you may receive the coordinates in a Point. For convenience, NinjaTrader provides a DXExtension.ToVector2() method used for converting between these two objects if needed:
Calculating Chart Coordinates
If you simply used a vector with static values, your Vector2 objects would never change, and your drawing would remain fixed on a particular area of the chart (which may be desired). However, since NinjaTrader charts are dynamic and responded to various market data updates, scroll, resize, and scale operations - you also need a way to recalculate vectors to display information dynamically. To assist in this process, NinjaTrader provides some GUI related utilities to help navigate the chart and calculate values for your custom rendering.
Common utilities fall under 4 key components, and you can learn more about their specific functions from the help guide topics linked in the table below:
Understanding SharpDX Brush Resources
To color or "paint" an area of the chart, you must define custom resources which describe how you wish the custom render to appear. SharpDX contains special resources modeled after the familiar WPF Brushes. However, the two objects are different in the way they are constructed and also in how they are managed after they are used.
There are many types of SharpDX Brush Resources which all derive from the same base Direct2D1.Brush class. This base object is not enough to describe how your object should be presented, so in order to use a brush for rendering purposes, you will need to determine exactly what type of brush you wish to use:
Describing SolidColorBrush Colors
The most common and simple brush to use is a Direct2D1.SolidColorBrush which allows you to paint using a solid color (or with transparency). In the most basic form, SolidColorBrush can be constructed using a predefined SharpDX.Color
Alternatively, you can set the "transparency" of an existing brush by accessing its Opacity property:
Converting SharpDX Brushes
SharpDX Brushes are device-dependent resources, which means they can only be used with the device (i.e., RenderTarget) which created them. In practice, this mean you should ONLY create your SharpDX brushes during the chart object's OnRender() or OnRenderTargetChanged() methods.
Because of this detail, a common problem you may run into is the requirement to share a SharpDX device brush resource with a WPF application brush. For example, you may have WPF brushes defined in the UI during OnStateChange() or recalculated conditionally during OnBarUpdate(), but ultimately wish to use also in custom rendering routines. For convenience, NinjaTrader provide a DXExtension.ToDxBrush() method used for converting these objects if necessary:
Understanding the RenderTarget
A SharpDX Render Target is a general purpose object resource used for receiving and executing drawing commands. When using a NinjaTrader chart object, a pre-constructed Chart RenderTarget object is available for you to use and ready to receive commands. You can think of the RenderTarget as the device context you are using to render to (i.e. the Chart Panel). While there is nothing special you need to do to setup this resource, it is important to understand some details regarding the RenderTarget to learn how it can be used.
The RenderTarget is primarily used for executing commands such as drawing shapes or text:
It is commonly used for creating various resources such as Brushes and other SharpDX objects:
It can also be used to set various properties to describe how the RenderTarget should render:
Sequencing RenderTarget commands
If the sequence in which objects render is essential to your custom rendering, you will need to be mindful of the order in which you call various RenderTarget members. For example, we can draw a second line which uses a different AntialiasMode and the renders each line in the order the render target received its commands:
In the above example, this order of operations would result in the second RenderTarget.DrawLine() to be rendered "on top" of the first RenderTarget.DrawLine(). If you instead called these two methods in reverse order, you would not see the thinner line since it would be covered up by the thicker line.
Using the RenderTarget with Device Resources
Throughout the lifetime of a chart, the render target is created and destroyed several times to satisfy various user commands. As a result, any resources that are created need to be recreated and destroyed as that render target is updated. The NinjaTrader OnRenderTargetChange() method was designed to help with this process and will be called anytime the RenderTarget has changed. You should use this method if you have objects which are passed around from various other resources.
RenderTarget Draw Methods
All drawings consistent of a few basic shapes which can be called through a handful of RenderTarget commands. "Draw..." methods create just the outline of the shape, and "Fill..." will paint the interior of the shape.
The simplest shape is a Line, executed by the RenderTarget.DrawLine() command which just takes two Vector2 objects which describe where to draw the line, and (optionally) the width of the line to draw:
Using either the RenderTarget.FillRectangle() or RenderTarget.DrawRectangle() requires a SharpDX.RectangleF structure, constructed using four values to represent the location (x, y) and size (width, height) of the rectangle to draw.
Similar to the Rectangle, you can draw an Ellipse (or circle) using either the RenderTarget.FillEllipse() or RenderTarget.DrawEllipse() methods using a SharpDX Direct2D1 Ellipse struct. For this structure, you will need to use a Vector2 object to determine the Center position of the ellipse, a RadiusX, and a RadiusY which determines the size of the ellipse:
For more complicated shapes, you can use the RenderTarget.FillGeometry() or RenderTarget.DrawGeometry() methods using a Direct2D1.PathGeometry object, which is ultimately defined by a Direct2D1.GeometrySink interface.
To describe a PathGeometry object's path, use the object's PathGeometry.Open() method to retrieve an GeometrySink. Then, use the GeometrySink to populate the geometry with figures and segments. To create a figure, call the GeometrySink.BeginFigure() method, specify the figure's start point, and then use its Add methods (such as GeometrySink.AddLine()) to add segments. When you are finished adding segments, call the GeometrySink.EndFigure() method. You can repeat this sequence to create additional figures. When you are finished creating figures, call the GeometrySink.Close() method.
Using SharpDX for rendering Text
Up until this point, we have been using the SharpDX.Direct2D1 namespace to render shapes. When dealing with text, there is a separate SharpDX.DirectWrite namespace which works along with the Direct2D1 objects.
There are two principle objects used for text rendering: A TextFormat object which sets the style of the text, and a TextLayout object used to construct complex texts with various settings and provides metrics for measuring the shape the formatted text.
Each one of these objects has their own RenderTarget methods: RenderTarget.DrawText() for simple TextFormat objects and RenderTarget.DrawTextLayout() for more advanced layouts. Both methods accept a TextFormat object; DrawTextLayout is more complicated but has better performance since it reuses the same text layout which does not need to be recalculated.
The TextFormat object determines the font size, style and family, among other properties.
Once the text formatting has been described, you can use this object to immediately start rendering text in the DrawText() method. This approach also requires a SharpDX.RectangleF to help determine the size and position the text renders on the chart.
One common approach to text formatting is to use the same formats as existing chart objects. This provides familiar text format matching other objects which exist on the chart. To accomplish this, you can simply use the ChartControl NinjaTrader.Gui.SimpleFont object and convert to SharpDX using the ToDirectWriteTextFormat() method.
The TextLayout object works in combination with the TextFormat object by extending its functionality and providing an interface more powerful than a simple Rectangle, enabling you to position, measure, or clip the text to a surrounding shape.
When constructing the TextLayout object, you will pass in the exact text as a string you wish to render, along with the desired TextFormat. This gives you the ability to measure the text string after it has been formatted. During construction, you also have an opportunity to specify the maximum height and width of the TextLayout. For example, we can set the text layout to bound to height and width chart panel:
Measuring Text Layouts
Working with an existing TextLayout object, you can use its TextLayout.Metrics object to retrieve metadata related to the size of the formatted text. This is helpful if you are unsure of the size of the text before it is rendered. For example, you may wish to draw a rectangle around the formatted text calculated width and height. Using the approach below, the rectangle will dynamically resize to fit the text values used:
Using the StrokeStyle Object
For convenience, SharpDX provides the StrokeStyleProperties struct for creating new a StrokeStyle:
Once you have your desired stroke style properties, you can create a new stroke style object.
And then use that object with the RenderTarget.DrawLine() method:
Creating a Custom DashStyle
By setting the StrokeStyle.DashStyle property to "Custom", you can further refine the appearance of a SharpDX rendered line or shape by describing the length and space between the lines. Creating a custom DashStyle is not only useful for using RenderTarget methods, but also can be used for customizing the appearance of standard NinjaScript Plots.
The code example creates a single StrokeStyle object using custom dash style properties. The example then uses those the custom stroke style object with user defined dashes for overriding the default NinjaTrader plot appearances, and using the same stroke style in a RenderTarget.DrawLine() command.
Understanding Device-dependent vs Device-independent resources
Direct2D has several types of resources which may be mapped to the different hardware devices:
When device-dependent resources are created, system resources are dedicated to that object. Resources which are device-dependent are associated with a particular RenderTarget device and are only available on that device. Therefore, objects which were created using a RenderTarget can only be used by that device. As the RenderTarget updates, objects which were previously created will no longer be compatible and can lead to errors. You can use the NinjaTrader OnRenderTargetChange() method to detect when the render target has updated and gives you an opportunity to recreate resources.
The following objects are associated with a specific RenderTarget. They must be created and dispose of any time the RenderTarget is updated:
The following objects are NOT associated with a specific device. They can be created once and last for the lifetime of your script, or until they need to be modified:
Although most C# objects stored in memory are handled by the operating system, there are a few SharpDX resources which are not managed. It is important to take care of these resources during the lifetime of your script as there is no guarantee that NinjaTrader will be able to dispose of these unmanaged references for you.
The following commonly used objects implement from the SharpDX.DisposeBase and should be disposed any time they are created:
Since there is no guarantee that NinjaTrader will release objects from memory when your script is terminated, it is best to protect these resources from issues and call Dispose() as soon as possible. This commonly involves calling Dispose() at the end of OnRender(),or during OnRenderTargetChanged() when dealing with device- dependent resources such as brush. Device-independent resources can be created once and then retained for the life of your application.
You can also consider implementing the using Statement (C# Reference) which will implicitly call Dispose() for you when you are done:
You can check to see if can object has been disposed of by using the DisposeBase.IsDiposed property:
You should also favor managing these resources yourself, which means methods which accept a SharpDX DisposeBase object as an argument should be created before they are passed into the method and disposed of after they are used. For example, the code below should be avoided:
Instead, you should manage these objects yourself:
Other Best Practices
If possible, you should avoid using the ToDxBrush() method if it is not necessary. It is relatively harmless to use this approach for a few brushes, but can introduce performance issues if used too liberally.
Instead, you should construct a SharpDX Brush directly if a WPF brush is not ever needed:
Rendering with anti-aliasing disabled can be used to render a higher qualify shapes but comes as a performance impact. You should make sure to set this render target property back to its default when you are finished with a render routine.