Langkau tajuk talian

WPF

Windows Presentation Foundation (WPF) ialah penyelesaian antara muka pengguna (UI) Windows yang merupakan sebahagian daripada .NET. Asalnya dibangunkan pada tahun 2006, ia bertujuan sebagai pengganti kepada WinForms. Untuk maklumat tambahan, rujuk dokumentasi rasminya.

WPF membolehkan penciptaan UI yang kaya dan canggih. Ia memisahkan kod daripada UI dengan menggunakan XAML, bahasa berasaskan XML yang dibangunkan oleh Microsoft. cTrader Windows ialah aplikasi WPF dan semua elemen UI yang anda lihat dikuasakan oleh penyelesaian ini.

Penggunaan WPF membolehkan pembangunan UI yang kaya dengan ciri yang hampir sempurna piksel. Walau bagaimanapun, WPF mempunyai keluk pembelajaran yang curam. Jika anda ingin mencipta elemen UI yang agak mudah, anda mungkin ingin menggunakan kawalan carta atau WinForms.

Nota

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

Untuk menggunakan WPF dengan cBot dan indikator, tukar penyusun cTrader anda daripada penyusun terbenam kepada penyusun .NET SDK.

Konfigurasi projek anda

Sama seperti WinForms, sebelum anda boleh menggunakan WPF dengan cBot dan indikator anda, anda perlu membuat beberapa perubahan pada fail projek indikator atau cBot anda. Oleh kerana WPF hanya berfungsi pada Windows, anda perlu menukar rangka kerja sasaran projek indikator atau cBot anda kepada varian Windows .NET.

Untuk berbuat demikian, buka fail projek anda dalam Visual Studio dan gantikan kandungannya 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 menambah tag UseWpf dan menukar nilai TargetFramework kepada net6.0-windows.

Selepas itu, tukar nilai parameter kelas AccessRights kepada FullAccess. Melainkan perubahan ini dibuat, sambungan anda tidak akan mempunyai hak akses yang mencukupi untuk beroperasi dengan WPF.

Bina semula projek anda selepas membuat perubahan di atas.

Cipta dan tunjukkan tetingkap menggunakan WPF

Proses mencipta tetingkap tersuai adalah serupa dengan mencipta WinForm tersuai.

Selepas anda mengkonfigurasi projek anda, klik kanan padanya, pilih Add dan kemudian pilih Window (WPF)....

Image title

Dalam tetingkap yang baru dibuka, pilih pilihan WPF window. Tetapkan nama tetingkap kepada MainWindow dan klik butang Add. Tetingkap WPF baharu akan muncul dalam penjelajah penyelesaian projek anda. Ia akan mempunyai dua fail, fail .XAML dan fail .cs yang mengandungi kod backend.

Buka fail .XAML dan salin dan tampal kod berikut ke dalamnya untuk menukar warna latar belakangnya kepada "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>

Tampal kod berikut ke dalam fail 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)
        {
        }
    }
}

Apabila anda menambah tetingkap WPF baharu, ia akan menggunakan ruang nama indikator atau cBot anda (seperti cAlgo). Dalam fail .cs yang mengandungi kod backend tetingkap, tukar ia kepada WPF_Test dan pastikan ia sepadan dengan pernyataan using anda dalam kod indikator.

Bina semula indikator sama ada dari Visual Studio atau menggunakan cTrader Algo.

Selepas proses pembinaan selesai, cipta contoh indikator. Anda sepatutnya melihat tetingkap tersuai anda muncul hampir serta-merta selepas pelancaran.

Image title

Paparkan panel dagangan

Dalam contoh ini, kita akan mencipta panel dagangan mudah menggunakan WPF. Cipta cBot baharu dan tetapkan namanya kepada WPF Trading Panel.

Buka robot baharu anda dalam Visual Studio, dan tambah sumber WPF baharu seperti yang digariskan di atas. Tetapkan nama tetingkap kepada MainWindow.

Image title

Selepas itu, buka fail .XAML tetingkap dan tampal kod 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>

Selepas itu, buka fail .cs backend dan tampal kod 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 kini bersedia untuk melancarkan tetingkap tersuai kita menggunakan cBot kita. Buka fail sumber cBot dan masukkan kod di bawah:

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

Bina semula kod anda menggunakan Visual Studio dan cipta contoh cBot anda. Jalankannya dan tetingkap berikut sepatutnya muncul.

Image title

Dalam tetingkap, pilih simbol daripada senarai simbol dan klik Execute. Walaupun panel dagangan kita agak mudah, ia berfungsi seperti yang dimaksudkan. Anda boleh menggunakannya sebagai templat untuk mencipta kawalan WPF yang kompleks.

Kod dalam fail .xaml.cs untuk tetingkap kita bertanggungjawab untuk menghantar arahan kepada cBot kita. Apabila membangunkan kod backend untuk tetingkap anda, ikuti garis panduan ini:

  • Cipta perkhidmatan untuk menguruskan interaksi dengan cBot atau indikator menggunakan kaedah BeginInvokeOnMainThread().
  • Jika anda membina kawalan WPF yang kompleks, gunakan MVVM, IoC dan asingkan komponen logik perniagaan anda daripada UI (contohnya, dengan menggunakan Prism)

Gunakan benang khusus untuk UI

Tetingkap WinForms dan WPF mesti dijalankan dalam bebenang bertanda STA, jika tidak, ia tidak akan berfungsi. Untuk maklumat lanjut, rujuk dokumentasi rasmi.

Anda tidak boleh menukar sifat ApartmentState benang yang sudah berjalan. Oleh kerana benang utama cBot atau indikator tidak ditandakan STA, anda perlu menggunakan benang baharu yang ditandakan STA untuk WinForms dan WPF.

Akses ahli API daripada bebenang UI

Seperti yang telah kami jelaskan sebelum ini, anda perlu menggunakan bebenang khusus yang berasingan untuk menjalankan kawalan WPF anda. Hanya kod yang dilaksanakan pada bebenang ini akan dapat mengakses kawalan dan sifat tetingkap.

Perkara yang sama juga berlaku untuk semua ahli API algo. Oleh kerana API tidak selamat bebenang, anda tidak boleh mengakses ahli API daripada bebenang UI. Sebaliknya, anda perlu menghantar kerja dengan menggunakan kaedah BeginInvokeOnMainThread() dalam kod indikator atau cBot anda.

Ini mungkin menimbulkan komplikasi jika anda ingin bertukar data antara indikator atau cBot anda dan tetingkap WPF. Untuk menyelesaikan isu ini, anda boleh mencipta kelas proksi untuk menguruskan interaksi antara cBot atau indikator anda dan bebenang berkaitan UI.