EN
 
English
Italian
French
German

C++ Data Visualisation Control Plugin

How great would it be to be able to design a completely custom visual control that behaves exactly the way we need it in order to visualize our data. With a little help of C++ and our new feature C++ Visual Control Plugin is now possible developing your own visual controls. Finish this course and try yourself in creating your very own visual control.

Did you ever want to visualize some data in Dewesoft but just couldn't find the right visual control? With the help of C++ Visual Control Plugin, you can create your own visual controls and integrate them directly into Dewesoft. C++ Visual Control Plugin uses Dewesoft's DCOM interface to access its internals but abstracts the interaction away from the programmer.

C++ Visual Control Plugin allows you to create your very own visual control and modify its behaviour to suit the needs of the data you want to present. The code is compiled into an external library and automatically recognized and loaded by Dewesoft. This is why your visual control can be easily exported and imported for use on other computers.

The examples of C++ Visual Control plugins we mention in this tutorial are available on Dewesoft's webpage under Support > Downloads > Developers > C++ Plugin. Note that you have to be logged in to access the C++ Plugin section. 

In order to start using C++ Visual Control Plugin, you must have Visual Studio 2017 IDE installed on your system. Some of the reasons we have chosen Visual Studio are its functionalities, powerful developer tooling, like IntelliSense code completion and debugging, fast code editor, easy modification/customization and many more. 

Once Visual Studio is downloaded and installed you will be able to download Dewesoft plugin template using New project window and selecting DewesoftX C++ Visual Control plugin template. The template can be found with the help of the Search text box (right top corner) in the online tab.

New project window is accessed in File tab > New > Project.

Alternatively, you can download the DewesoftX Visual Control plugin template the Dewesoft webpage under Support > Downloads > Developers > C++ Plugin. Note that you have to be logged in to access the C++ Plugin section. After downloading, just double-click the file and VSIX installer will guide you through the installation process.

To get a better understanding of how to work with C++ Visual Control Plugin we will implement a very simple visual control similar to a Digital meter that already exists in Dewesoft. The Digital meter shows the value of the channel at the current timestamp, it allows one input channel at a time and it shows the name of the channel in the top left corner. 

The Digital meter that we will mimic looks like this:

Our visual control will also allow only one channel at a time to be shown and we will only show the value of the channel and not the name. 

Now we go back to Visual Studio. To create a new C++ Visual Control Plugin we click the Project button in File tab > New > Project. We select the  DewesoftX Visual Control plugin template as our template and fill in the name of our project. After clicking the Ok button a wizard window will appear to guide us through the creation of the plugin.

Since your plugin will be integrated inside Dewesoft, it needs to know Dewesoft's location. We can use our custom location (specifying the absolute path), or we can use the system variable DEWESOFT_EXE_X86 if we use 32-bit version of Dewesoft or DEWESOFT_EXE_X64 if we use 64-bit Dewesoft. We set the variable using System properties window (it can be found pressing Windows key and searching for Edit the system environment variables), and under advanced tab clicking the Environment variables.

If you only have the 64-bit (or 32-bit) version of Dewesoft on your computer, you will only be able to create 64-bit (or 32-bit) plugins.

