跳转至

WPF

Windows Presentation Foundation (WPF) 是 Windows 用户界面 (UI) 解决方案,是 .NET 的一部分。 最初于 2006 年开发,旨在作为 WinForms 的继任者。 有关更多信息,请参阅 其官方文档

WPF 允许创建丰富且复杂的 UI。 它通过使用 XAML(一种由 Microsoft 开发的基于 XML 的语言)将代码与 UI 分离。 cTrader Windows 是一个 WPF 应用程序,您看到的所有 UI 元素都由该解决方案提供支持。

使用 WPF 可以开发近乎像素级完美的功能丰富的 UI。 然而,WPF 的学习曲线较为陡峭。 如果您想创建相对简单的 UI 元素,您可能希望使用 图表控件WinForms

注意

使用 WinForms 或 WPF 的算法只能在 Windows 机器上运行。

要将 WPF 与 cBot 和指标一起使用,请将您的 cTrader 编译器从嵌入式编译器更改为 .NET SDK 编译器。

配置您的项目

WinForms 类似,在将 WPF 与您的 cBot 和指标一起使用之前,您必须对您的指标或 cBot 项目文件进行一些更改。 由于 WPF 仅在 Windows 上运行,您必须将您的指标或 cBot 项目的目标框架更改为 .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 类似。

配置项目后,右键单击它,选择 添加,然后选择 窗口 (WPF)...

Image title

在新打开的窗口中,选择 WPF 窗口 选项。 将窗口名称设置为 MainWindow,然后单击 添加 按钮。 新的 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 窗口时,它将使用您的指标或 cBot 的命名空间(例如 cAlgo)。 在包含窗口后端代码的 .cs 文件中,将其更改为 WPF_Test,并确保它与指标代码中的 using 语句匹配。

从 Visual Studio 或使用 cTrader Algo 重新构建指标。

构建过程完成后,创建一个指标实例。 您应该会在启动时几乎立即看到您的自定义窗口出现。

Image title

显示交易面板

在此示例中,我们将使用 WPF 创建一个简单的交易面板。 创建一个新的 cBot,并将其名称设置为 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());
            });
        }
    }
}

我们现在准备使用我们的 cBot 启动自定义窗口。 打开 cBot 源文件并插入以下代码:

 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 重新构建您的代码,并创建您的 cBot 实例。 运行它,以下窗口应该会出现。

Image title

在窗口中,从交易品种列表中选择一个交易品种,然后单击 执行。 虽然我们的交易面板有点简单,但它按预期工作。 您可以将其用作创建复杂 WPF 控件的模板。

我们窗口的 .xaml.cs 文件中的代码负责向我们的 cBot 发送指令。 在为您的窗口开发后端代码时,请遵循以下准则:

  • 使用 BeginInvokeOnMainThread() 方法创建一个服务来管理与 cBot 或指标的交互。
  • 如果您正在构建复杂的 WPF 控件,请使用 MVVMIoC 并将您的业务逻辑组件与 UI 分离(例如,通过使用 Prism

为 UI 使用专用线程

WinForms 和 WPF 窗口必须在 STA 标记的线程 内运行,否则它们将无法工作。 有关更多信息,请参阅 官方文档

您无法更改已运行线程的 ApartmentState 属性。 由于主 cBot 或指标线程未标记为 STA,您必须为 WinForms 和 WPF 使用新的 STA 标记线程。

从 UI 线程访问 API 成员

正如我们之前解释的那样,您必须使用一个单独的专用线程来运行您的 WPF 控件。 只有在此线程上执行的代码才能访问窗口控件和属性。

对于所有算法 API 成员也是如此。 由于 API 不是线程安全的,您无法从 UI 线程访问 API 成员。 相反,您必须通过在您的指标或 cBot 代码中使用 BeginInvokeOnMainThread() 方法来分派工作。

如果您希望在指标或 cBot 与 WPF 窗口之间交换数据,这可能会带来复杂性。 为了解决这个问题,您可以创建一个代理类来管理您的 cBot 或指标与 UI 相关线程之间的交互。