Przejdź do treści

WPF

Windows Presentation Foundation (WPF) to rozwiązanie interfejsu użytkownika (UI) Windows, które jest częścią .NET. Pierwotnie opracowane w 2006 roku, było zamierzone jako następca WinForms. Aby uzyskać dodatkowe informacje, zapoznaj się z jego oficjalną dokumentacją.

WPF pozwala na tworzenie bogatego i wyrafinowanego UI. Oddziela kod od UI, używając XAML, języka opartego na XML opracowanego przez Microsoft. cTrader Windows to aplikacja WPF, a wszystkie elementy UI, które widzisz, są zasilane przez to rozwiązanie.

Użycie WPF pozwala na tworzenie niemal piksel-perfekcyjnego, bogatego w funkcje UI. Jednak WPF ma stromą krzywą uczenia się. Jeśli chcesz tworzyć stosunkowo proste elementy UI, możesz chcieć użyć kontrolek wykresu lub WinForms.

Uwaga

Algorytmy używające WinForms lub WPF mogą być uruchamiane tylko na komputerach z systemem Windows.

Aby używać WPF z cBotami i wskaźnikami, zmień swój kompilator cTrader z wbudowanego kompilatora na kompilator .NET SDK.

Konfiguracja projektu

Podobnie jak w przypadku WinForms, zanim będziesz mógł używać WPF ze swoimi cBotami i wskaźnikami, będziesz musiał wprowadzić pewne zmiany w pliku projektu swojego wskaźnika lub cBota. Ponieważ WPF działa tylko na Windows, będziesz musiał zmienić docelowy framework swojego projektu wskaźnika lub cBota na wariant Windows .NET.

Aby to zrobić, otwórz plik projektu w Visual Studio i zastąp jego zawartość następującą:

 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>

Dodaliśmy tag UseWpf i zmieniliśmy wartość TargetFramework na net6.0-windows.

Następnie zmień wartość parametru klasy AccessRights na FullAccess. Jeśli ta zmiana nie zostanie wprowadzona, Twoje rozszerzenie nie będzie miało wystarczających praw dostępu do pracy z WPF.

Przebuduj swój projekt po wprowadzeniu powyższych zmian.

Utwórz i wyświetl okno za pomocą WPF

Proces tworzenia niestandardowego okna jest podobny do tworzenia niestandardowego WinForm.

Po skonfigurowaniu projektu kliknij go prawym przyciskiem myszy, wybierz Dodaj, a następnie wybierz Okno (WPF)....

Image title

W nowo otwartym oknie wybierz opcję Okno WPF. Ustaw nazwę okna na MainWindow i kliknij przycisk Dodaj. Nowe okno WPF pojawi się w eksploratorze rozwiązań Twojego projektu. Będzie miało dwa pliki: plik .XAML i plik .cs zawierający kod backendu.

Otwórz plik .XAML i skopiuj i wklej do niego następujący kod, aby zmienić jego kolor tła na "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>

Wklej następujący kod do głównego pliku źródłowego swojego wskaźnika:

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

Gdy dodasz nowe okno WPF, użyje ono przestrzeni nazw Twojego wskaźnika lub cBota (takiej jak cAlgo). W pliku .cs zawierającym kod backendu okna zmień ją na WPF_Test i upewnij się, że pasuje do Twojej instrukcji using w kodzie wskaźnika.

Przebuduj wskaźnik z Visual Studio lub używając cTrader Algo.

Po zakończeniu procesu budowania utwórz instancję wskaźnika. Twoje niestandardowe okno powinno pojawić się niemal natychmiast po uruchomieniu.

Image title

Wyświetl panel handlowy

W tym przykładzie utworzymy prosty panel handlowy za pomocą WPF. Utwórz nowego cBota i ustaw jego nazwę na WPF Trading Panel.

Otwórz swojego nowego robota w Visual Studio i dodaj nowy zasób WPF, jak opisano powyżej. Ustaw nazwę okna na MainWindow.

Image title

Następnie otwórz plik .XAML okna i wklej do niego następujący kod:

 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>

Następnie otwórz plik backendu .cs i wklej następujący kod:

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

Jesteśmy teraz gotowi do uruchomienia naszego niestandardowego okna za pomocą naszego cBota. Otwórz plik źródłowy cBota i wstaw poniższy kod:

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

Przebuduj swój kod za pomocą Visual Studio i utwórz instancję swojego cBota. Uruchom go, a następujące okno powinno się pojawić.

Image title

W oknie wybierz symbol z listy symboli i kliknij Wykonaj. Chociaż nasz panel handlowy jest nieco prosty, działa zgodnie z przeznaczeniem. Możesz go użyć jako szablonu do tworzenia złożonych kontrolek WPF.

Kod w pliku .xaml.cs dla naszego okna jest odpowiedzialny za wysyłanie instrukcji do naszego cBota. Podczas tworzenia kodu backendu dla swoich okien przestrzegaj następujących wytycznych:

  • Utwórz usługę do zarządzania interakcjami z cBotem lub wskaźnikiem przy użyciu metody BeginInvokeOnMainThread().
  • Jeśli tworzysz złożone kontrolki WPF, użyj MVVM, IoC i oddziel komponenty logiki biznesowej od interfejsu użytkownika (na przykład przy użyciu Prism)

Używanie dedykowanego wątku dla interfejsu użytkownika

Okna WinForms i WPF muszą działać w wątku oznaczonym jako STA, w przeciwnym razie nie będą działać. Więcej informacji znajdziesz w oficjalnej dokumentacji.

Nie możesz zmienić właściwości ApartmentState już działającego wątku. Ponieważ główny wątek cBota lub wskaźnika nie jest oznaczony jako STA, musisz użyć nowego wątku oznaczonego jako STA dla WinForms i WPF.

Uzyskuj dostęp do elementów API z wątku interfejsu użytkownika

Jak wyjaśniliśmy wcześniej, musisz użyć oddzielnego dedykowanego wątku do uruchamiania kontrolek WPF. Tylko kod wykonywany w tym wątku będzie mógł uzyskać dostęp do kontrolek i właściwości okna.

To samo dotyczy wszystkich elementów API algorytmów. Ponieważ API nie jest bezpieczne wątkowo, nie możesz uzyskać dostępu do elementów API z wątku interfejsu użytkownika. Zamiast tego musisz wysłać pracę przy użyciu metody BeginInvokeOnMainThread() w kodzie wskaźnika lub cBota.

Może to stworzyć komplikacje, jeśli chcesz wymieniać dane między wskaźnikiem lub cBotem a oknem WPF. Aby rozwiązać ten problem, możesz utworzyć klasę proxy do zarządzania interakcjami między cBotem lub wskaźnikiem a wątkami związanymi z interfejsem użytkownika.