After clicking the Next button the following window appears which is used to set Plugin information such as plugin name, its ownership, and version.

  • Plugin name - The name that will be seen in Dewesoft.
  • Description - Short description of your plugin.
  • Vendor - Company that created the plugin.
  • Copyright - Owner of the plugin.
  • Major version - Sets the initial major version. The value should change when a breaking change occurs (it's incompatible with previous versions).
  • Minor version - Sets the initial minor version. The value should change when new features and bug fixes are added without breaking compatibility.
  • Release version - Sets the initial release version. The value should change if the new changes contain only bugfixes.

All fields are optional except for Plugin name

After clicking the Next button a final window appears. This window is used to set your Base class name. It is used as a prefix for class and project name. When the Base class name is set, we can click the Finish button and the wizard will generate the plugin template based on your choices. 

The Project name is the name of the file created by the Visual Studio.
The Plugin name is is the name of the plugin as seen in Dewesoft.
The Base class name is the name of the plugin inside of Visual Studio and has to be a valid C++ name.

Structure of the solution

When a new C++ Visual Control plugin project is created, the wizard will create the basic files and project structure needed for development. In the picture below you can see the structure of a project in a tree view with collapsed items. In our case, ProTutorial refers to text which was used as the Base class name.

  • ProTutorialPlugin - The actual plugin implementation
  • ProTutorialPluginTest - Solution for writing unit tests for ProTutorialPlugin.
  • gtest - Simple library, required by ProTutorialPluginTest for unit testing your plugin. This project should not be modified. 

As mentioned before, our plugin implementation is inside the ProTutorialPlugin project. It contains files for writing the main code and Dewesoft_internal folder with methods for interacting with Dewesoft. The main code of the plugin is written in the plugin.h and plugin.cpp files. Here we connect the input channels and write the code for behavior of our custom visual control. We can also set additional properties of the plugin and save the setup variables. 

dewesoft_internal folder should not be modified.
When the solution is built for the first time, we recommend rescanning it (to clear cache). If not, some false positive errors might appear and auto-complete might not work. You can do this by clicking on the Project tab and choosing Rescan solution from the drop-down list.

When our project is successfully generated, we will be able to extend Dewesoft. But before implementing the logic behind our plugin, let's take a look at how our plugin is integrated into Dewesoft by default. In order to do that, we have to start our program using the shortcut F5 or pressing the Start button in the center of Visual Studio main toolbar. 

After Dewesoft loads, our visual control can be accessed in Measure mode under Measure > More > Pro tutorial

As we can see, it already contains an example of visual control.

As mentioned before, when we create a new C++ Visual Control Plugin it already contains an example of visual control. Before we write our own visual control we will remove the code of this example. We will also remove all the methods we won't use for writing our plugin.

It is important to keep in mind that C++ uses header files (you can recognize them by the .h extension) in addition to source files. Header files are designed to provide information about your class and are used for declaration of variables and methods, while their initialization is done in the source files with .cpp extension.  In order for our plugin to still work we need to remove the code from both the .h file and .cpp file.

The plugin.h  file should now look like this:

#pragma once
#include "interface/plugin_base.h"
#include "dcomlib/dcom_utils/colors.h"
#include <map>

enum class VisualPropertyId : long
{
};

class ProTutorialVisualControl : public Dewesoft::VisualControls::Api::VisualControl
{
public:    
    ProTutorialVisualControl();
    ~ProTutorialVisualControl() override;

    static void getPluginProperties(PluginProperties& props); 

    void drawCanvasData(DrawDataParams& drawParams) override;
    bool acceptChannel(IChannelPtr ch) override;

    void updateSetup(Setup& setup) override;

    void initVisualProperties(IVCPropertiesPtr pVCProperties) override;
    void updateVisualProperties(IVCPropertiesPtr pVCProperties) override;
    void visualPropertyChanged(bstr_t groupId, IVCPropertyPtr pVCProperty) override;
    void visualPropertyButtonClick(bstr_t groupId, IVCPropertyPtr pVCProperty, int buttonIndex) override;

private:    
    static std::map<_bstr_t, VisualPropertyId> propertyStringToEnum;
    static std::map<VisualPropertyId, _bstr_t> propertyEnumToString;

    IVCPropertiesGroupPtr group;
};

and the plugin.cpp file should now look like this:

#include "StdAfx.h"
#include "plugin.h"
#include <cstdlib>
#include <algorithm>

namespace dcom = Dewesoft::Utils::Dcom::Utils;

void ProTutorialVisualControl::getPluginProperties(PluginProperties& props)
{
    props.name = "Pro tutorial";
    props.description = "Pro tutorial example.";
    props.maxAllowedInputChannels = 4;
    props.width = 400;
    props.height = 300;
    props.extendOnAdd = true;
    props.hasUnifiedProperties = true;
}

ProTutorialVisualControl::ProTutorialVisualControl()
{
}

ProTutorialVisualControl::~ProTutorialVisualControl()
{
}

void ProTutorialVisualControl::drawCanvasData(DrawDataParams& drawParams)
{
}

bool ProTutorialVisualControl::acceptChannel(IChannelPtr ch)
{
    return true;
}

void ProTutorialVisualControl::updateSetup(Setup& setup)
{
}

void ProTutorialVisualControl::initVisualProperties(IVCPropertiesPtr pVCProperties)
{
}

void ProTutorialVisualControl::updateVisualProperties(IVCPropertiesPtr pVCProperties)
{
}

void ProTutorialVisualControl::visualPropertyChanged(bstr_t groupId, IVCPropertyPtr pVCProperty)
{
}

void ProTutorialVisualControl::visualPropertyButtonClick(bstr_t groupId, IVCPropertyPtr pVCProperty, int buttonIndex)
{
}

std::map<_bstr_t, VisualPropertyId> ProTutorialVisualControl::propertyStringToEnum =
{
};

std::map<VisualPropertyId, _bstr_t> ProTutorialVisualControl::propertyEnumToString
{
};

When we now run the plugin you should get an empty black window when you add the visual control to your display.

In order for our plugin to work it needs to communicate with Dewesoft. The plugin communicates with Dewesoft through files and functions found in Dewesoft_internals but we will access this code through functions and variables found in plugin.h and plugin.cpp files of the project. These files contain functions and events, that are triggered at a certain time (e.g. when measuring is started, when measuring is stopped, when setup is saved,...). 

In the rest of this section, we describe methods which were modified so our plugin works as it should. 

getPluginProperties

This method gets called when your plugin gets loaded into Dewesoft, this happens every time you start Dewesoft. The properties set in this method are set only once and cannot be changed later on. We can set the properties for the name and description of the visual control, the default width and height of the window when visual control is first added to the display and the number of channels the visual control can display at once. 

void ProTutorialVisualControl::getPluginProperties(PluginProperties& props)
{
    props.name = "Pro tutorial";
    props.description = "Visual control example for Pro tutorial";
    props.maxAllowedInputChannels = 1;
    props.width = 200;
    props.height = 100;
    props.extendOnAdd = true;
    props.hasUnifiedProperties = true;
}

The last two properties to set affect the behavior of the visual control when it is extended to contain multiple controls. These two properties are connected to the drawing region properties found in the upper part of the left panel that is seen when the visual control is selected. You can add or remove the number of visual controls by clicking the  or  Controls buttons (see picture below).  If the .extendOnAdd property is set to true, then when you add a new control it is the same size as the first control. But if this property is set to false then both visual controls get scaled to fit in the window of the size set for the first control. 

These multiple controls can have unified properties, this means that for example when a user sets the display color for one visual control it gets changed for all of them or if the user changes the graph type from line to histogram then the type of the graph changes for all controls. If we do not want to give the user the option to change control setting uniformly then we set the .hasUnifiedProperties property to false. If this property is set to true the user can still manually disable it by unchecking the checkbox next to Unified properties (see picture below).

VisualControl

The very first procedure that gets called when a new visual control is created. We will set the channel precision and font size to the default values and also check if the default values are correct

ProTutorialVisualControl::ProTutorialVisualControl()
{
    userChannelPrecision = kDefaultChannelPrecision;
    userFontSize = kDefaultFontSize;

    assert(userChannelPrecision >= 0 && "Precision can not be smaller than 0.");
    assert(userFontSize > 0  && "Font size must be greater than 0.");
}

In order for the above code to work, we also need to define the variables we used in the header file of the plugin. To do that we will add the following code to the private section ProTutorialVisualControl class inside the plugin.h file.

int kDefaultChannelPrecision = 3;
int kDefaultFontSize = 36;

int userChannelPrecision;
int userFontSize;

acceptChannel

Our visual control might not work for all channel types and we want to prevent the user from adding the channel to the visual control as this may produce an error. In our example, we only want to display the scalar channels and not allow the user to choose to display vector or matrix channels. In order to do that we use the acceptChannel() method. This method gets called for every channel available in the current setup of Dewesoft and if the return value for some channel is true it is then visible on the right panel of the display when the visual control is selected. 

bool ProTutorialVisualControl::acceptChannel(IChannelPtr ch)
{
    return !(ch->ArrayChannel);
}

drawCanvasData

Now we are ready to actually visualize the data from our channels. This is done in the drawCanvasData() method which usually gets called with a rate of 50 Hz. This number depends on the refresh rate set in Dewesoft settings or current system performance. The input parameter of this method is a structure called DrawDataParams that holds the canvas, rect and colorPalette. 

It is important that this function runs fast because if it is too slow you can get acquisition data lost.

Before we continue with our example let us take a closer look at these parameters and how to use them.


The colorPalette parameter holds the information about common colors that all visual controls use and are defined by the selected Dewesoft theme. The following colors are specified:

  • backColor - the background color of the control
  • highlightBackColor - the background color of controls that have some sort of user interaction or must be emphasized
  • fontColor - the color of the font
  • ticksColor - the color of axis ticks (if drawing graphs)
  • subticksColor - the color of axis subticks (if drawing graphs)

The rect parameter holds the information of the location and size of the drawing region (rectangle) of the visual control. The location of the rectangle is relative to the display, meaning that the upper left corner of the visual control will always have coordinates (0, 0) no matter where on the display it is located. We will use the rect parameter to calculate the middle of the drawing region to determine where to display the text. 

int x = int(round(rect.x + (rect.width - rect.x) / 2 - (canvas->TextWidth(text.c_str()) / 2)));    
int y = int(round(rect.y + (rect.height - rect.y) / 2 - (canvas->TextHeight(text.c_str()) / 2)));

The canvas parameter holds the functions and properties used for drawing. The Font is used for setting the text, the Brush is used for setting the text and drawn objects (rectangle, circle, etc.) background the Pen is used for setting the borders of drawn objects (lines, rectangles, etc.) We can set the following properties:

Font:

  • Color - the color of the font set as a long dcom::Color
  • Style - the style of the font (bold, italic, underline, strikeout) set as a custom enum, you can combine them with "|" operator (cbsBold | cbsUnderline) 
  • Size - the size of the font set in pixels as an integer
  • Name - the name of the font family (arial, tahoma, sanserif, etc.) set as a string

Brush:

  • Color - the color of the brush set as a long dcom::Color
  • Style - the pattern for the brush (clear, solid, diagonal, vertical, etc.) set as a custom enum

Pen:

  • Color - the color of the pen set as a long dcom::Color
  • Style - the style in which the pen draws lines (solid, dot, dash, clear, etc.) set as a custom enum
  • Width - the width of the pen in pixels set as an integer
  • Mode - determine how the color of the pen interacts with the color on the canvas (always black, the inverse of canvas background color, unchanged, etc.)

Majority of the properties are of type integer and to make the code more descriptive we use predefined custom enums to set them. All enums follow the same naming principle, the prefix of the enum is the first letters of the property you want to set (the prefix of the enum for setting canvas->Pen->Mode is cpm) followed by the general name of the setting. For example, if we want to set the font style to bold we would write canvas->Font->Style = cfsBold or if we want to set the pen style to dashed we would write canvas->Pen->Style = cpsDash.

The properties of the font, brush, and pen have to be set before we start drawing to the canvas as our drawing functions will use them to draw the objects. To draw on the canvas we use the predefined functions. If we want to just write a text to our visual control we use the TextOut() or TextRect(). The difference between these two functions is that the TextRect() function outputs the text inside a rectangle if the text is too long for the rectangle it gets clipped to fit inside it. We can also draw a rectangle: use the Rectangle() function, an ellipse: use the Ellipse() function, and a line: use the MoveTo() and LineTo() functions. The input parameters to all of these functions is the location of the object.


To get all the input channels the user selected to show on the visual control we use the getInputChannels() method and to get the current value of the channel we use the getCurrentValue() method which accepts the channel to read the value from.

In our example, we will just write the current value of the input channel to the canvas. We will use the TextOut() function and we will set the font color to match the color of the channel and the brush style to be clear. The full code in the drawCanvasData() should look like this.

void ProTutorialVisualControl::drawCanvasData(DrawDataParams& drawParams)
{
    if (getInputChannels()->Count < 1)
        return;

    ICanvasPtr canvas = drawParams.canvas;
    Rect& controlRect = drawParams.rect;
    IChannelPtr channel = getInputChannels()->Item[0];

    double currentValue = getCurrentValue(channel);
    std::string text = (std::stringstream() << std::setprecision(userChannelPrecision) << std::fixed << currentValue).str();

    canvas->Font->Size = userFontSize;

    int x = int(round(controlRect.x + (controlRect.width - controlRect.x) / 2 - (canvas->TextWidth(text.c_str()) / 2)));
    int y = int(round(controlRect.y + (controlRect.height - controlRect.y) / 2 - (canvas->TextHeight(text.c_str()) / 2)));

    canvas->Font->Color = channel->MainDisplayColor;
    canvas->Brush->Style = cbsClear;
    canvas->TextOut(x, y, text.c_str());
}

updateSetup

Even though the user can not change the precision of the channel or the font size of the text just yet (we will do this in the next section of this tutorial) we still want our plugin to remember and set these values if we save or load the setup with our visual control. Saving or loading a variable is done with the update() function on setup.

  • The first parameter of the function is the name of XML element under which your setting is saved. This parameter should be unique for every setting.
  • The second parameter is the actual value to be stored.
  • The optional third parameter specifies what the default value should be.

Updating our settings inside plugin.cpp file is done in updateSetup() as seen in the code below.

void ProTutorialVisualControl::updateSetup(Setup& setup)
{
    setup.update("fontSize", userFontSize);
    setup.update("channelPrecision", userChannelPrecision);
}

If we now run the plugin by pressing F5 and go to measure tab we can add our visual control to the display. On the picture below you can see how the visual control looks like. The channel outputted on the visual control is the time signal, you can create it in the Ch. Setup > Math > Formula and under Signals tab choosing time signal.

But what if the user wants a bigger font for the text or wants the output to have more digits. We can add an option to allow the user to change those settings. We do this by defining the visual properties that can be seen on the left panel when the visual control is selected. We will add a drop-down menu for allowing users to change the font size and a text box for allowing the user to change the precision of the channel. We will also add a reset button to reset the changed values back to default.

We will define these visual properties as enums of the type VisualPropertyId in the plugin.h file

enum class VisualPropertyId : long
{
    FontSizeDropDown = 0,
    PrecisionEdit = 1,
    ResetButton = 2
};

In the background, Dewesoft searches and uses these properties by its names defined as strings. This is why we also have to write functions for converting from enums to strings and back. In order to do this, we add the following maps to the private section of our visual control class in the plugin.h file

static std::map<_bstr_t, VisualPropertyId> propertyStringToEnum;
static std::map<VisualPropertyId, _bstr_t> propertyEnumToString;

and in the plugin.cpp we add the code for converting between the different types.

std::map<_bstr_t, VisualPropertyId> ProTutorialVisualControl::propertyStringToEnum =
{
    {L"FontSizeDropDown", VisualPropertyId::FontSizeDropDown},
    {L"PrecisionEdit", VisualPropertyId::PrecisionEdit},
    {L"ResetButton", VisualPropertyId::ResetButton}
};

std::map<VisualPropertyId, _bstr_t> ProTutorialVisualControl::propertyEnumToString
{
    {VisualPropertyId::FontSizeDropDown, L"FontSizeDropDown"},
    {VisualPropertyId::PrecisionEdit, L"PrecisionEdit"},
    {VisualPropertyId::ResetButton, L"ResetButton"}
};

initVisualProperties

We initialize the visual properties we want to add to our visual control in the initVisualProperties method. We add them to a group that we can name. We can create as many groups as we want to visually group the properties and make them easier to use. 

As mentioned before, we will add a drop-down menu, a text box, and a button. All the visual properties are added to the panel with Add...Property(_bstr_t PropertyID, _bstr_t Name ) functions. The first parameter of the function is a string name of the property and the second parameter is the text we want to display next to the property.

We add the drop-down menu with AddSelectProperty() function. To add the available font sizes to the drop-down we use the Add() call, we add the sizes as strings. 

To add the text box for precision we call the AddFloatProperty() function which adds a text box that only allows number inputs. We also set the minimal and maximal number of decimal points we will allow users to input using the MinValue and MaxValue properties of the FloatProperty. If the user inputs invalid precision number the plugin will not take into account the property change. 

The reset button is added to the panel with AddLabelProperty() function call. We still need to then add a button to this property with AddButton() function, the first parameter of the function is a hint and the second is the name of the icon. The AddButton() function is only available on the label property.

void ProTutorialVisualControl::initVisualProperties(IVCPropertiesPtr pVCProperties)
{
    group = pVCProperties->AddOrFindGroup("Advanced");
    group->Name = "Drawing Options";

    IVCSelectPropertyPtr fontSizeDropDown(group->AddSelectProperty(propertyEnumToString[VisualPropertyId::FontSizeDropDown], "Font size"));
    fontSizeDropDown->Add("36");
    fontSizeDropDown->Add("40");
    fontSizeDropDown->Add("44");
    fontSizeDropDown->Add("48");

    IVCFloatPropertyPtr precisionEdit(group->AddFloatProperty(propertyEnumToString[VisualPropertyId::PrecisionEdit], "Number of decimals"));
    precisionEdit->MinValue = 0;
    precisionEdit->MaxValue = 15;

    IVCPropertyPtr resetButton(group->AddLabelProperty(propertyEnumToString[VisualPropertyId::ResetButton], "Reset to default"));
    resetButton->AddButton("Reset properties to default", "REFRESH");
}

updateVisualProperties

The updateVisualProperties() procedure gets called immediately after initVisualProperties() and every time after a visual property changes on visualPropertyChanged(). Here we set the values of the properties, but they must be initialized beforehand. We will set the drop-down menu's chosen item to the value saved in the userFontSize variable and we will display the userChannelPrecision value in the precision text box.

void ProTutorialVisualControl::updateVisualProperties(IVCPropertiesPtr pVCProperties)
{
    group = pVCProperties->FindGroup("Advanced");

    IVCSelectPropertyPtr fontSizeDropDown(group->FindProperty(propertyEnumToString[VisualPropertyId::FontSizeDropDown]));
    if (fontSizeDropDown)
    {
        _bstr_t fontSizeText = _variant_t(userFontSize);
        fontSizeDropDown->ItemIndex = 0;
        for (int i = 0; i < fontSizeDropDown->Count; i++)
        {
            if (fontSizeDropDown->Item(i) == fontSizeText)
            {
                fontSizeDropDown->ItemIndex = i;
                userFontSize = _wtoi(fontSizeDropDown->Item(i));
            }
        }
    }

    IVCFloatPropertyPtr precisionEdit(group->FindProperty(propertyEnumToString[VisualPropertyId::PrecisionEdit]));
    if (precisionEdit)
        precisionEdit->PutValue(userChannelPrecision);
}

visualPropertyChanged

The procedure which gets called every time a property changes, for example when the user chooses a different font size in the drop-down menu or when they input a new valid precision. We check which property has changed and assign the changed property value to the corresponding variable.

void ProTutorialVisualControl::visualPropertyChanged(bstr_t groupId, IVCPropertyPtr pVCProperty)
{
    switch (propertyStringToEnum[pVCProperty->Id])
    {
        case (VisualPropertyId::FontSizeDropDown):
        {
            IVCSelectPropertyPtr select(pVCProperty);
            userFontSize = _wtoi(select->Item(select->ItemIndex));
            break;
        }
        case (VisualPropertyId::PrecisionEdit):
        {
            IVCFloatPropertyPtr precisionText(pVCProperty);
            userChannelPrecision = static_cast<int>(precisionText->Value);
            break;
        }
    }
}

visualPropertyButtonClick

Every time a button on the visual property panel is clicked the visualPropertyButtonClick() procedure gets called. In our case, we will set the font size and precision back to default when the user clicks the reset button.

void ProTutorialVisualControl::visualPropertyButtonClick(bstr_t groupId, IVCPropertyPtr pVCProperty, int buttonIndex)
{
    if (propertyStringToEnum[pVCProperty->Id] == VisualPropertyId::ResetButton)
    {
        IVCSelectPropertyPtr fontSizeDropDown(group->FindProperty(propertyEnumToString[VisualPropertyId::FontSizeDropDown]));
        if (fontSizeDropDown)
        {
            fontSizeDropDown->ItemIndex = 0;
            userFontSize = kDefaultFontSize;
        }

        IVCFloatPropertyPtr precisionEdit(group->FindProperty(propertyEnumToString[VisualPropertyId::PrecisionEdit]));
        if (precisionEdit)
        {
            precisionEdit->PutValue(kDefaultChannelPrecision);
            userChannelPrecision = kDefaultChannelPrecision;
        }
    }
}


After all the previous steps our custom visual control will look like this:

The input channels our visual control accepts are scalar channels and the user can change the font size, precision and then with a click of the button resets the changes back to default values.

In the previous sections of this tutorial, we created a very simple digital meter which accepts scalar channels and displays the current value of the channel. But let us now move on to a more complicated example. 

We will implement a horizontal bar graph with the value of the channel written at the beginning of the bar and the value visually represented with the fullness of the bar. This visual control will accept multiple channels of scalar, vector or matrix type. We will allow the user to set the font size of the text displaying the channel name and the minimum and maximum value of the channel. The user could also choose if they want to use the set minimum and maximum value of the channel. We will also add a reset button for resetting settings back to default.

In the end, our visual control will look like this:

To create this visual control, we can create an entirely new plugin or change the one we have. In any case, we should again remove all the code from our plugin and the plugin.h and plugin.cpp files should again look like this

getPluginProperties

For this example we will set the plugin properties to allow multiple input channels to be displayed at once, we will allow the maximum number of input channels to be 4 and we will also make the default window size of the visual control a little bit bigger than in the previous case. 

void ProTutorialVisualControl::getPluginProperties(PluginProperties& props)
{
    props.name = "Pro tutorial";    
    props.description = "Visual control example for Pro tutorial";
    props.maxAllowedInputChannels = 4;
    props.width = 400;
    props.height = 300;
    props.extendOnAdd = true;
    props.hasUnifiedProperties = true;
}

As mentioned we will allow the user to choose the minimum and maximum value of the channel, this is why we have to add the variables to hold this values to the private section of the ProTutorialVisualControl class in the plugin.h file. At this point we will also add the variables to hold the information for the font size and whether or not to use the limits for the channel value the user selected.

int userFontSize = 12;
bool useManualLimits = false;
double minChannelValue = -10.0;
double maxChannelValue = 10.0;

VisualControl

In the constructor of the class, we will check if the minimum and maximum channel values we defined are valid.

ProTutorialVisualControl::ProTutorialVisualControl()
{
    assert(minChannelValue <= maxChannelValue && "Minimum channel value cannot be greater than maximum channel value.");
}

acceptChannel

As mentioned we will create a visual control that can display all channel types this is why we will just simply allow all the channels found in the current setup to be the input channels to our visual control.

bool ProTutorialVisualControl::acceptChannel(IChannelPtr ch)
{
    return true;
}

updateSetup

When loading the setup with our visual control we want to also load the information of the font size, the minimum and maximum value of the channel and whether or not to use the limits. In order to do that we need to update the variables that hold this information in the updateSetup() method.

void ProTutorialVisualControl::updateSetup(Setup& setup)
{
    setup.update("fontSize", userFontSize);
    setup.update("minChannelValue", minChannelValue);
    setup.update("maxChannelValue", maxChannelValue);
    setup.update("useManualLimits", useManualLimits);
}

The drawCanvasData() function will be a little more complicated than in the example before. To make it a little more understandable we will split the code into multiple smaller functions performing only a specific task. The main drawCanvasData()  function and the corresponding helper functions are written and explained in the next section.

In the code for drawing the visual control, we will use a lot of constant values for variables like horizontal and vertical spacing between two objects, the height of the bar, etc. We will define these values as constants in the private section of the ProTutorialVisualControl class in the plugin.h file, to distinguish them from other variables we will use the prefix "k". The names of the constants should be descriptive enough for other developers to know what they mean and what they are used for when they see them.

const int kBarHeight = 18;

const int kYSpacing = 30;
const int kXSpacing = 30; 
 
const int kTitleFontSize = 10;
const int kValueFontSize = 8;

const double kValidatorMaxLimit = 1000;
const double kValidatorMinLimit = -1000;

The main drawCanvasData() function for our example will look like this:

void ProTutorialVisualControl::drawCanvasData(DrawDataParams& drawParams)
{
    ICanvasPtr canvas = drawParams.canvas;
    Rect& controlRect = drawParams.rect;

    int yScreenCoordinate = kYSpacing;
    Rect titleRect(0, 0, controlRect.width, yScreenCoordinate);

    drawGraphTitle(drawParams, titleRect, L"Visual control example");
    yScreenCoordinate += kYSpacing;

    size_t maxChannelsOnVisibleArea = (size_t)round(controlRect.height / kYSpacing);

    int channelIndex = 0;
    while ((channelIndex < getInputChannels()->Count) && (maxChannelsOnVisibleArea > 0))
    {
        IChannelPtr channel = getInputChannels()->Item[channelIndex];

        drawChannelName(channel, drawParams, kXSpacing, yScreenCoordinate);
        yScreenCoordinate += canvas->TextHeight(channel->Name);

        drawChannelValue(channel, drawParams, controlRect.width, yScreenCoordinate, maxChannelsOnVisibleArea);
        yScreenCoordinate += channel->ArraySize * kYSpacing;

        channelIndex++;
    }
}

As we can see we will create two local variables one to remember the index of the last displayed channel and the other to keep track of how much horizontal space on the visual control we already used. We will also create a title rectangular for displaying the global title of the visual control and display it with the drawGraphTitle() function that we implemented ourselves. We will also implement a function for drawing the current channel's name, value and unit. Our functions for drawing are defined in the private section of the ProTutorialVisualControl  class in the plugin.h file. We will also need a lot of additional helper functions, you can see their definitions in the code below. 

class ProTutorialVisualControl : public Dewesoft::VisualControls::Api::VisualControl
{
public:
    // ...

private:
    // ...
    double getChannelMinValue(IChannelPtr ch);
    double getChannelMaxValue(IChannelPtr ch);

    int getCenterTextCoordinate(DrawDataParams& drawParams, Rect& rect, const std::wstring& text);
    int getBarWidth(double currentValue, double minValue, double maxValue, Rect& barRect);

    void drawGraphTitle(DrawDataParams& drawParams, Rect& titleRect, const std::wstring& text);
    void drawChannelName(IChannelPtr channel, DrawDataParams& drawParams, int x, int y);
    void drawChannelValue(IChannelPtr channel, DrawDataParams& drawParams, int controlRectWidth, int y, size_t& maxChannelsOnVisibleArea);
    void drawUnit(IChannelPtr channel, DrawDataParams& drawParams, Rect& barRect);

    void drawBar(IChannelPtr channel, size_t arrayIndex, DrawDataParams& drawParams, Rect& barRect);
    std::wstring buildBarText(IChannelPtr channel, size_t arrayIndex, double currentValue);

    std::wstring getChannelAxisValue(IChannelPtr ch, int axisIndex, int index);
}
What each of these functions does and how to use them will be explained later on in this section.

void ProTutorialVisualControl::drawGraphTitle(DrawDataParams& drawParams, Rect& titleRect, const std::wstring& text) 
{
    ICanvasPtr canvas = drawParams.canvas;
    ColorPalette& colorPalette = drawParams.colorPalette;

    int textCenterXY = getCenterTextCoordinate(drawParams, titleRect, text);

    canvas->Font->Size = kTitleFontSize;
    canvas->Font->Color = static_cast<long>(colorPalette.fontColor);
    canvas->Brush->Style = cbsClear;
    canvas->TextRect(titleRect.x, textCenterXY, titleRect.width, titleRect.height, text.c_str(), ctfCenter); 
}

The drawGraphTitle() function accepts drawParams, title rectangle, and title text as parameters and it draws the text inside a TextRect(). We want to output the text in the center on the top of the rectangle. The center of the rectangular is calculated with the getCenterTextCoordinate() function whose implementation looks like this.

int ProTutorialVisualControl::getCenterTextCoordinate(DrawDataParams& drawParams, Rect& rect, const std::wstring& text)
{
    return int(round(rect.y + (rect.height - rect.y) / 2 - (drawParams.canvas->TextHeight(text.c_str()) / 2)));
}

void ProTutorialVisualControl::drawChannelName(IChannelPtr channel, DrawDataParams& drawParams, int x, int y)
{
    ICanvasPtr canvas = drawParams.canvas;
    
    canvas->Font->Size = userFontSize;
    canvas->Font->Color = channel->MainDisplayColor;
    canvas->Brush->Style = cbsClear;
    canvas->TextOut(x, y, channel->Name);
}

The drawChannelName() is a very simple function that sets the font size and color of the text we want to display and then displays the name of the current channel. The position of the text is calculated beforehand in the drawCanvasData() function.


void ProTutorialVisualControl::drawChannelValue(IChannelPtr channel, DrawDataParams& drawParams, int controlRectWidth, int y, size_t& maxChannelsOnVisibleArea)
{
    Rect barRect(kXSpacing, y, controlRectWidth - 4 * kYSpacing, kBarHeight);

    drawUnit(channel, drawParams, barRect);

    size_t arrayIndex = 0;
    while ((arrayIndex <static_cast<size_t>(channel->ArraySize)) && (maxChannelsOnVisibleArea > 0))
    {
        drawBar(channel, arrayIndex, drawParams, barRect);

        maxChannelsOnVisibleArea--;
        arrayIndex++;
    }
}

Now we just need to draw the channel value inside a bar. The bar will be represented as a rectangular and inside of it we first draw the value of the channel and then fill the rectangular to match the channel value. We will create an additional function drawBar() for drawing the actual bar. We also add additional helper functions getBarWidth() for calculating how much of the bar we have to fill, buildBarText() for reading the correct text from the channel and getChannelAxisValue() if the input channel is vector or matrix to read the values from the axis. The drawChannelValue() function gets called for every channel that will be displayed on our visual control. If the channel is of vector or matrix type then we will display every axis value as its own bar with name and unit. We also need to keep track of how many channels we been already drawn on visual control because we do not want to draw a channel on an area of the visual control that can not be seen by the user.


void ProTutorialVisualControl::drawUnit(IChannelPtr channel, DrawDataParams& drawParams, Rect& barRect) 
{ 
    ICanvasPtr canvas = drawParams.canvas;
    ColorPalette& colorPalette = drawParams.colorPalette;

    canvas->Font->Size = kValueFontSize; 
    canvas->Font->Color = static_cast<long>(colorPalette.fontColor); 
    canvas->Brush->Style = cbsClear;
    canvas->TextOut(barRect.x + barRect.width + 10, barRect.y, channel->Unit_); 
}

The drawUnit() is again a very simple function that sets the font size and color and displays the unit of the channel at the end of the bar.


void ProTutorialVisualControl::drawBar(IChannelPtr channel, size_t arrayIndex, DrawDataParams& drawParams, Rect& barRect)
{
    ICanvasPtr canvas = drawParams.canvas;
    ColorPalette& colorPalette = drawParams.colorPalette;

    double maxValue = getChannelMaxValue(channel);
    double minValue = getChannelMinValue(channel);

    double currentValue = getCurrentValue(channel, static_cast<int>(arrayIndex));
   
    canvas->Brush->Color = static_cast<long>(colorPalette.backColor);
    canvas->Pen->Color = static_cast<long>(colorPalette.fontColor);
    canvas->Rectangle(barRect.x, barRect.y, barRect.x + barRect.width, barRect.y + barRect.height);

    canvas->Brush->Color = channel->MainDisplayColor;
    canvas->Pen->Color = static_cast<long>(colorPalette.fontColor);
    canvas->FillRect(barRect.x + 1, barRect.y + 1, getBarWidth(currentValue, minValue, maxValue, barRect), barRect.y + barRect.height);
   
    canvas->Font->Size = kValueFontSize;
    canvas->Font->Color = static_cast<long>(dcom::Color::White);
    canvas->Brush->Style = cbsClear;
    canvas->TextOut(barRect.x + 2, barRect.y + 2, buildBarText(channel, arrayIndex, currentValue).c_str());

    barRect.y += kYSpacing;
}

The drawBar() function draws the value of the channel and fills the bar in the ratio to the value and the minimal and maximal value of the channel. To draw a the container of the bar we use the Rectangle() function with the specified rectangle dimensions. To fill the bar (rectangle) we use the FillRect() function and with the helper function getBarWidth() we calculate the point to which the bar needs to be filled. We will also write a getter function for reading the minimum and maximum values of the channel whether they are user-defined or default.

double ProTutorialVisualControl::getChannelMinValue(IChannelPtr ch)
{
    if (useManualLimits) 
        return minChannelValue;
    else
        return ch->TypicalMinValue;
}

double ProTutorialVisualControl::getChannelMaxValue(IChannelPtr ch)
{
    if (useManualLimits)
        return maxChannelValue;
    else
        return ch->TypicalMaxValue;
}

int ProTutorialVisualControl::getBarWidth(double currentValue, double minValue, double maxValue, Rect& barRect) 
{ 
    currentValue = std::clamp(currentValue, minValue, maxValue); 
    
    int currentValueInPixels;
    if (maxValue - minValue == 0.0) 
        currentValueInPixels = 0;
     else
        currentValueInPixels = static_cast<int>(round((currentValue - minValue) / (maxValue - minValue) * barRect.width)); 

    return barRect.x + currentValueInPixels - 1; 
} 

std::wstring ProTutorialVisualControl::buildBarText(IChannelPtr channel, size_t arrayIndex, double currentValue) 
{ 
    if (channel->ArrayChannel) 
        return getChannelAxisValue(channel, 0, arrayIndex) + L": " + std::to_wstring(currentValue);
    else
        return std::to_wstring(currentValue); 
}

Because array channels have axis values we also need to add a function for reading the values. Array channels can have multiple axes so we need to specify the index of which axis information we would like to access in the GetAxisDef() function. The axis values can be of three types: string, float or a linear function and we need to distinguish the reading of the values for each of these types. 

std::wstring ProTutorialVisualControl::getChannelAxisValue(IChannelPtr ch, int axisIndex, int index)
{
    IAxisDefPtr axisDef(ch->ArrayInfo->GetAxisDef(axisIndex));
    std::wstring unit = L" " + std::wstring(axisDef->_Unit, SysStringLen(axisDef->_Unit));
    std::wstringstream stream;
    stream << std::fixed << std::setprecision(axisDef->Precision);

    switch (axisDef->AxisType)
    {
        case TAxisType::atFloat:
            stream << axisDef->GetFloatValues(index) << unit;
            return stream.str();
        case TAxisType::atFloatLinearFunc:
            stream << axisDef->StepValue * index + axisDef->StartValue << unit;
            return stream.str();
        case TAxisType::atString:
            return std::wstring(axisDef->GetStringValues(index), SysStringLen(axisDef->GetStringValues(index))) + unit;
        default:
            return unit;
    }
}

Our visual control will now show the data like this:

The image shows two input channels both of them are the output channels of the FFT Analyser with one begin scalar channel (FFT block count) and the other one being a vector channel (AI 1/AmplFFT).

The last thing we need to do now is add the visual properties for manipulating our visual control. We will add a drop-down menu for choosing the font size, a checkbox for choosing whether or not to use manual minimum and maximum limits, two text boxes for inputting the limits and a reset button for resetting the changed properties back to default. In the end, our left panel should look like this:

As before we will first define these visual properties as enums of the type VisualProprtyId in the plugin.h file 

enum class VisualPropertyId : long
{
    MinEditText = 0,
    MaxEditText = 1,
    FontSizeSelect = 2,
    ResetLabel = 3,
    ManualLimitsCheckbox = 4
};

And create the converter maps for converting between the enum and string and then back.

std::map<_bstr_t, VisualPropertyId> ProTutorialVisualControl::propertyStringToEnum =
{
    {L"MinEditText", VisualPropertyId::MinEditText},
    {L"MaxEditText", VisualPropertyId::MaxEditText},
    {L"FontSizeSelect", VisualPropertyId::FontSizeSelect},
    {L"ResetLabel", VisualPropertyId::ResetLabel},
    {L"ManualLimitsCheckbox", VisualPropertyId::ManualLimitsCheckbox}
};

std::map<VisualPropertyId, _bstr_t> ProTutorialVisualControl::propertyEnumToString
{
    {VisualPropertyId::MinEditText, L"MinEditText"},
    {VisualPropertyId::MaxEditText, L"MaxEditText"},
    {VisualPropertyId::FontSizeSelect, L"FontSizeSelect"},
    {VisualPropertyId::ResetLabel, L"ResetLabel"},
    {VisualPropertyId::ManualLimitsCheckbox, L"ManualLimitsCheckbox"}
};

initVisualProperties

We will initiate the visual properties the same way we did in the first example. The drop-down menu, the reset button, and the text box are all properties we used before. We need to add a checkbox with AddCheckBoxProperty().

void ProTutorialVisualControl::initVisualProperties(IVCPropertiesPtr pVCProperties)
{
    group = pVCProperties->AddOrFindGroup("Advanced");
    group->Name = "Drawing Options";

    IVCSelectPropertyPtr fontSizeDropDown(group->AddSelectProperty(propertyEnumToString[VisualPropertyId::FontSizeSelect], "Font size"));
    fontSizeDropDown->Add("12");
    fontSizeDropDown->Add("14");
    fontSizeDropDown->Add("16");
    fontSizeDropDown->Add("18");

    group->AddCheckBoxProperty(propertyEnumToString[VisualPropertyId::ManualLimitsCheckbox], "Use manual limits");

    group->AddFloatProperty(propertyEnumToString[VisualPropertyId::MaxEditText], "Max channel value");
    group->AddFloatProperty(propertyEnumToString[VisualPropertyId::MinEditText], "Min channel value");

    IVCPropertyPtr labelButton(group->AddLabelProperty(propertyEnumToString[VisualPropertyId::ResetLabel], "Reset to default"));
    labelButton->AddButton("Reset properties to default", "REFRESH");
}

Before we look at the other functions we still need to implement, we will add functions for updating the validators for minimum and maximum channel values. We need to make sure that the minimum channel value is always lower than the maximum channel value. We will also enable or disable the text boxes based on the users choice to use the manual limits or not.

Our functions for setting the validator are defined in the private section of the ProTutorialVisualControl class in the plugin.h file.

class ProTutorialVisualControl : public Dewesoft::VisualControls::Api::VisualControl { public:    
    // ...
 private:    
    // ... 
    void updateMinEditValidator(); 
    void updateMaxEditValidator();  
}

The implementation of the functions in the plugin.cpp file looks like this:

void ProTutorialVisualControl::updateMinEditValidator()
{
    IVCFloatPropertyPtr minEditText(group->FindProperty(propertyEnumToString[VisualPropertyId::MinEditText]));
    if (minEditText)
    {
        minEditText->MinValue = kValidatorMinLimit;
        minEditText->MaxValue = maxChannelValue;
        IVCPropertyPtr(minEditText)->Enabled = useManualLimits;
    }
}

void ProTutorialVisualControl::updateMaxEditValidator()
{
    IVCFloatPropertyPtr maxEditText(group->FindProperty(propertyEnumToString[VisualPropertyId::MaxEditText]));
    if (maxEditText)
    {
        maxEditText->MinValue = minChannelValue;
        maxEditText->MaxValue = kValidatorMaxLimit;
        IVCPropertyPtr(maxEditText)->Enabled = useManualLimits;
    }
}

updateVisualProperties

We update all the visual properties to the value of the corresponding variables.

void ProTutorialVisualControl::updateVisualProperties(IVCPropertiesPtr pVCProperties)
{
    group = pVCProperties->FindGroup("Advanced");

    IVCCheckBoxPropertyPtr manualLimitsCheckbox(group->FindProperty(propertyEnumToString[VisualPropertyId::ManualLimitsCheckbox]));
    if (manualLimitsCheckbox)
        manualLimitsCheckbox->Checked = useManualLimits;

    IVCFloatPropertyPtr minEditText(group->FindProperty(propertyEnumToString[VisualPropertyId::MinEditText]));
    if (minEditText)
    {
        minEditText->PutValue(minChannelValue);
        updateMaxEditValidator();
    }

    IVCFloatPropertyPtr maxEditText(group->FindProperty(propertyEnumToString[VisualPropertyId::MaxEditText]));
    if (maxEditText)
    {
        maxEditText->PutValue(maxChannelValue);
        updateMinEditValidator();
    }

    IVCSelectPropertyPtr fontSizeDropDown(group->FindProperty(propertyEnumToString[VisualPropertyId::FontSizeSelect]));
    if (fontSizeDropDown)
    {
        _bstr_t fontSizeText = _variant_t(userFontSize);
        fontSizeDropDown->ItemIndex = 0;
        for (int i = 0; i < fontSizeDropDown->Count; i++)
        {
            if (fontSizeDropDown->Item(i) == fontSizeText)
            {
                fontSizeDropDown->ItemIndex = i;
                userFontSize = _wtoi(fontSizeDropDown->Item(i));
            }
        }
    }
}

visualPropertyChanged

When one of the visual properties changes we save the new value of the property to a corresponding variable.

void ProTutorialVisualControl::visualPropertyChanged(bstr_t groupId, IVCPropertyPtr pVCProperty)
{
    switch (propertyStringToEnum[pVCProperty->Id])
    {
        case VisualPropertyId::ManualLimitsCheckbox:
        {
            IVCCheckBoxPropertyPtr manualLimitsCheckbox(pVCProperty);
            useManualLimits = manualLimitsCheckbox->Checked;
            break;
        }
        case VisualPropertyId::MinEditText:
        {
            IVCFloatPropertyPtr minText(pVCProperty);
            minChannelValue = minText->Value;
            break;
        }
        case VisualPropertyId::MaxEditText:
        {
            IVCFloatPropertyPtr maxText(pVCProperty);
            maxChannelValue = maxText->Value;
            break;
        }
        case VisualPropertyId::FontSizeSelect:
        {
            IVCSelectPropertyPtr select(pVCProperty);
            userFontSize = _wtoi(select->Item(select->ItemIndex));
            break;
        }
    }
}

visualPropertyButtonClick

When the user clicks the reset button we will call a function resetPropertiesToDefault() to set the visual properties back to default.

void ProTutorialVisualControl::visualPropertyButtonClick(bstr_t groupId, IVCPropertyPtr pVCProperty, int buttonIndex)
{
    if (propertyStringToEnum[pVCProperty->Id] == VisualPropertyId::ResetLabel)
        resetPropertiesToDefault();
}

The function should first be defined in the private section of the ProTutorialVisualControl class in the plugin.h file. 

class ProTutorialVisualControl: public Dewesoft::VisualControls::Api::VisualControl
{
public:
    // ...

private
    // ...
    void resetPropertiesToDefault();
}

The implementation of the function in the plugin.cpp file looks like this:

void ProTutorialVisualControl::resetPropertiesToDefault()
{
    IVCTextPropertyPtr minEditText(group->FindProperty(propertyEnumToString[VisualPropertyId::MinEditText]));
    if (minEditText)
    {
        minEditText->PutValue("0");
        minChannelValue = 0.0;
    }

    IVCTextPropertyPtr maxEditText(group->FindProperty(propertyEnumToString[VisualPropertyId::MaxEditText]));
    if (maxEditText)
    {
        maxEditText->PutValue("0");
        maxChannelValue = 0.0;
    }

    IVCSelectPropertyPtr fontSizeDropDown(group->FindProperty(propertyEnumToString[VisualPropertyId::FontSizeSelect]));
    if (fontSizeDropDown)
    {
        fontSizeDropDown->ItemIndex = 0;
        userFontSize = 12;
    }
}

After all the steps in the previous sections, we should get a visual control that looks like this. It accepts input channels of any type (scalar, vector or matrix) and displays the current value of the channel or the axis and the channel name and unit. It also displays the title of the graph. User can change the font type and set manual minimum and maximum limits of the channel. We also added a reset button to reset the changed values back to default. 

The channels displayed on the image above are the time signal and the output channels from the FFT Analysis when the acquisition sample rate is set to 100 and the resolution of the FFT is set to 5 lines.

In this Pro Training, we have created two new visual controls. We might want to use them in other setups or on other computers. C++ Visual control Plugin packs your plugin into an external library, which can be inserted into any Dewesoft around the world.

Your C++ Visual control Plugin is found inside a file with .vc extension (it contains instructions that Dewesoft can call upon to do certain things, based on the purpose of your plugin). To export it, you need to locate these file first. It can be found inside DEWESoftX\DEWEsoft\Bin\Addons folder in a folder with the same name as the plugin base class name.

To import your plugin you have to copy and paste a file with .vc extension into any Dewesoft that requires your plugin. You need to paste it inside Addons folder so Dewesoft will be able to automatically recognize and load it.

Your C++ Visual control Plugin also creates a file with the .pdb extension, which contains instructions for your debugger. It is not necessary to export it with your .vc file in order for your plugin to work, but in case the imported plugin will be debugged, copying the entire folder is a good idea.
This website uses cookies to ensure you get the best experience on our website. Learn more