Lewati ke isi

WPF

Windows Presentation Foundation (WPF) adalah solusi antarmuka pengguna (UI) Windows yang merupakan bagian dari .NET. Awalnya dikembangkan pada tahun 2006, dimaksudkan sebagai penerus WinForms. Untuk informasi tambahan, lihat dokumentasi resminya.

WPF memungkinkan pembuatan UI yang kaya dan canggih. Ini memisahkan kode dari UI dengan menggunakan XAML, bahasa berbasis XML yang dikembangkan oleh Microsoft. cTrader Windows adalah aplikasi WPF dan semua elemen UI yang Anda lihat didukung oleh solusi ini.

Penggunaan WPF memungkinkan pengembangan UI yang kaya fitur dan hampir sempurna piksel. Namun, WPF memiliki kurva pembelajaran yang curam. Jika Anda ingin membuat elemen UI yang relatif sederhana, Anda mungkin ingin menggunakan kontrol grafik atau WinForms.

Catatan

Algo yang menggunakan WinForms atau WPF hanya dapat dijalankan pada mesin Windows.

Untuk menggunakan WPF dengan cBot dan indikator, ubah kompilator cTrader Anda dari kompilator tersemat ke kompilator .NET SDK.

Mengonfigurasi proyek Anda

Mirip dengan WinForms, sebelum Anda dapat menggunakan WPF dengan cBot dan indikator Anda, Anda harus membuat beberapa perubahan pada file proyek indikator atau cBot Anda. Karena WPF hanya berfungsi di Windows, Anda harus mengubah target framework proyek indikator atau cBot Anda ke varian Windows dari .NET.

Untuk melakukannya, buka file proyek Anda di Visual Studio dan ganti isinya dengan yang berikut:

 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>

Kami telah menambahkan tag UseWpf dan mengubah nilai TargetFramework menjadi net6.0-windows.

Setelah itu, ubah nilai parameter kelas AccessRights menjadi FullAccess. Kecuali perubahan ini dilakukan, ekstensi Anda tidak akan memiliki hak akses yang cukup untuk beroperasi dengan WPF.

Bangun ulang proyek Anda setelah membuat perubahan di atas.

Buat dan tampilkan jendela menggunakan WPF

Proses pembuatan jendela kustom mirip dengan membuat WinForm kustom.

Setelah Anda mengonfigurasi proyek Anda, klik kanan proyek tersebut, pilih Add dan kemudian pilih Window (WPF)....

Image title

Di jendela yang baru terbuka, pilih opsi WPF window. Atur nama jendela menjadi MainWindow dan klik tombol Add. Jendela WPF baru akan muncul di explorer solusi proyek Anda. Ini akan memiliki dua file, file .XAML dan file .cs yang berisi kode backend.

Buka file .XAML dan salin dan tempel kode berikut ke dalamnya untuk mengubah warna latar belakangnya menjadi "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>

Tempel kode berikut ke dalam file sumber utama indikator Anda:

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

Ketika Anda menambahkan jendela WPF baru, itu akan menggunakan namespace indikator atau cBot Anda (seperti cAlgo). Di file .cs yang berisi kode backend jendela, ubah menjadi WPF_Test dan pastikan cocok dengan pernyataan using Anda dalam kode indikator.

Bangun ulang indikator baik dari Visual Studio atau menggunakan cTrader Algo.

Setelah proses build selesai, buat instance indikator. Anda seharusnya melihat jendela kustom Anda muncul hampir segera setelah diluncurkan.

Image title

Menampilkan panel trading

Dalam contoh ini, kita akan membuat panel trading sederhana menggunakan WPF. Buat cBot baru dan atur namanya menjadi WPF Trading Panel.

Buka robot baru Anda di Visual Studio, dan tambahkan sumber daya WPF baru seperti yang diuraikan di atas. Atur nama jendela menjadi MainWindow.

Image title

Setelah itu, buka file .XAML jendela dan tempelkan kode berikut ke dalamnya:

 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>

Selanjutnya, buka file backend .cs dan tempelkan kode berikut:

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

Kita sekarang siap untuk meluncurkan jendela kustom kita menggunakan cBot kita. Buka file sumber cBot dan masukkan kode di bawah ini:

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

Bangun ulang kode Anda menggunakan Visual Studio dan buat instance cBot Anda. Jalankan dan jendela berikut seharusnya muncul.

Image title

Di jendela, pilih simbol dari daftar simbol dan klik Execute. Meskipun panel trading kita agak sederhana, ia berfungsi sebagaimana dimaksud. Anda dapat menggunakannya sebagai template untuk membuat kontrol WPF yang kompleks.

Kode di file .xaml.cs untuk jendela kita bertanggung jawab untuk mengirim instruksi ke cBot kita. Saat mengembangkan kode backend untuk jendela Anda, ikuti pedoman berikut:

  • Buat layanan untuk mengelola interaksi dengan cBot atau indikator menggunakan metode BeginInvokeOnMainThread().
  • Jika Anda membangun kontrol WPF yang kompleks, gunakan MVVM, IoC dan pisahkan komponen logika bisnis Anda dari UI (misalnya, dengan menggunakan Prism)

Gunakan thread khusus untuk UI

Jendela WinForms dan WPF harus berjalan di dalam thread yang ditandai STA, jika tidak, mereka tidak akan berfungsi. Untuk informasi lebih lanjut, lihat dokumentasi resmi.

Anda tidak dapat mengubah properti ApartmentState dari thread yang sudah berjalan. Karena thread utama cBot atau indikator tidak ditandai STA, Anda harus menggunakan thread baru yang ditandai STA untuk WinForms dan WPF.

Akses anggota API dari thread UI

Seperti yang telah kami jelaskan sebelumnya, Anda harus menggunakan thread terpisah yang didedikasikan untuk menjalankan kontrol WPF Anda. Hanya kode yang dieksekusi pada thread ini yang akan dapat mengakses kontrol dan properti jendela.

Hal yang sama berlaku untuk semua anggota algo API. Karena API tidak thread safe, Anda tidak dapat mengakses anggota API dari thread UI. Sebaliknya, Anda harus mengirim pekerjaan dengan menggunakan metode BeginInvokeOnMainThread() dalam kode indikator atau cBot Anda.

Ini mungkin menciptakan komplikasi jika Anda ingin bertukar data antara indikator atau cBot Anda dan jendela WPF. Untuk mengatasi masalah ini, Anda dapat membuat kelas proxy untuk mengelola interaksi antara cBot atau indikator Anda dan thread terkait UI.