WPF Windows Presentation Foundation(WPF)은 .NET의 일부인 Windows 사용자 인터페이스(UI) 솔루션입니다. 2006년에 처음 개발된 WPF는 WinForms 의 후속 제품으로 의도되었습니다. 추가 정보는 공식 문서 를 참조하세요.
WPF는 풍부하고 정교한 UI를 생성할 수 있도록 합니다. Microsoft에서 개발한 XML 기반 언어인 XAML을 사용하여 코드와 UI를 분리합니다. cTrader Windows는 WPF 애플리케이션이며, 사용자가 보고 있는 모든 UI 요소는 이 솔루션으로 구동됩니다.
WPF를 사용하면 픽셀 단위로 완벽에 가까운 기능이 풍부한 UI를 개발할 수 있습니다. 그러나 WPF는 학습 곡선이 가파릅니다. 상대적으로 간단한 UI 요소를 생성하려는 경우 차트 컨트롤 또는 WinForms 를 사용할 수 있습니다.
참고
WinForms 또는 WPF를 사용하는 알고리즘은 Windows 머신에서만 실행할 수 있습니다.
cBots 및 지표와 함께 WPF를 사용하려면 cTrader 컴파일러 를 임베디드 컴파일러에서 .NET SDK 컴파일러로 변경하세요.
프로젝트 구성 WinForms 와 마찬가지로, cBots 및 지표와 함께 WPF를 사용하기 전에 지표 또는 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 생성 과 유사합니다.
프로젝트를 구성한 후 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 추가 를 선택한 다음 Window(WPF)... 를 선택하세요.
새로 열린 창에서 WPF 창 옵션을 선택하세요. 창 이름을 MainWindow로 설정하고 추가 버튼을 클릭하세요. 새 WPF 창이 프로젝트 솔루션 탐색기에 나타납니다. 이 창은 .XAML 파일과 백엔드 코드가 포함된 .cs 파일 두 개를 갖게 됩니다.
.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 파일에서 이를 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 파일을 열고 다음 코드를 붙여넣으세요:
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 인스턴스를 생성하세요. 실행하면 다음 창이 나타납니다.
창에서 심벌 목록에서 심벌을 선택하고 실행 을 클릭하세요. 트레이딩 패널은 다소 간단하지만 의도한 대로 작동합니다. 복잡한 WPF 컨트롤을 생성하기 위한 템플릿으로 사용할 수 있습니다.
창의 .xaml.cs 파일에 있는 코드는 cBot에 지시를 보내는 역할을 합니다. 창을 위한 백엔드 코드를 개발할 때 다음 지침을 따르세요:
BeginInvokeOnMainThread () 메서드를 사용하여 cBot 또는 지표와의 상호 작용을 관리하는 서비스를 생성하세요. 복잡한 WPF 컨트롤을 구축하는 경우 MVVM , IoC 를 사용하고 비즈니스 로직 구성 요소를 UI와 분리하세요(예: Prism 사용). UI 전용 스레드 사용 WinForms 및 WPF 창은 STA 표시 스레드 내에서 실행되어야 합니다. 그렇지 않으면 작동하지 않습니다. 자세한 내용은 공식 문서 를 참조하세요.
이미 실행 중인 스레드의 ApartmentState 속성을 변경할 수 없습니다. 주요 cBot 또는 지표 스레드가 STA 표시되지 않았기 때문에, WinForms 및 WPF를 위해 새로운 STA 표시된 스레드를 사용해야 합니다.
UI 스레드에서 API 멤버에 액세스 이전에 설명한 대로, WPF 컨트롤을 실행하려면 별도의 전용 스레드를 사용해야 합니다. 이 스레드에서 실행되는 코드만이 창 컨트롤과 속성에 액세스할 수 있습니다.
모든 알고리즘 API 멤버도 마찬가지입니다. API는 스레드 안전하지 않기 때문에 UI 스레드에서 API 멤버에 액세스할 수 없습니다. 대신, 지표 또는 cBot 코드에서 BeginInvokeOnMainThread () 메서드를 사용하여 작업을 디스패치해야 합니다.
이로 인해 지표 또는 cBot과 WPF 창 간에 데이터를 교환하려는 경우 복잡성이 발생할 수 있습니다. 이 문제를 해결하기 위해 cBot 또는 지표와 UI 관련 스레드 간의 상호 작용을 관리하는 프록시 클래스를 생성할 수 있습니다.