Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

A C# 3D Surface Plot Control

5.00/5 (20 votes)
19 Feb 2021CPOL6 min read 46.4K   3.1K  
A 3D surface plot control in C#
This article describes a 3D surface plot control that can display real time data in C# WinForms and WPF applications. The library includes a WPF configuration view, and classes to serialise the configuration to and from the registry. Adding the control to an application is very straight forward.

Introduction

This article describes a C# control that plots a 2D array of Z values as a surface or a scatter plot. The control can be used in WPF and WinForms applications. It is designed for the real time display of data, i.e., data that is constantly being updated. Adding the control to an application is very straight forward.

The display is highly configurable including:

  1. Optional labels on each axis
  2. The user can select the colour, size and orientation of the labels.
  3. An optional Z bar which displays the colour corresponding to each Z axis value.
  4. Optional grid connecting the data points
  5. The user can select the grid colour.
  6. Optional surface shading
  7. Optional scatter plot display
  8. A hold feature, whereby only the maximum (or minimum) Z values at each X,Y point are shown.
  9. User selectable projections: 3D, orthographic and bird's eye
  10. User selectable background colour
  11. Optional frame around the sides of the display
  12. The user can select the frame colour.
  13. A WPF view that allows the user to configure the display.
  14. The user can manipulate the view using the keyboard including zoom, rotate and move.
  15. User selectable perspective.

The control shapes and text using Open GL, via the C# OpenTK library. OpenGL is fast as it uses the PC's graphics card. Performance will of course depend on the nature of the PC and the graphics card.

The settings are passed to the control as an instance of a class derived from the IConfiguration interface. I have provided a basic implementation. The configuration can be read from and written to the registry.

The library includes a WPF user control that can be added to a form or a dialog and which allows the user to configure the display settings, such as the frame colour and the label size. The settings include zoom, Z axis scaling and perspective. I have also created a simple export method for use by WinForms applications that displays the configuration view.

There are two simple demonstration applications, one using WPF and the other using WinForms.

Example Plots

The following shows a simple surface plot with axes, labels and frame:

Image 1

In the above, the surface shading is continuous, i.e., smooth.

The following example has floating point axes formatted by the calling application:

Image 2

Elements such as labels and axes titles can be removed. In the following example, the labels and axes titles are not drawn:

Image 3

The colour scheme can be changed. In the following example, the background is black:

Image 4

In the following example, the grid mesh is not drawn, and the grid shading is coarse rather than continuous:

Image 5

Data can be plotted as points:

Image 6

The surface can be shown in a bird's eye view:

Image 7

And data can be shown as an orthographic projection (from the side):

Image 8

Configuration View

The library includes a view, implemented as a WPF user control, that allows the user to adjust the display settings:

Image 9

Keyboard Controls

The user can manipulate the view using the mouse and keyboard as follows:

  • Zoom in and out: Hold the left mouse button down, and rotate the mouse wheel.
  • Moves the plot: Hold the left mouse button and control key down, and move the mouse.
  • Rotate about the X, Y and Z axes: Hold the left mouse button down, and move the mouse.

Background

You will need a good understanding of writing Windows applications in C#. A basic knowledge of WPF is helpful, but not essential. You do not need to know anything about OpenGL.

Using the Code

I have provided a simple demonstration application which shows how to use the control:

Image 10

The surface plot control is embedded in the application's main window as follows:

XML
<WindowsFormsHost Grid.Row="0" Grid.RowSpan="2" 
 Grid.Column="1" Margin="0" Background="Transparent" 
 HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <SurfacePlot:SurfacePlotControl x:Name="_surfacePlotControl"/>
</WindowsFormsHost>

The main window initializes the control after the window has been loaded:

C#
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    OpenControls.Wpf.SurfacePlotterDemo.ViewModel.MainViewModel mainViewModel = 
    new OpenControls.Wpf.SurfacePlotterDemo.ViewModel.MainViewModel();
    DataContext = mainViewModel;

    mainViewModel.Load();
    _configurationControl.DataContext = 
    new OpenControls.Wpf.SurfacePlot.ViewModel.ConfigurationControlViewModel
    (mainViewModel.IConfiguration);
    _surfacePlotControl.Initialise(mainViewModel.IConfiguration);
}

The IConfiguration property of the main window view model class is declared as follows:

C#
private readonly Model.IConfiguration IConfiguration;

The IConfiguration interface defines the configuration settings:

C#
public interface IConfiguration : IConfigurationSubject
{
    // Signalled whenever a configuration setting is changed
    event ConfigurationChangedEventHandler ConfigurationChanged;

    void Load(IConfigurationSerialiser configurationSerialiser);
    void Save(IConfigurationSerialiser configurationSerialiser);

