Zum Inhalt

WPF

Die Windows Presentation Foundation (WPF) ist eine Windows-Benutzeroberflächenlösung (UI), die Teil von .NET ist. Ursprünglich im Jahr 2006 entwickelt, war sie als Nachfolger von WinForms gedacht. Weitere Informationen finden Sie in der offiziellen Dokumentation.

WPF ermöglicht die Erstellung einer umfangreichen und anspruchsvollen Benutzeroberfläche. Es trennt den Code von der Benutzeroberfläche durch die Verwendung von XAML, einer von Microsoft entwickelten XML-basierten Sprache. cTrader Windows ist eine WPF-Anwendung und alle UI-Elemente, die Sie sehen, werden von dieser Lösung unterstützt.

Die Verwendung von WPF ermöglicht die Entwicklung einer nahezu pixelgenauen, funktionsreichen Benutzeroberfläche. WPF hat jedoch eine steile Lernkurve. Wenn Sie relativ einfache UI-Elemente erstellen möchten, sollten Sie Chart-Steuerelemente oder WinForms verwenden.

Hinweis

Algorithmen, die WinForms oder WPF verwenden, können nur auf Windows-Rechnern ausgeführt werden.

Um WPF mit cBots und Indikatoren zu verwenden, ändern Sie Ihren cTrader-Compiler vom eingebetteten Compiler zum .NET SDK-Compiler.

Ihr Projekt konfigurieren

Ähnlich wie bei WinForms müssen Sie, bevor Sie WPF mit Ihren cBots und Indikatoren verwenden können, einige Änderungen an Ihrer Indikator- oder cBot-Projektdatei vornehmen. Da WPF nur unter Windows funktioniert, müssen Sie das Ziel-Framework Ihres Indikator- oder cBot-Projekts auf die Windows-Variante von .NET ändern.

Öffnen Sie dazu Ihre Projektdatei in Visual Studio und ersetzen Sie deren Inhalt durch Folgendes:

 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>

Wir haben das Tag UseWpf hinzugefügt und den Wert von TargetFramework in net6.0-windows geändert.

Ändern Sie anschließend den Wert des Klassenparameters AccessRights in FullAccess. Wenn diese Änderung nicht vorgenommen wird, verfügt Ihre Erweiterung nicht über ausreichende Zugriffsrechte, um mit WPF zu arbeiten.

Erstellen Sie Ihr Projekt nach den oben genannten Änderungen neu.

Erstellen und Anzeigen eines Fensters mit WPF

Der Prozess zum Erstellen eines benutzerdefinierten Fensters ähnelt dem Erstellen eines benutzerdefinierten WinForm.

Nachdem Sie Ihr Projekt konfiguriert haben, klicken Sie mit der rechten Maustaste darauf, wählen Sie Hinzufügen und dann Fenster (WPF)....

Image title

Wählen Sie im neu geöffneten Fenster die Option WPF-Fenster. Setzen Sie den Fensternamen auf MainWindow und klicken Sie auf die Schaltfläche Hinzufügen. Das neue WPF-Fenster wird in Ihrem Projekt-Lösungs-Explorer angezeigt. Es wird zwei Dateien haben, eine .XAML-Datei und eine .cs-Datei, die den Backend-Code enthält.

Öffnen Sie die .XAML-Datei und kopieren Sie den folgenden Code hinein, um die Hintergrundfarbe in "SlateGray" zu ändern.

 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>

Fügen Sie den folgenden Code in Ihre Indikator-Hauptquelldatei ein:

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

Wenn Sie ein neues WPF-Fenster hinzufügen, verwendet es den Namespace Ihres Indikators oder cBots (z. B. cAlgo). Ändern Sie ihn in der .cs-Datei, die den Fenster-Backend-Code enthält, in WPF_Test und stellen Sie sicher, dass er mit Ihrer using-Anweisung im Indikatorcode übereinstimmt.

Erstellen Sie den Indikator entweder von Visual Studio oder mit cTrader Algo neu.

