Skip to content

WPF

Introduction

The Windows Presentation Foundation (WPF) is a Window UI solution that is a part of .NET. Originally developed in 2006, it was intended as a successor to WinForms. For additional information, please consult its official documentation.

In brief, WPF allows for creating rich and sophisticated UI. It separates the code from the UI by using XAML, an XML-based language developed by Microsoft. In fact, as of the time of publishing this documentation, cTrader is a WPF application and all UI elements you are seeing are powered by this solution.

The use of WPF allows for developing near pixel-perfect feature-rich UI. However, WPF has a steep learning curve. If you want to create relatively simple UI elements, you may want to use chart controls or WinForms.

To use WPF with cBots/indicators, change your cTrader Automate compiler from the embedded compiler to the .NET SDK compiler.

How to Configure Your Project

Similarly to WinForms, before you can use WPF with your cBots/indicators, you will have to make some changes to your indicator/cBot project file. As WPF only works on Windows, you will have to change the target of your indicator/cBot framework of your project to the Windows 'flavor' of .NET.

To do so, open your project file in Visual Studio and replace its contents with the following.

 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 have added the UseWpf tag and changed the value of TargetFramework to net6.0-windows.

Afterward, change the value of the AccessRights class parameter to FullAccess. Unless this change is made, your extension will not have sufficient access rights to open a WinForms window.

Rebuild your project after making the above changes. If the rebuild fails, change your Automate compiler to the .NET SDK compiler.

How to Create and Show a Window Using WPF

The process of creating a custom window is very similar to creating a custom WinForm.

After you have configured your project, right-click on it, select 'Add' and then choose 'Window (WPF)...'.

Image title

In the newly opened window, select the 'WPF Window' option. Set the window name to "MainWindow" and click on the 'Add' button.

The new WPF window will appear in your project solution explorer. It will have two files, a .XAML file and a .cs file containing the backend code.

Open the .XAML file and copy and paste the following code into it to change its 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>

Paste the following code into 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 new WPF window, it will use the namespace of your indicator/cBot (e.g., cAlgo). In the .cs file containing the window backend code, change it to WPF_Test and make sure it matches your using statement in the indicator code.

Rebuild the indicator either from Visual Studio or using cTrader Automate.

After the build process is finished, create an indicator instance. You should see your custom window appear almost immediately upon launch.

Image title

How to Display a Trading Panel

In this example, we will create a simple trading panel using WPF. To do so, create a new cBot and set its name to WPF Trading Panel.

Open your new robot in Visual Studio, and add a new WPF resource as outlined above. Set the window name to MainWindow.

Image title

Afterward, open the window .XAML file and paste the following code into 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>

Subsequently, open the backend .cs file and paste the following code.

  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());
            });
        }
    }
}

We are now ready to launch our custom window using our cBot. Open the cBot source file and insert the below code.

 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 your code using Visual Studio and create an instance of your cBot. Run it, and the following window should appear.

Image title

In the window, select a symbol from the symbols list and click on 'Execute'. While our trading panel is a bit simple, it works as intended. You can use it as a template for creating complex WPF controls.

The code in the .xaml.cs file for our window is responsible for sending instructions to our cBot. When developing backend code for your windows, please, follow these guidelines.

  • Create a service for managing interactions with a cBot/indicator using the BeginInvokeOnMainThread() method.
  • If you are building complex WPF controls, use MVVM, IoC, and decouple your business functionality from the UI (e.g., by using Prism)

How to Use a Dedicated Thread for UI

WindowsForms and WPF windows have to run inside an STA-marked thread, otherwise, they will not work. For more information, please, refer to the official documentation.

You cannot change the ApartmentState property of an already running thread. Because the main cBot/indicator threat is not STA-marked, you have to use a new STA-marked thread for WinForms and WPF.

How to Access API Members From the UI Thread

As we have explained before, you have to use a separate dedicated thread to run your WPF controls. Only the code executing on this thread will be able to access the window controls and properties.

The same is true for cTrader Automate API members. As the API is not thread safe, you cannot access API members from the UI thread. Instead, you have to dispatch the work by using the BeginInvokeOnMainThread() method in your indicator/cBot code.

This may create complications if you want to exchange data between your indicator/cBot and a WPF window. To resolve this issue, you may create a 'proxy' class to manage the interactions between your cBot/indicator and UI-related threads.


Last update: February 3, 2023