    int Zoom { get; set; }
    int MaximumZoom { get; }
    int MinimumZoom { get; }
    double ZScale { get; set; }
    string BackgroundColour { get; set; }
    bool ShowAxes { get; set; }
    bool ShowAxesTitles { get; set; }
    bool ShowZBar { get; set; }
    bool ShowFrame { get; set; }
    string FrameColour { get; set; }
    bool ShowLabels { get; set; }
    string LabelColour { get; set; }
    int LabelFontSize { get; set; }
    int LabelAngleInDegrees { get; set; }
    bool TransparentLabelBackground { get; set; }
    XYLabelPosition XYLabelPosition { get; set; }
    float Perspective { get; set; }
    ViewProjection ViewProjection { get; set; }
    ShadingMethod ShadingMethod { get; set; }
    bool ShowGrid { get; set; }
    string GridColour { get; set; }
    bool ShowScatterPlot { get; set; }
    bool ShowShading { get; set; }
    ShadingAlgorithm ShadingAlgorithm { get; set; }
    short BlueLevel { get; set; }
    short RedLevel { get; set; }
    bool Hold { get; set; }
    bool HoldMaximum { get; set; }
}

Observers (including the surface plot control) request a notification when a setting changes by adding a handler to the ConfigurationChanged event. The application may also serialize settings to and from a storage medium.

The IConfiguration interface is implemented by the Configuration class.

C#
IConfiguration = new OpenControls.Wpf.SurfacePlot.Model.Configuration();

The plot is updated by calling the SetData method on the surface plot control. This method must be called on a UI thread as per the following example:

C#
this.Dispatcher.Invoke(delegate
{
    _surfacePlotControl.SetData(data, -50, 50, 21, -50, 50, 21, zMin, zMax, 21);
});

The arguments to the SetData method are as follows:

numberOfZLabels

Type Name Meaning
List<List<float>> lineData The Z values to plot
float xMin The minimum X value
float xMax The maximum X value
int numberOfXLabels The number of X axis labels
float yMin The minimum Y value
float yMax The maximum Y value
int numberOfYLabels The number of Y axis labels
float zMin The minimum Z value
float zMax The maximum Z value
float numberOfZLabels The number of Z axis labels

Note that the minimum and maximum values define the axes label values.

Storing the Configuration

The main window loads the configuration as follows:

C#
IConfiguration.Load(IConfigurationSerialiser);

And it saves the configuration as follows:

C#
IConfiguration.Save(IConfigurationSerialiser);

The IConfigurationSerialiser argument is an instance of the IConfigurationSerialiser interface:

C#
public interface IConfigurationSerialiser
{
    void WriteEntry<T>(string key, T value);
    T ReadEntry<T>(string key, T value);
}

The interface defines methods to serialize settings to and from a storage medium.

The IConfigurationSerialiser interface is implemented by the ConfigurationSerialiser class:

C#
IConfigurationSerialiser = new SurfacePlot.Model.ConfigurationSerialiser();

The ConfigurationSerialiser class serializes settings to and from the registry.

You can, of course, implement your own class and derive it from the IConfigurationSerialiser interface.

Label Format

By default, the labels are formatted as per the following example:

C#
public string XLabel(float x, int index) => x.ToString("G4");

Thus 100 would appear as "100" and 12.5 as "12.5".

The application may override the default label format by passing an instance of the ILabelFormatter interface to the surface plot control. For example:

C#
_surfacePlotControl.ILabelFormatter = this;

In the above example, the this pointer is an instance of a class that implements the interface.

The ILabelFormatter interface is defined as follows:

C#
public interface ILabelFormatter
{
    string XLabel(float x, int index);
    string YLabel(float y, int index);
    string ZLabel(float z);
}

WinForms: Displaying the Configuration Dialog

The configuration dialog is for use by WinForms applications:

Image 11

The following code displays the dialog:

C#
OpenControls.Wpf.SurfacePlot.Exports.ShowConfigurationDialog(configuration);

Where the configuration argument is an instance of the Configuration class described earlier.

Shortcomings

The control does not render the scatter plot points as spheres or circles. I tried drawing polygons to approximate spheres, but it required too much processing, and slowed down the drawing to an unacceptable degree.

Points of Interest

OpenGL does not define text drawing methods. On initialization, the code creates and saves a bitmap of characters. To draw a character, it draws a square using a section from the bitmap as the fill texture. This is repeated for each character in each string.

OpenGL can be used in direct and indirect modes. In indirect mode, the data to be rendered is copied to internal buffers for subsequent display during a paint request. In direct mode, the data is passed directly to Open GL during the paint request. Indirect mode is more efficient and faster. However, it has little if any advantage when displaying real time data, as the buffers have to be constantly updated. The surface plot control uses OpenGL in direct mode.

GitHub

The code is available on GitHub as part of the OpenControls solution:

History

  • 19th February, 2021: First version
  • 21st February, 2021: Added a function to display the settings view, for use by WinForms applications,
  • 23rd February, 2021: Adjusted the projection parameters as the far point was too close when plotting a large number of points

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)