Bỏ qua

WPF

Windows Presentation Foundation (WPF) là một giải pháp giao diện người dùng (UI) Windows là một phần của .NET. Ban đầu được phát triển vào năm 2006, nó được dự định là người kế nhiệm của WinForms. Để biết thêm thông tin, hãy tham khảo tài liệu chính thức của nó.

WPF cho phép tạo ra UI phong phú và tinh vi. Nó tách biệt mã khỏi UI bằng cách sử dụng XAML, một ngôn ngữ dựa trên XML được phát triển bởi Microsoft. cTrader Windows là một ứng dụng WPF và tất cả các phần tử UI bạn đang thấy đều được hỗ trợ bởi giải pháp này.

Việc sử dụng WPF cho phép phát triển UI giàu tính năng gần như hoàn hảo đến từng pixel. Tuy nhiên, WPF có đường cong học tập dốc. Nếu bạn muốn tạo các phần tử UI tương đối đơn giản, bạn có thể muốn sử dụng điều khiển biểu đồ hoặc WinForms.

Ghi chú

Các thuật toán sử dụng WinForms hoặc WPF chỉ có thể chạy trên máy Windows.

Để sử dụng WPF với cBot và chỉ báo, thay đổi trình biên dịch cTrader của bạn từ trình biên dịch tích hợp sang trình biên dịch .NET SDK.

Cấu hình dự án của bạn

Tương tự như WinForms, trước khi bạn có thể sử dụng WPF với cBot và chỉ báo của mình, bạn sẽ phải thực hiện một số thay đổi đối với tệp dự án chỉ báo hoặc cBot của bạn. Vì WPF chỉ hoạt động trên Windows, bạn sẽ phải thay đổi khung mục tiêu của dự án chỉ báo hoặc cBot của bạn thành biến thể Windows của .NET.

Để làm điều này, mở tệp dự án của bạn trong Visual Studio và thay thế nội dung của nó bằng những điều sau:

 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>

Chúng tôi đã thêm thẻ UseWpf và thay đổi giá trị của TargetFramework thành net6.0-windows.

Sau đó, thay đổi giá trị của tham số lớp AccessRights thành FullAccess. Trừ khi thay đổi này được thực hiện, tiện ích mở rộng của bạn sẽ không có đủ quyền truy cập để hoạt động với WPF.

Xây dựng lại dự án của bạn sau khi thực hiện các thay đổi trên.

Tạo và hiển thị cửa sổ bằng WPF

Quá trình tạo cửa sổ tùy chỉnh tương tự như tạo WinForm tùy chỉnh.

Sau khi bạn đã cấu hình dự án của mình, nhấp chuột phải vào nó, chọn Add và sau đó chọn Window (WPF)....

Image title

Trong cửa sổ mới mở, chọn tùy chọn WPF window. Đặt tên cửa sổ là MainWindow và nhấp vào nút Add. Cửa sổ WPF mới sẽ xuất hiện trong trình khám phá giải pháp dự án của bạn. Nó sẽ có hai tệp, một tệp .XAML và một tệp .cs chứa mã backend.

Mở tệp .XAML và sao chép và dán mã sau vào đó để thay đổi màu nền thành "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>

Dán mã sau vào tệp nguồn chính của chỉ báo của bạn:

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

Khi bạn thêm một cửa sổ WPF mới, nó sẽ sử dụng không gian tên của chỉ báo hoặc cBot của bạn (như cAlgo). Trong tệp .cs chứa mã backend của cửa sổ, thay đổi nó thành WPF_Test và đảm bảo nó khớp với câu lệnh using trong mã chỉ báo của bạn.

Xây dựng lại chỉ báo từ Visual Studio hoặc sử dụng cTrader Algo.

Sau khi quá trình xây dựng hoàn tất, tạo một phiên bản chỉ báo. Bạn sẽ thấy cửa sổ tùy chỉnh của mình xuất hiện gần như ngay lập tức khi khởi chạy.

Image title

Hiển thị bảng giao dịch

Trong ví dụ này, chúng ta sẽ tạo một bảng giao dịch đơn giản bằng WPF. Tạo một cBot mới và đặt tên là WPF Trading Panel.

Mở robot mới của bạn trong Visual Studio và thêm một tài nguyên WPF mới như đã nêu ở trên. Đặt tên cửa sổ là MainWindow.

Image title

Sau đó, mở tệp .XAML của cửa sổ và dán mã sau vào đó:

 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>

Tiếp theo, mở tệp .cs backend và dán mã sau:

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

Bây giờ chúng ta đã sẵn sàng để khởi chạy cửa sổ tùy chỉnh của mình bằng cBot của chúng ta. Mở tệp nguồn cBot và chèn mã bên dưới:

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

Xây dựng lại mã của bạn bằng Visual Studio và tạo một phiên bản cBot của bạn. Chạy nó và cửa sổ sau sẽ xuất hiện.

Image title

Trong cửa sổ, chọn một ký hiệu từ danh sách ký hiệu và nhấp vào Execute. Mặc dù bảng giao dịch của chúng ta hơi đơn giản, nhưng nó hoạt động như dự định. Bạn có thể sử dụng nó làm mẫu để tạo các điều khiển WPF phức tạp.

Mã trong tệp .xaml.cs cho cửa sổ của chúng ta chịu trách nhiệm gửi hướng dẫn đến cBot của chúng ta. Khi phát triển mã backend cho cửa sổ của bạn, hãy tuân theo các hướng dẫn sau:

  • Tạo một dịch vụ để quản lý tương tác với cBot hoặc chỉ báo bằng phương thức BeginInvokeOnMainThread().
  • Nếu bạn đang xây dựng các điều khiển WPF phức tạp, hãy sử dụng MVVM, IoC và tách các thành phần logic kinh doanh của bạn khỏi UI (ví dụ: bằng cách sử dụng Prism)

Sử dụng một luồng chuyên dụng cho giao diện người dùng

Cửa sổ WinForms và WPF phải chạy bên trong một luồng được đánh dấu STA, nếu không chúng sẽ không hoạt động. Để biết thêm thông tin, hãy tham khảo tài liệu chính thức.

Bạn không thể thay đổi thuộc tính ApartmentState của một luồng đang chạy. Vì luồng chính của cBot hoặc chỉ báo không được đánh dấu STA, bạn phải sử dụng một luồng mới được đánh dấu STA cho WinForms và WPF.

Truy cập các thành viên API từ luồng UI

Như chúng tôi đã giải thích trước đó, bạn phải sử dụng một luồng chuyên dụng riêng biệt để chạy các điều khiển WPF của mình. Chỉ có mã thực thi trên luồng này mới có thể truy cập các điều khiển và thuộc tính của cửa sổ.

Điều tương tự cũng đúng với tất cả các thành viên API thuật toán. Vì API không an toàn cho luồng, bạn không thể truy cập các thành viên API từ luồng UI. Thay vào đó, bạn phải gửi công việc bằng cách sử dụng phương thức BeginInvokeOnMainThread() trong mã chỉ báo hoặc cBot của bạn.

Điều này có thể tạo ra các phức tạp nếu bạn muốn trao đổi dữ liệu giữa chỉ báo hoặc cBot của bạn và một cửa sổ WPF. Để giải quyết vấn đề này, bạn có thể tạo một lớp proxy để quản lý tương tác giữa cBot hoặc chỉ báo của bạn và các luồng liên quan đến UI.