Using SharpDX for Custom Chart Rendering
<< Click to Display Table of Contents >> 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.
Tips: 1.There are several pre-installed examples of OnRender() and SharpDX objects used in the NinjaTrader.Custom project. For starters, please look at the SampleCustomRender indicator file 2.Although not entirely identical, the SharpDX wrapper is designed to resemble System.Drawing namespace; experienced GDI developers will be familiar with concepts discussed in this section. 3.Microsoft provides various DirectX Programming Guides aimed to educate users with the underlying C++ DirectX API. While SharpDX (C#) syntax is different, you may find these guides helpful for understanding SharpDX concepts not offered by this guide. |
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.
SharpDX Vectors and Charting Coordinates
Understanding the SharpDX.Vector2SharpDX 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 CoordinatesIf 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 ResourcesTo 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 ColorsThe 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
You can also use a SharpDX.Color3 or SharpDX.Color4 structure as a way to get more customizable colors in your rendering:
Alternatively, you can set the "transparency" of an existing brush by accessing its Opacity property:
Converting SharpDX BrushesSharpDX 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 RenderTargetA 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 commandsIf 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 ResourcesThroughout 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 OnRenderTargetChanged() 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 MethodsAll 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.
LineThe 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:
RectangleUsing 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.
EllipseSimilar 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:
GeometryFor 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 TextUp 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.
Formatting TextThe 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.
Converting TextOne 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.
Text LayoutsThe 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:
After the text has its format and layout, you can use the RenderTarget.DrawTextLayout() method to specify the exact location as a Vector2, as well as the Brush used to draw the text.
Measuring Text LayoutsWorking 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 ObjectWhen rendering SharpDX Lines and Shapes, you can optionally configure a SharpDX.Direct2D1.StrokeStyle allowing you to utilize several pre-made dash styles, or even create a custom dash pattern.
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.
|
Best Practices for SharpDX Resources
Understanding Device-dependent vs Device-independent resourcesDirect2D has several types of resources which may be mapped to the different hardware devices:
•Device-independent resources are on the CPU •Device-dependent resources are on the GPU
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 OnRenderTargetChanged() method to detect when the render target has updated and gives you an opportunity to recreate resources.
Device-dependent resources The following objects are associated with a specific RenderTarget. They must be created and dispose of any time the RenderTarget is updated:
Device-independent resourcesThe 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:
SharpDX DisposeBaseAlthough 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 PracticesIf 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.
|