Nachdem der Build-Prozess abgeschlossen ist, erstellen Sie eine Indikatorinstanz. Sie sollten Ihr benutzerdefiniertes Fenster fast unmittelbar nach dem Start erscheinen sehen.

Image title

Ein Handelspanel anzeigen

In diesem Beispiel erstellen wir ein einfaches Handelspanel mit WPF. Erstellen Sie einen neuen cBot und setzen Sie seinen Namen auf WPF Trading Panel.

Öffnen Sie Ihren neuen Roboter in Visual Studio und fügen Sie eine neue WPF-Ressource hinzu, wie oben beschrieben. Setzen Sie den Fensternamen auf MainWindow.

Image title

Öffnen Sie anschließend die .XAML-Datei des Fensters und fügen Sie den folgenden Code ein:

 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>

Öffnen Sie anschließend die Backend-.cs-Datei und fügen Sie den folgenden Code ein:

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

Wir sind nun bereit, unser benutzerdefiniertes Fenster mit unserem cBot zu starten. Öffnen Sie die cBot-Quelldatei und fügen Sie den folgenden Code ein:

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

Erstellen Sie Ihren Code mit Visual Studio neu und erstellen Sie eine Instanz Ihres cBots. Führen Sie ihn aus und das folgende Fenster sollte erscheinen.

Image title

Wählen Sie im Fenster ein Symbol aus der Symbolliste aus und klicken Sie auf Ausführen. Obwohl unser Handelspanel etwas einfach ist, funktioniert es wie beabsichtigt. Sie können es als Vorlage für die Erstellung komplexer WPF-Steuerelemente verwenden.

Der Code in der .xaml.cs-Datei für unser Fenster ist dafür verantwortlich, Anweisungen an unseren cBot zu senden. Befolgen Sie beim Entwickeln von Backend-Code für Ihre Fenster diese Richtlinien:

  • Erstellen Sie einen Dienst zur Verwaltung von Interaktionen mit einem cBot oder Indikator mithilfe der BeginInvokeOnMainThread()-Methode.
  • Wenn Sie komplexe WPF-Steuerelemente erstellen, verwenden Sie MVVM, IoC und trennen Sie Ihre Geschäftslogikkomponenten von der Benutzeroberfläche (z. B. durch Verwendung von Prism)

Verwenden Sie einen dedizierten Thread für die Benutzeroberfläche

WinForms- und WPF-Fenster müssen innerhalb eines STA-markierten Threads ausgeführt werden, andernfalls funktionieren sie nicht. Weitere Informationen finden Sie in der offiziellen Dokumentation.

Sie können die ApartmentState-Eigenschaft eines bereits laufenden Threads nicht ändern. Da der Haupt-cBot- oder Indikator-Thread nicht STA-markiert ist, müssen Sie einen neuen STA-markierten Thread für WinForms und WPF verwenden.

Auf API-Mitglieder vom UI-Thread zugreifen

Wie wir bereits erklärt haben, müssen Sie einen separaten dedizierten Thread verwenden, um Ihre WPF-Steuerelemente auszuführen. Nur der Code, der auf diesem Thread ausgeführt wird, kann auf die Fenstersteuerelemente und -eigenschaften zugreifen.

Dasselbe gilt für alle Algo-API-Mitglieder. Da die API nicht thread-sicher ist, können Sie nicht vom UI-Thread auf API-Mitglieder zugreifen. Stattdessen müssen Sie die Arbeit verteilen, indem Sie die BeginInvokeOnMainThread()-Methode in Ihrem Indikator- oder cBot-Code verwenden.

Dies kann zu Komplikationen führen, wenn Sie Daten zwischen Ihrem Indikator oder cBot und einem WPF-Fenster austauschen möchten. Um dieses Problem zu lösen, können Sie eine Proxy-Klasse erstellen, um die Interaktionen zwischen Ihrem cBot oder Indikator und UI-bezogenen Threads zu verwalten.