Saltar a contenido

WPF

Windows Presentation Foundation (WPF) es una solución de interfaz de usuario (UI) de Windows que forma parte de .NET. Desarrollada originalmente en 2006, estaba destinada a ser la sucesora de WinForms. Para obtener información adicional, consulte su documentación oficial.

WPF permite crear interfaces de usuario ricas y sofisticadas. Separa el código de la interfaz de usuario utilizando XAML, un lenguaje basado en XML desarrollado por Microsoft. cTrader Windows es una aplicación WPF y todos los elementos de la interfaz de usuario que ve están impulsados por esta solución.

El uso de WPF permite desarrollar interfaces de usuario ricas en funciones y casi perfectas a nivel de píxel. Sin embargo, WPF tiene una curva de aprendizaje pronunciada. Si desea crear elementos de interfaz de usuario relativamente simples, es posible que desee utilizar controles de gráficos o WinForms.

Nota

Los algoritmos que usan WinForms o WPF solo se pueden ejecutar en máquinas Windows.

Para utilizar WPF con cBots e indicadores, cambie su compilador de cTrader del compilador incorporado al compilador del SDK de .NET.

Configurar su proyecto

De manera similar a WinForms, antes de poder utilizar WPF con sus cBots e indicadores, tendrá que realizar algunos cambios en el archivo de proyecto de su indicador o cBot. Como WPF solo funciona en Windows, tendrá que cambiar el marco de destino de su proyecto de indicador o cBot a la variante de Windows de .NET.

Para hacerlo, abra el archivo de su proyecto en Visual Studio y reemplace su contenido con lo siguiente:

 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>

Hemos agregado la etiqueta UseWpf y cambiado el valor de TargetFramework a net6.0-windows.

Después, cambie el valor del parámetro de clase AccessRights a FullAccess. A menos que se realice este cambio, su extensión no tendrá suficientes derechos de acceso para operar con WPF.

Reconstruya su proyecto después de hacer los cambios anteriores.

Crear y mostrar una ventana usando WPF

El proceso de crear una ventana personalizada es similar a crear un WinForm personalizado.

Después de haber configurado su proyecto, haga clic con el botón derecho en él, seleccione Agregar y luego elija Ventana (WPF)....

Image title

En la ventana recién abierta, seleccione la opción Ventana WPF. Establezca el nombre de la ventana como MainWindow y haga clic en el botón Agregar. La nueva ventana WPF aparecerá en el explorador de soluciones de su proyecto. Tendrá dos archivos, un archivo .XAML y un archivo .cs que contiene el código backend.

Abra el archivo .XAML y copie y pegue el siguiente código en él para cambiar su color de fondo a "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>

Pegue el siguiente código en el archivo de fuente principal de su indicador:

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

Cuando agrega una nueva ventana WPF, utilizará el espacio de nombres de su indicador o cBot (como cAlgo). En el archivo .cs que contiene el código backend de la ventana, cámbielo a WPF_Test y asegúrese de que coincida con su declaración using en el código del indicador.

Reconstruya el indicador ya sea desde Visual Studio o usando cTrader Algo.

Después de que finalice el proceso de compilación, cree una instancia del indicador. Debería ver aparecer su ventana personalizada casi inmediatamente después del lanzamiento.

Image title

Mostrar un panel de operaciones

En este ejemplo, crearemos un panel de operaciones simple utilizando WPF. Cree un nuevo cBot y establezca su nombre como WPF Trading Panel.

Abra su nuevo robot en Visual Studio y agregue un nuevo recurso WPF como se describe arriba. Establezca el nombre de la ventana como MainWindow.

Image title

Después, abra el archivo .XAML de la ventana y pegue el siguiente código en él:

 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>

Posteriormente, abra el archivo .cs backend y pegue el siguiente código:

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

Ahora estamos listos para lanzar nuestra ventana personalizada usando nuestro cBot. Abra el archivo fuente del cBot e inserte el código a continuació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
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();
        }
    }
}

Reconstruya su código usando Visual Studio y cree una instancia de su cBot. Ejecútelo y debería aparecer la siguiente ventana.

Image title

En la ventana, seleccione un símbolo de la lista de símbolos y haga clic en Ejecutar. Aunque nuestro panel de operaciones es un poco simple, funciona como se pretende. Puede usarlo como plantilla para crear controles WPF complejos.

El código en el archivo .xaml.cs de nuestra ventana es responsable de enviar instrucciones a nuestro cBot. Al desarrollar código backend para sus ventanas, siga estas pautas:

  • Cree un servicio para gestionar las interacciones con un cBot o indicador utilizando el método BeginInvokeOnMainThread().
  • Si está construyendo controles WPF complejos, utilice MVVM, IoC y separe sus componentes de lógica de negocio de la interfaz de usuario (por ejemplo, utilizando Prism)

Utilizar un hilo dedicado para la interfaz de usuario

Las ventanas WinForms y WPF deben ejecutarse dentro de un hilo marcado como STA, de lo contrario, no funcionarán. Para obtener más información, consulte la documentación oficial.

No puede cambiar la propiedad ApartmentState de un hilo que ya está en ejecución. Debido a que el hilo principal del cBot o indicador no está marcado como STA, debe utilizar un nuevo hilo marcado como STA para WinForms y WPF.

Acceda a los miembros de la API desde el hilo de la interfaz de usuario

Como hemos explicado anteriormente, debe utilizar un hilo dedicado separado para ejecutar sus controles WPF. Solo el código que se ejecuta en este hilo podrá acceder a los controles y propiedades de la ventana.

Lo mismo es cierto para todos los miembros de la API de algo. Como la API no es segura para hilos, no puede acceder a los miembros de la API desde el hilo de la interfaz de usuario. En su lugar, debe despachar el trabajo utilizando el método BeginInvokeOnMainThread() en el código de su indicador o cBot.

Esto puede crear complicaciones si desea intercambiar datos entre su indicador o cBot y una ventana WPF. Para resolver este problema, puede crear una clase proxy para gestionar las interacciones entre su cBot o indicador y los hilos relacionados con la interfaz de usuario.