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 กับ cBots และอินดิเคเตอร์ เปลี่ยนคอมไพเลอร์ cTrader ของคุณ จากคอมไพเลอร์แบบฝังตัวเป็นคอมไพเลอร์ .NET SDK
กำหนดค่าโปรเจกต์ของคุณ
เช่นเดียวกับ WinForms ก่อนที่คุณจะสามารถใช้ WPF กับ cBots และอินดิเคเตอร์ของคุณได้ คุณจะต้องทำการเปลี่ยนแปลงบางอย่างกับไฟล์โปรเจกต์อินดิเคเตอร์หรือ 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 ที่มีโค้ด backend
เปิดไฟล์ .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 ที่มีโค้ด backend ของหน้าต่าง ให้เปลี่ยนเป็น 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 ของ backend และวางโค้ดต่อไปนี้:
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 ของเรา เมื่อพัฒนาโค้ด backend สำหรับหน้าต่างของคุณ ให้ปฏิบัติตามแนวทางเหล่านี้:
- สร้างบริการสำหรับจัดการการโต้ตอบกับ cBot หรืออินดิเคเตอร์โดยใช้เมธอด
BeginInvokeOnMainThread() - หากคุณกำลังสร้างตัวควบคุม WPF ที่ซับซ้อน ให้ใช้ MVVM IoC และแยกส่วนประกอบตรรกะทางธุรกิจของคุณออกจาก UI (ตัวอย่างเช่น โดยใช้ Prism)
ใช้เธรดเฉพาะสำหรับ UI
หน้าต่าง WinForms และ WPF ต้องทำงานภายใน เธรดที่ทำเครื่องหมาย STA มิฉะนั้นจะไม่ทำงาน สำหรับข้อมูลเพิ่มเติม โปรดดูที่ เอกสารอย่างเป็นทางการ
คุณไม่สามารถเปลี่ยนคุณสมบัติ ApartmentState ของเธรดที่กำลังทำงานอยู่ได้ เนื่องจากเธรดหลักของ cBot หรืออินดิเคเตอร์ไม่ได้ทำเครื่องหมาย STA คุณต้องใช้เธรดใหม่ที่ทำเครื่องหมาย STA สำหรับ WinForms และ WPF
เข้าถึงสมาชิก API จากเธรด UI
ตามที่เราได้อธิบายไปก่อนหน้านี้ คุณต้องใช้เธรดเฉพาะแยกต่างหากเพื่อรันตัวควบคุม WPF ของคุณ เฉพาะโค้ดที่ทำงานบนเธรดนี้เท่านั้นที่จะสามารถเข้าถึงตัวควบคุมและคุณสมบัติของหน้าต่างได้
สิ่งเดียวกันนี้เป็นจริงสำหรับสมาชิก API ทั้งหมด เนื่องจาก API ไม่ปลอดภัยต่อเธรด คุณไม่สามารถเข้าถึงสมาชิก API จากเธรด UI ได้ แทนที่จะทำเช่นนั้น คุณต้องส่งงานโดยใช้เมธอด BeginInvokeOnMainThread() ในโค้ดอินดิเคเตอร์หรือ cBot ของคุณ
สิ่งนี้อาจสร้างความยุ่งยากหากคุณต้องการแลกเปลี่ยนข้อมูลระหว่างอินดิเคเตอร์หรือ cBot และหน้าต่าง WPF เพื่อแก้ไขปัญหานี้ คุณอาจสร้างคลาสพร็อกซีเพื่อจัดการการโต้ตอบระหว่าง cBot หรืออินดิเคเตอร์และเธรดที่เกี่ยวข้องกับ UI ของคุณ