Ir para o conteúdo

WPF

O Windows Presentation Foundation (WPF) é uma solução de interface de utilizador (UI) do Windows que faz parte do .NET. Originalmente desenvolvido em 2006, foi concebido como um sucessor do WinForms. Para informações adicionais, consulte a sua documentação oficial.

O WPF permite criar UIs ricas e sofisticadas. Separa o código da UI usando XAML, uma linguagem baseada em XML desenvolvida pela Microsoft. O cTrader Windows é uma aplicação WPF e todos os elementos de UI que vê são alimentados por esta solução.

O uso do WPF permite desenvolver UIs ricas em recursos quase pixel-perfect. No entanto, o WPF tem uma curva de aprendizagem acentuada. Se quiser criar elementos de UI relativamente simples, pode querer usar controlos de gráfico ou WinForms.

Nota

Os Algos que usam WinForms ou WPF só podem ser executados em máquinas Windows.

Para usar WPF com cBots e indicadores, mude o seu compilador cTrader do compilador incorporado para o compilador .NET SDK.

Configurar o seu projeto

De forma semelhante aos WinForms, antes de poder usar WPF com os seus cBots e indicadores, terá de fazer algumas alterações ao ficheiro de projeto do seu indicador ou cBot. Como o WPF só funciona no Windows, terá de alterar o framework alvo do seu projeto de indicador ou cBot para a variante Windows do .NET.

Para o fazer, abra o ficheiro do seu projeto no Visual Studio e substitua o seu conteúdo pelo seguinte:

 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>

Adicionámos a tag UseWpf e alterámos o valor de TargetFramework para net6.0-windows.

Depois, altere o valor do parâmetro da classe AccessRights para FullAccess. A menos que esta alteração seja feita, a sua extensão não terá direitos de acesso suficientes para operar com WPF.

Reconstrua o seu projeto após fazer as alterações acima.

Criar e mostrar uma janela usando WPF

O processo de criar uma janela personalizada é semelhante a criar um WinForm personalizado.

Depois de ter configurado o seu projeto, clique com o botão direito nele, selecione Add e depois escolha Window (WPF)....

Image title

Na janela recém-aberta, selecione a opção WPF window. Defina o nome da janela como MainWindow e clique no botão Add. A nova janela WPF aparecerá no explorador de soluções do seu projeto. Terá dois ficheiros, um ficheiro .XAML e um ficheiro .cs contendo o código de backend.

Abra o ficheiro .XAML e copie e cole o seguinte código nele para alterar a cor de fundo para "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>

Cole o seguinte código no ficheiro fonte principal do seu indicador:

 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)
        {
        }
    }
}

Quando adiciona uma nova janela WPF, ela usará o namespace do seu indicador ou cBot (como cAlgo). No ficheiro .cs contendo o código de backend da janela, altere-o para WPF_Test e certifique-se de que corresponde à sua declaração using no código do indicador.

Reconstrua o indicador a partir do Visual Studio ou usando o cTrader Algo.

Após o processo de compilação estar concluído, crie uma instância do indicador. Deverá ver a sua janela personalizada aparecer quase imediatamente após o lançamento.

Image title

Exibir um painel de negociação

Neste exemplo, criaremos um painel de negociação simples usando WPF. Crie um novo cBot e defina o seu nome como WPF Trading Panel.

Abra o seu novo robô no Visual Studio e adicione um novo recurso WPF como descrito acima. Defina o nome da janela como MainWindow.

Image title

Depois, abra o ficheiro .XAML da janela e cole o seguinte código nele:

 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>

Subsequentemente, abra o ficheiro .cs de backend e cole o seguinte código:

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

Agora estamos prontos para lançar a nossa janela personalizada usando o nosso cBot. Abra o ficheiro fonte do cBot e insira o código abaixo:

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

Reconstrua o seu código usando o Visual Studio e crie uma instância do seu cBot. Execute-o e a seguinte janela deverá aparecer.

Image title

Na janela, selecione um símbolo da lista de símbolos e clique em Execute. Embora o nosso painel de negociação seja um pouco simples, funciona como pretendido. Pode usá-lo como um modelo para criar controlos WPF complexos.

O código no ficheiro .xaml.cs para a nossa janela é responsável por enviar instruções ao nosso cBot. Ao desenvolver código de backend para as suas janelas, siga estas diretrizes:

  • Crie um serviço para gerir interações com um cBot ou indicador usando o método BeginInvokeOnMainThread().
  • Se estiver a construir controlos WPF complexos, use MVVM, IoC e separe os seus componentes de lógica de negócios da UI (por exemplo, usando Prism)

Usar uma thread dedicada para UI

As janelas WinForms e WPF têm de ser executadas dentro de um thread marcado como STA, caso contrário, não funcionarão. Para mais informações, consulte a documentação oficial.

Não pode alterar a propriedade ApartmentState de uma thread já em execução. Como a thread principal do cBot ou indicador não está marcada como STA, tem de usar uma nova thread marcada como STA para WinForms e WPF.

Aceder a membros da API a partir do thread da UI

Como explicámos anteriormente, tem de usar um thread dedicado separado para executar os seus controlos WPF. Apenas o código executado neste thread poderá aceder aos controlos e propriedades da janela.

O mesmo se aplica a todos os membros da API de algo. Como a API não é thread safe, não pode aceder aos membros da API a partir do thread da UI. Em vez disso, tem de despachar o trabalho usando o método BeginInvokeOnMainThread() no código do seu indicador ou cBot.

Isto pode criar complicações se quiser trocar dados entre o seu indicador ou cBot e uma janela WPF. Para resolver este problema, pode criar uma classe proxy para gerir as interações entre os threads relacionados com o seu cBot ou indicador e UI.