Перейти к содержанию

WPF

Windows Presentation Foundation (WPF) — это решение для пользовательского интерфейса (UI) Windows, которое является частью .NET. Первоначально разработанный в 2006 году, он задумывался как преемник WinForms. Для получения дополнительной информации обратитесь к его официальной документации.

WPF позволяет создавать богатый и сложный UI. Он отделяет код от UI, используя XAML, язык на основе XML, разработанный Microsoft. cTrader Windows — это WPF-приложение, и все элементы UI, которые вы видите, работают на этом решении.

Использование WPF позволяет разрабатывать практически попиксельно точный многофункциональный UI. Однако у WPF крутая кривая обучения. Если вы хотите создать относительно простые элементы UI, вы можете использовать элементы управления графиком или WinForms.

Примечание

Алгоритмы, использующие WinForms или WPF, могут запускаться только на компьютерах с Windows.

Чтобы использовать WPF с сиБотами и индикаторами, измените компилятор cTrader со встроенного компилятора на компилятор .NET SDK.

Настройка вашего проекта

Аналогично WinForms, прежде чем вы сможете использовать WPF с вашими сиБотами и индикаторами, вам придется внести некоторые изменения в файл проекта вашего индикатора или сиБота. Поскольку WPF работает только в Windows, вам придется изменить целевую платформу вашего проекта индикатора или сиБота на вариант .NET для Windows.

Для этого откройте файл проекта в Visual Studio и замените его содержимое следующим:

 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>

Мы добавили тег UseWpf и изменили значение TargetFramework на net6.0-windows.

Затем измените значение параметра класса AccessRights на FullAccess. Если это изменение не будет внесено, ваше расширение не будет иметь достаточных прав доступа для работы с WPF.

Пересоберите ваш проект после внесения вышеуказанных изменений.

Создание и отображение окна с использованием WPF

Процесс создания пользовательского окна аналогичен созданию пользовательской WinForm.

После того как вы настроили свой проект, щелкните по нему правой кнопкой мыши, выберите Add, а затем выберите Window (WPF)....

Image title

В открывшемся окне выберите опцию WPF window. Задайте имя окна MainWindow и нажмите кнопку Add. Новое окно WPF появится в обозревателе решений вашего проекта. У него будет два файла: файл .XAML и файл .cs, содержащий код бэкенда.

Откройте файл .XAML и скопируйте в него следующий код, чтобы изменить цвет фона на "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>

Вставьте следующий код в основной исходный файл вашего индикатора:

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

Когда вы добавляете новое окно WPF, оно будет использовать пространство имен вашего индикатора или сиБота (например, cAlgo). В файле .cs, содержащем код бэкенда окна, измените его на WPF_Test и убедитесь, что оно соответствует вашему оператору using в коде индикатора.

Пересоберите индикатор либо из Visual Studio, либо с помощью cTrader Algo.

После завершения процесса сборки создайте экземпляр индикатора. Вы должны увидеть свое пользовательское окно практически сразу после запуска.

Image title

Отобразите торговую панель

В этом примере мы создадим простую торговую панель с помощью WPF. Создайте новый сиБот и задайте ему имя WPF Trading Panel.

Откройте свой новый робот в Visual Studio и добавьте новый ресурс WPF, как описано выше. Задайте окну имя MainWindow.

Image title

После этого откройте файл .XAML окна и вставьте в него следующий код:

 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>

Затем откройте файл бэкенда .cs и вставьте следующий код:

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

Теперь мы готовы запустить наше пользовательское окно с помощью нашего сиБота. Откройте исходный файл сиБота и вставьте код ниже:

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

Пересоберите свой код с помощью Visual Studio и создайте экземпляр вашего сиБота. Запустите его, и должно появиться следующее окно.

Image title

В окне выберите инструмент из списка инструментов и нажмите Execute. Хотя наша торговая панель довольно проста, она работает так, как задумано. Вы можете использовать ее в качестве шаблона для создания сложных элементов управления WPF.

Код в файле .xaml.cs для нашего окна отвечает за отправку инструкций нашему сиБоту. При разработке кода бэкенда для ваших окон следуйте этим рекомендациям:

  • Создайте сервис для управления взаимодействием с сиБотом или индикатором с помощью метода BeginInvokeOnMainThread().
  • Если вы создаете сложные элементы управления WPF, используйте MVVM, IoC и отделяйте компоненты бизнес-логики от пользовательского интерфейса (например, с помощью Prism)

Использование выделенного потока для UI

Окна WinForms и WPF должны работать внутри потока с пометкой STA, иначе они не будут работать. Для получения дополнительной информации обратитесь к официальной документации.

Вы не можете изменить свойство ApartmentState уже запущенного потока. Поскольку основной поток сиБота или индикатора не имеет пометки STA, вы должны использовать новый поток с пометкой STA для WinForms и WPF.

Получите доступ к членам API из потока пользовательского интерфейса

Как мы объяснили ранее, вам необходимо использовать отдельный выделенный поток для запуска ваших элементов управления WPF. Только код, выполняющийся в этом потоке, сможет получить доступ к элементам управления и свойствам окна.

То же самое верно для всех членов API алгоритмов. Поскольку API не является потокобезопасным, вы не можете получить доступ к членам API из потока пользовательского интерфейса. Вместо этого вам необходимо отправить работу с помощью метода BeginInvokeOnMainThread() в коде вашего индикатора или сиБота.

Это может создать сложности, если вы хотите обмениваться данными между вашим индикатором или сиБотом и окном WPF. Чтобы решить эту проблему, вы можете создать прокси-класс для управления взаимодействием между вашим сиБотом или индикатором и потоками, связанными с пользовательским интерфейсом.