Skip to content

WPF

Introduction

WPF or Windows Presentation Foundation is a Windows UI solution that is part of .NET Windows.

It is developed in 2006 by Microsoft as a successor to another very popular Windows UI framework WinFroms.

WPF allows you to create very rich and sophisticated UI, it separates the code from UI by using XAML which is a XML based language developed by Microsoft.

As of writing this documentation cTrader desktop is also a WPF application and all of it's fancy UI is powered by it.

If you want to develop a cBot or an indicator that need pixel perfect rich UI then you can use WPF.

You should use WPF only if you need a mature heavy UI technology, for other lightweight UI you can use cTrader Automate chart controls or WinFroms.

WPF has a very deep and long learning curve, if you already have experience with WPF then you can skip WinFroms and use it on your cBots/Indicators, otherwise you have to put some good amount of time to learn it.

For more about WPF please check it's documentation.

To use WPF you have to change your cTrader Automate Compiler from Embedded to SDK.

We will use Visual Studio with .NET SDK compiler not the Embedded compiler for the rest of this guide, if you don't have them please install both on your system.

Configuring Project

Before you be able to use WPF on your cBot/Indicator you have to make some changes on your indicator/cBot project file.

cTrader indicators and cBots uses .NET cross platform class library like project configuration, as WPF only works on Windows you have change the target of your indicator/cBot to Windows flavor of .NET.

To do that, open your indicator/cBot project file by double clicking on your project file in Visual Studio, then replace it's content with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0-windows</TargetFramework>
    <UseWpf>true</UseWpf>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="cTrader.Automate" Version="1.*" />
  </ItemGroup>
</Project>

We added UseWpf and changed the TargetFramework from net6.0 to net6.0-windows.

After changing your project file you also have to change the access rights of your indicator/cBot to FullAccess, otherwise it will not be able show a window.

Rebuild your project and you can start using WPF now.

If rebuild fails change the cTrader Automate compiler from Embedded to SDK, to do that go to your cTrader settings -> Automate section and select the SDK compiler instead of Embedded.

Creating and Showing a Window

It all starts from creating a WPF window and then being able to show it from your cTrader Indicator/cBot.

Here we will use an indicator and for cBots the process is same.

After you configured your indicator project file and added UseWpf tag, add a WPF window to your indicator project by right clicking on your project file and selecting Add -> Window (WPF)... option:

Image title

It will open the Add new item dialog box and it will select automatically the WPF Window from the list, set the Window name to "MainWindow" and click "Add" button.

The new WPF window will appear on your indicator project solution explorer, it will have two files, one for XAML and another which will contain the code behind of window.

Open the window XAML file and change it's background color to "SlateGray":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<Window x:Class="WPF_Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_Test"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" Background="SlateGray">
    <Grid />
</Window>

Then Paste the following code on your indicator main source file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using cAlgo.API;
using System.Threading;
using WPF_Test;

namespace WPFTest
{
    [Indicator(IsOverlay = true, AccessRights = AccessRights.FullAccess)]
    public class WPFTest : Indicator
    {
        protected override void Initialize()
        {
            var thread = new Thread(() =>
            {
                var window = new MainWindow();

                _ = window.ShowDialog();
            });

            thread.SetApartmentState(ApartmentState.STA);

            thread.Start();
        }

        public override void Calculate(int index)
        {
        }
    }
}

When you add a WPF window it will use your Indicator/cBot namespace, check the namespace of added window and then add it's using to your indicator/cBot source code file, in our case it was "WPF_Test".

Rebuild the indicator either from Visual Studio or cTrader Automate.

After your indicator built finished successfully create an instance of it by attaching it on a chart, and your WPF window should appear:

Image title

Sample Trading Panel

Let's create a sample trading panel with WPF, create a new cBot and set it's name to WPF Trading Panel.

Open it in Visual Studio, make the necessary changes on project file as described earlier, then add a new WPF window on it, set the window name to "MainWindow":

Image title

Open the Window XAML file, and paste the following code on it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<Window x:Class="WPF_Trading_Panel.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_Trading_Panel"
        mc:Ignorable="d"
        Title="WPF Trading Panel" Height="200" Width="500" Background="SlateGray" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" Closed="Window_Closed" Loaded="Window_Loaded" DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <TextBlock Grid.Column="0" Grid.Row="0" Text="Symbol" Margin="5" />
        <ComboBox Grid.Column="1" Grid.Row="0" ItemsSource="{Binding Symbols, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding SelectedSymbol, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="5" />

        <TextBlock Grid.Column="0" Grid.Row="1" Text="Direction" Margin="5" />
        <ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Directions, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding SelectedDirection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="5" />

        <TextBlock Grid.Column="0" Grid.Row="2" Text="Volume" Margin="5" />
        <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding Volume, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="5" />

        <StackPanel Orientation="Vertical" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="3" VerticalAlignment="Bottom">
            <Button x:Name="ExecuteButton" Content="Execute" Click="ExecuteButton_Click" Margin="5" />
            <Button x:Name="CloseAllButton" Content="Close All Symbol Positions" Click="CloseAllButton_Click"  Margin="5" />
        </StackPanel>
    </Grid>
</Window>

