WPF

紹介

Windows Presentation Foundation (WPF) は、.NET の一部として提供されるウィンドウ UI ソリューションです。2006年に最初に開発され、WinFormsの後継として意図されていました。詳細については、公式ドキュメントを参照してください。

WPF は、リッチで洗練された UI を作成することを可能にします。Microsoft が開発した XML ベースの言語である XAML を使用して、コードと UI を分離します。cTrader デスクトップは WPF アプリケーションであり、すべての UI 要素はこのソリューションによって提供されています。

WPF を使用することで、ほぼピクセル完璧な機能豊富な UI を開発できます。ただし、WPF には急な学習曲線があります。比較的シンプルな UI 要素を作成したい場合は、チャートコントロールWinFormsの使用を検討するかもしれません。

注意

WinForms または WPF を使用しているアルゴは、Windows マシンでのみ実行できます。

WPF を cBots/インジケーターで使用するには、cTrader コンパイラを変更する必要があります。埋め込まれているコンパイラから .NET SDK コンパイラに変更してください。

プロジェクトの構成方法

WinForms と同様に、WPF を cBots/インジケーターで使用するには、インジケーター/cBot プロジェクトファイルにいくつかの変更を加える必要があります。WPF は Windows でのみ動作するため、プロジェクトのインジケーター/cBot フレームワークのターゲットを Windows 向けの .NET に変更する必要があります。

これを行うには、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)…」 を選択します。

画像タイトル

新しく開いたウィンドウで、「WPF ウィンドウ」 オプションを選択します。ウィンドウの名前をMainWindow と設定し、「追加」 ボタンをクリックします。

新しい WPF ウィンドウがプロジェクトのソリューションエクスプローラーに表示されます。このウィンドウには、.XAML ファイルと、バックエンドコードを含む .cs ファイルの 2 つのファイルが含まれます。

.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>

Paste the following code into your indicator main source file.

 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 を使用してインジケーターを再ビルドします。

ビルドプロセスが終了した後、インジケーターのインスタンスを作成します。起動後すぐにカスタムウィンドウが表示されるはずです。

画像タイトル

トレーディングパネルの表示方法

この例では、WPF を使用してシンプルなトレーディングパネルを作成します。新しい cBot を作成し、その名前を WPF Trading Panel に設定します。

Visual Studio で新しいロボットを開き、上記の手順に従って新しい WPF リソースを追加します。ウィンドウの名前を MainWindow に設定します。

画像タイトル

その後、ウィンドウの .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

ウィンドウ内で、シンボルリストからシンボルを選択し、「Execute」をクリックします。私たちのトレーディングパネルは少しシンプルですが、意図した通りに機能します。これをテンプレートとして使用して、複雑な WPF コントロールを作成できます。

ウィンドウの .xaml.cs ファイル内のコードは、cBot への指示を送信する役割を果たします。ウィンドウのバックエンドコードを開発する際は、以下のガイドラインに従ってください。

  • cBot/indicator との対話を管理するサービスを作成し、BeginInvokeOnMainThread() メソッドを使用します。
  • 複雑な WPF コントロールを構築する場合は、MVVMIoC を使用し、ビジネスロジックのコンポーネントを UI から分離します(例:Prism を使用するなど)。

UI のための専用スレッドの使用方法

WinForms および WPF ウィンドウは STA マーク付きスレッド 内で実行する必要があります。そうでないと機能しません。詳細については、公式ドキュメント を参照してください。

すでに実行中のスレッドの ApartmentState プロパティを変更することはできません。メインの cBot/indicator スレッドは STA マークが付いていないため、WinForms および WPF には新しい STA マーク付きスレッドを使用する必要があります。

UI スレッドから API メンバーにアクセスする方法

前述のように、WPF コントロールを実行するには専用のスレッドを使用する必要があります。このスレッドで実行されるコードのみがウィンドウコントロールやプロパティにアクセスできます。

すべての algo API メンバーにも同様のことが言えます。API は スレッドセーフではない ため、UI スレッドから API メンバーにアクセスすることはできません。代わりに、BeginInvokeOnMainThread() メソッドを使用して作業をディスパッチする必要があります。

このため、cBot/indicator と WPF ウィンドウ間でデータを交換する際に問題が生じる可能性があります。この問題を解決するには、cBot/indicator と UI 関連のスレッド間の対話を管理する「プロキシ」クラスを作成することをお勧めします。

目次

このページについて