Vai al contenuto

WPF

Windows Presentation Foundation (WPF) è una soluzione di interfaccia utente (UI) per Windows che fa parte di .NET. Originariamente sviluppata nel 2006, era destinata a essere il successore di WinForms. Per ulteriori informazioni, consulta la sua documentazione ufficiale.

WPF consente di creare interfacce utente ricche e sofisticate. Separa il codice dall'interfaccia utente utilizzando XAML, un linguaggio basato su XML sviluppato da Microsoft. cTrader Windows è un'applicazione WPF e tutti gli elementi dell'interfaccia utente che vedi sono alimentati da questa soluzione.

L'uso di WPF consente di sviluppare interfacce utente ricche di funzionalità e quasi perfette a livello di pixel. Tuttavia, WPF ha una curva di apprendimento ripida. Se desideri creare elementi dell'interfaccia utente relativamente semplici, potresti voler utilizzare controlli del grafico o WinForms.

Nota

Gli algoritmi che utilizzano WinForms o WPF possono essere eseguiti solo su macchine Windows.

Per utilizzare WPF con cBot e indicatori, modifica il compilatore di cTrader dal compilatore integrato al compilatore .NET SDK.

Configurazione del progetto

Analogamente a WinForms, prima di poter utilizzare WPF con i tuoi cBot e indicatori, dovrai apportare alcune modifiche al file di progetto del tuo indicatore o cBot. Poiché WPF funziona solo su Windows, dovrai modificare il framework di destinazione del progetto del tuo indicatore o cBot nella variante Windows di .NET.

Per farlo, apri il file di progetto in Visual Studio e sostituisci il suo contenuto con quanto segue:

 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>

Abbiamo aggiunto il tag UseWpf e modificato il valore di TargetFramework in net6.0-windows.

Successivamente, modifica il valore del parametro della classe AccessRights in FullAccess. A meno che questa modifica non venga apportata, la tua estensione non avrà diritti di accesso sufficienti per operare con WPF.

Ricompila il tuo progetto dopo aver apportato le modifiche sopra indicate.

Crea e mostra una finestra utilizzando WPF

Il processo di creazione di una finestra personalizzata è simile a creare un WinForm personalizzato.

Dopo aver configurato il tuo progetto, fai clic con il pulsante destro del mouse su di esso, seleziona Aggiungi e poi scegli Finestra (WPF)....

Image title

Nella finestra appena aperta, seleziona l'opzione Finestra WPF. Imposta il nome della finestra su MainWindow e fai clic sul pulsante Aggiungi. La nuova finestra WPF apparirà nell'esplora soluzioni del tuo progetto. Avrà due file, un file .XAML e un file .cs contenente il codice backend.

Apri il file .XAML e copia e incolla il seguente codice per cambiare il colore di sfondo in "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>

Incolla il seguente codice nel file sorgente principale del tuo indicatore:

 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 aggiungi una nuova finestra WPF, utilizzerà il namespace del tuo indicatore o cBot (come cAlgo). Nel file .cs contenente il codice backend della finestra, modificalo in WPF_Test e assicurati che corrisponda alla tua istruzione using nel codice dell'indicatore.

Ricompila l'indicatore da Visual Studio o utilizzando cTrader Algo.

Al termine del processo di compilazione, crea un'istanza dell'indicatore. Dovresti vedere la tua finestra personalizzata apparire quasi immediatamente all'avvio.

Image title

Visualizza un pannello di trading

In questo esempio creeremo un semplice pannello di trading utilizzando WPF. Crea un nuovo cBot e imposta il suo nome su WPF Trading Panel.

Apri il tuo nuovo robot in Visual Studio e aggiungi una nuova risorsa WPF come descritto sopra. Imposta il nome della finestra su MainWindow.

Image title

Successivamente, apri il file .XAML della finestra e incolla il seguente codice:

 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>

Successivamente, apri il file backend .cs e incolla il seguente codice:

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

Ora siamo pronti per avviare la nostra finestra personalizzata utilizzando il nostro cBot. Apri il file sorgente del cBot e inserisci il codice qui sotto:

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

Ricompila il tuo codice usando Visual Studio e crea un'istanza del tuo cBot. Eseguilo e dovrebbe apparire la seguente finestra.

Image title

Nella finestra, seleziona un simbolo dall'elenco dei simboli e clicca su Esegui. Anche se il nostro pannello di trading è piuttosto semplice, funziona come previsto. Puoi usarlo come modello per creare controlli WPF complessi.

Il codice nel file .xaml.cs per la nostra finestra è responsabile dell'invio di istruzioni al nostro cBot. Quando sviluppi il codice backend per le tue finestre, segui queste linee guida:

  • Crea un servizio per gestire le interazioni con un cBot o un indicatore usando il metodo BeginInvokeOnMainThread().
  • Se stai creando controlli WPF complessi, usa MVVM, IoC e separa i componenti della logica di business dall'interfaccia utente (ad esempio, usando Prism)

Utilizzo di un thread dedicato per l'interfaccia utente

Le finestre WinForms e WPF devono essere eseguite all'interno di un thread contrassegnato come STA, altrimenti non funzioneranno. Per maggiori informazioni, consulta la documentazione ufficiale.

Non puoi modificare la proprietà ApartmentState di un thread già in esecuzione. Poiché il thread principale del cBot o dell'indicatore non è contrassegnato come STA, devi utilizzare un nuovo thread contrassegnato come STA per WinForms e WPF.

Accedi ai membri dell'API dal thread dell'interfaccia utente

Come abbiamo spiegato in precedenza, devi usare un thread dedicato separato per eseguire i tuoi controlli WPF. Solo il codice in esecuzione su questo thread sarà in grado di accedere ai controlli e alle proprietà della finestra.

Lo stesso vale per tutti i membri API degli algoritmi. Poiché l'API non è thread safe, non puoi accedere ai membri dell'API dal thread dell'interfaccia utente. Invece, devi inviare il lavoro usando il metodo BeginInvokeOnMainThread() nel codice del tuo indicatore o cBot.

Questo può creare complicazioni se vuoi scambiare dati tra il tuo indicatore o cBot e una finestra WPF. Per risolvere questo problema, puoi creare una classe proxy per gestire le interazioni tra il tuo cBot o indicatore e i thread relativi all'interfaccia utente.