After that open the MainWindow code behind file (.cs) and paste the following code on it:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System;
using System.Collections.Generic;
using System.Windows;
using System.Collections.ObjectModel;
using cAlgo.API;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Linq;

namespace WPF_Trading_Panel
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private readonly Robot _robot;
        private string _volume, _selectedSymbol, _selectedDirection;

        public MainWindow(Robot robot)
        {
            _robot = robot;

            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<string> Symbols { get; } = new ObservableCollection<string>();

        public ObservableCollection<string> Directions { get; } = new ObservableCollection<string>(new string[] { "Buy", "Sell" });

        public string SelectedSymbol
        {
            get => _selectedSymbol;
            set
            {
                if (SetValue(ref _selectedSymbol, value))
                {
                    ChangeVolume();
                }
            }
        }

        public string SelectedDirection
        {
            get => _selectedDirection;
            set => SetValue(ref _selectedDirection, value);
        }

        public string Volume
        {
            get => _volume;
            set => SetValue(ref _volume, value);
        }

        private void ExecuteButton_Click(object sender, RoutedEventArgs e)
        {
            _robot.BeginInvokeOnMainThread(() =>
            {
                var direction = "Buy".Equals(SelectedDirection, StringComparison.Ordinal) ? TradeType.Buy : TradeType.Sell;

                var volume = double.Parse(Volume);

                _ = _robot.ExecuteMarketOrder(direction, SelectedSymbol, volume);
            });
        }

        private void Window_Closed(object sender, EventArgs _) => _robot.BeginInvokeOnMainThread(_robot.Stop);

        private bool SetValue<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Equals(storage, value))
            {
                return false;
            }

            storage = value;

            OnPropertyChanged(propertyName);

            return true;
        }

        private void Window_Loaded(object sender, RoutedEventArgs _)
        {
            PopulateSymbols();

            SelectedDirection = Directions[0];
        }

        private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        private void CloseAllButton_Click(object sender, RoutedEventArgs e)
        {
            _robot.BeginInvokeOnMainThread(() =>
            {
                foreach (var position in _robot.Positions)
                {
                    if (position.SymbolName.Equals(SelectedSymbol, StringComparison.Ordinal) is false) continue;

                    _ = _robot.ClosePositionAsync(position);
                }
            });
        }

        private void PopulateSymbols()
        {
            _robot.BeginInvokeOnMainThread(() =>
            {
                var symbolsList = new List<string>();

                symbolsList.AddRange(_robot.Symbols);

                _ = Dispatcher.BeginInvoke(() =>
                {
                    foreach (var symbol in symbolsList)
                    {
                        Symbols.Add(symbol);
                    }

                    SelectedSymbol = Symbols.FirstOrDefault();
                });
            });
        }

        private void ChangeVolume()
        {
            if (string.IsNullOrWhiteSpace(SelectedSymbol)) return;

            _robot.BeginInvokeOnMainThread(() =>
            {
                var minVolume = _robot.Symbols.GetSymbol(SelectedSymbol).VolumeInUnitsMin;

                _ = Dispatcher.BeginInvoke(() => Volume = minVolume.ToString());
            });
        }
    }
}

Now our window is ready, but we have to launch it from our cBot, open your cBot main source file, and then paste the following code on it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using cAlgo.API;
using System.Threading;
using WPF_Trading_Panel;

namespace WPFTradingPanel
{
    [Robot(AccessRights = AccessRights.FullAccess)]
    public class WPFTradingPanel : Robot
    {
        protected override void OnStart()
        {
            var thread = new Thread(() =>
            {
                var window = new MainWindow(this);

                _ = window.ShowDialog();
            });

            thread.SetApartmentState(ApartmentState.STA);

            thread.Start();
        }
    }
}

Rebuild the cBot from Visual Studio, if there is any error try to fix it.

Once it got built successfully create an instance of it and start it, you should see this window:

Image title

Select a symbol from it's symbols list, and click on Execute button, it should open a new market order for that symbol, play with it.

Our WPF Trading Panel doesn't work very good, but it works.

For sending commands to cBot we used the code behind file of our Window, if you are going to develop something please follow theses guidelines:

  • Create a service for managing the interaction with cBot by using it's BeginInvokeOnMainThread method
  • If you are building something serious use MVVM, IoC, and decouple your business from UI (Prism maybe a good option)
  • For making your UI better you can use MahApps.Metro or any other third party WPF controls

Using a Dedicated Thread for UI

When running your WPF UI related code you have to use an STA marked thread because Windows Forms and WPF windows must run inside a STA marked thread otherwise they will not work.

For more detail regarding STA thread apartment check STAThreadAttribute and CA2232.

You can't change a running thread ApartmentState, because the main cBot/Indicator thread is not a STA marked you have to create a new thread and use it only for your UI related stuff.

Accessing API Members From UI Thread

As we explained before you have to use a separate dedicated thread to run the window, and only code executing by this thread will be able to access the controls, bindings, and other thins related to your UI.

The same is through for cTrader Automate API members, as API is not thread safe you can't access the API members from the UI thread, you have to dispatch the work by using your indicator/cBot BeginInvokeOnMainThread method.

It will add a bit of complexity when you want to exchange data between your indicator/cBot and the UI thread.

You can create a proxy class that will manage the interactions between your cBot/Indicator and UI threads.


Last update: July 1, 2022

Comments