WPF
Windows Presentation Foundation(WPF)は、.NETの一部であるWindowsユーザーインターフェース(UI)ソリューションです。 2006年に開発され、WinFormsの後継として意図されました。 詳細については、公式ドキュメントを参照してください。
WPFは豊かで洗練されたUIの作成を可能にします。 MicrosoftによってXMLベースの言語として開発されたXAMLを使用して、コードとUIを分離します。 cTrader WindowsはWPFアプリケーションであり、表示されているすべてのUI要素はこのソリューションによって動作しています。
WPFの使用により、ほぼピクセルパーフェクトで機能豊富なUIの開発が可能になります。 しかし、WPFには急な学習曲線があります。 比較的シンプルなUI要素を作成したい場合は、チャートコントロールまたはWinFormsの使用を検討することをお勧めします。
注意
WinFormsまたはWPFを使用するアルゴリズムは、Windowsマシンでのみ実行できます。
cBotとインジケーターでWPFを使用するには、cTraderコンパイラを埋め込みコンパイラから.NET SDKコンパイラに変更してください。
プロジェクトの設定
WinFormsと同様に、cBotとインジケーターでWPFを使用する前に、インジケーターまたはcBotのプロジェクトファイルにいくつかの変更を加える必要があります。 WPFはWindowsでのみ動作するため、インジケーターまたはcBotプロジェクトのターゲットフレームワークをWindowsバリアントの.NETに変更する必要があります。
これを行うには、Visual Studioでプロジェクトファイルを開き、その内容を以下のように置き換えます。
| <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" に変更します。
| <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を使用してインジケーターを再ビルドします。
ビルドプロセスが完了したら、インジケーターのインスタンスを作成します。 起動後すぐにカスタムウィンドウが表示されるはずです。

トレーディングパネルを表示する
この例では、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のインスタンスを作成します。 実行すると、以下のウィンドウが表示されるはずです。

ウィンドウで、通貨ペアリストから通貨ペアを選択し、Executeをクリックします。 トレーディングパネルは少し単純ですが、意図したとおりに機能します。 これを複雑なWPFコントロールを作成するためのテンプレートとして使用できます。
ウィンドウの.xaml.csファイル内のコードは、cBotに指示を送信する役割を果たします。 ウィンドウのバックエンドコードを開発する際は、以下のガイドラインに従ってください:
BeginInvokeOnMainThread()メソッドを使用して、cBotまたはインジケーターとの対話を管理するサービスを作成します。 - 複雑なWPFコントロールを構築する場合は、MVVM、IoCを使用し、ビジネスロジックコンポーネントをUIから分離します(例えば、Prismを使用)。
UIに専用スレッドを使用する
WinFormsとWPFウィンドウはSTAマークされたスレッド内で実行する必要があります。そうしないと機能しません。 詳細については、公式ドキュメントを参照してください。
既に実行中のスレッドの ApartmentState プロパティを変更することはできません。 メインのcBotまたはインジケータースレッドはSTAマークされていないため、WinFormsとWPFには新しいSTAマークされたスレッドを使用する必要があります。
UIスレッドからAPIメンバーにアクセスする
前述のとおり、WPFコントロールを実行するには専用の別スレッドを使用する必要があります。 このスレッドで実行されるコードのみがウィンドウのコントロールとプロパティにアクセスできます。
すべてのalgo APIメンバーについても同様です。 APIはスレッドセーフではないため、UIスレッドからAPIメンバーにアクセスすることはできません。 代わりに、インジケーターまたはcBotコードでBeginInvokeOnMainThread()メソッドを使用して作業をディスパッチする必要があります。
これにより、インジケーターまたはcBotとWPFウィンドウ間でデータを交換したい場合に複雑になる可能性があります。 この問題を解決するために、cBotまたはインジケーターとUI関連スレッド間の対話を管理するプロキシクラスを作成することができます。