由于 cTrader 指标和 cBot 构成 .NET 应用程序,您可以在创建它们时使用所有 .NET 技术,包括 WinForms 和 WPF 等 UI 相关解决方案。 cTrader 支持 WinForms 和 WPF 提供的所有功能,包括对话框、表单、Visual Studio WinForms 设计器、第三方 UI 控件和组件。
注意
使用 WinForms 或 WPF 的算法只能在 Windows 机器上运行。
要使用 WinForms,请将您的 cTrader 编译器 更改为 .NET SDK 编译器。
我们将通过创建一个自定义 WinForms 对话框来展示如何使用 WinForms。 它们对于显示警报或请求用户输入非常有用。
配置您的项目
在您的 cBot 和指标中使用 WinForms 之前,您需要在 cBot 或指标项目文件中进行一些更改。 由于 WinForms 仅在 Windows 上工作,您需要将项目中的指标或 cBot 框架目标更改为 .NET 的 Windows 版本。
为此,请在 Visual Studio 中打开您的项目文件,并将其内容替换为以下内容:
| <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="cTrader.Automate" Version="1.*" />
</ItemGroup>
</Project>
|
我们添加了 UseWindowsForms 标签,并将 TargetFramework 的值更改为 net6.0-windows。
然后,将 AccessRights 类参数的值更改为 FullAccess。 除非进行此更改,否则您的扩展将没有足够的访问权限来打开 WinForms 窗口。
在进行上述更改后,重新构建您的项目。
使用对话框
幸运的是,WinForms 包含多个现成的对话框,适用于各种情况,最显著的是显示错误消息、获取用户确认或选择文件或文件夹。
以下示例使用 MessageBox 类以及文件打开和保存对话框。
MessageBox
您可以像在任何其他 .NET 应用程序中使用 System.Windows.Forms.MessageBox 类一样使用它。
以下代码将标准确认框附加到指标:
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 | using cAlgo.API;
using System.Windows.Forms;
namespace WinFormsTest
{
[Indicator(IsOverlay = true, AccessRights = AccessRights.FullAccess)]
public class WinFormsTest : Indicator
{
protected override void Initialize()
{
var result = System.Windows.Forms.MessageBox.Show("Are you sure you want confirm?", "Confirmation", MessageBoxButtons.OKCancel, MessageBoxIcon.Information);
if (result == DialogResult.OK)
{
Print("OK");
}
else
{
Print("Cancel");
}
}
public override void Calculate(int index)
{
}
}
}
|
运行此指标的实例后,您应立即看到带有您指定文本的确认框。
打开或保存文件对话框
打开或保存文件对话框可用于允许 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
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 | using cAlgo.API;
using System.Windows.Forms;
using System;
using System.Threading;
namespace WinFormsTest
{
[Indicator(IsOverlay = true, AccessRights = AccessRights.FullAccess)]
public class WinFormsTest : Indicator
{
protected override void Initialize()
{
var thread = new Thread(() =>
{
var dialog = new OpenFileDialog
{
CheckFileExists = true,
CheckPathExists = true,
Filter = "Text Files (*.txt)|*.txt",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
Multiselect = true,
RestoreDirectory = true,
};
var result = dialog.ShowDialog();
if (result == DialogResult.OK)
{
foreach (var fileName in dialog.FileNames)
{
Print(fileName);
}
}
else
{
Print("Dialog Canceled");
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}
public override void Calculate(int index)
{
}
}
}
|
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 | using cAlgo.API;
using System.Windows.Forms;
using System;
using System.Threading;
namespace WinFormsTest
{
[Indicator(IsOverlay = true, AccessRights = AccessRights.FullAccess)]
public class WinFormsTest : Indicator
{
protected override void Initialize()
{
var thread = new Thread(() =>
{
var dialog = new SaveFileDialog
{
CheckFileExists = true,
CheckPathExists = true,
Filter = "Text Files (*.txt)|*.txt",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
RestoreDirectory = true,
};
var result = dialog.ShowDialog();
if (result == DialogResult.OK)
{
Print(dialog.FileName);
}
else
{
Print("Dialog Canceled");
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}
public override void Calculate(int index)
{
}
}
}
|
在示例中,我们使用了一个单独的线程来运行与对话框相关的代码。 这样做是为了考虑到主 cBot 或指标线程不是 STA 线程的事实。 我们将在下一节中详细讨论此问题。
为 UI 使用专用线程
当运行引用 WinForms 的代码时,您必须使用 STA 标记的线程,否则在执行过程中可能会出现错误。 有关更多信息,请参阅 官方文档。
您无法更改已运行线程的 ApartmentState 属性。 由于主 cBot 或指标线程未标记为 STA,您必须为 WinForms 和 WPF 使用新的 STA 标记线程。
上一节中的代码还调用了 thread.Join() 方法。 它会阻止主 cBot 或指标线程的执行,直到 UI 相关线程被释放(例如,用户点击确认对话框中的 OK)。
每当您想显示 Windows 窗体或 WPF 元素时,您必须在单独的 STA 标记线程中运行代码。
创建新的自定义表单
到目前为止,我们使用了内置的 WinForms 类。 然而,也可以创建自定义类。 在本节中,我们将创建一个显示当前交易账户信息的表单,例如其余额和保证金。
要创建新表单,请在 Visual Studio 中右键单击您的 cBot 或指标,点击 添加 并选择 用户控件 (Windows Forms) ...'

Visual Studio 将打开一个对话框窗口,您可以在其中选择要添加到项目中的项目类型。 选择 表单 (Windows Forms)(Visual Studio 应自动选择它)。

然后,将表单名称从 Form1.cs 更改为 AccountInfoForm.cs。 点击 添加 按钮。
将在您的项目中创建一个新表单;IDE 将显示 Visual Studio WinForms 设计器。 因此,您将能够使用其所有功能,包括通过工具箱添加控件。

在此示例中,我们已经创建了一个表单。 将以下代码复制并粘贴到 AccountIntoForm.Designer.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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262 | namespace WinForms_Test
{
partial class AccountInfoForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.NumberLabel = new System.Windows.Forms.Label();
this.BrokerLabel = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.CurrencyLabel = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.BalanceLabel = new System.Windows.Forms.Label();
this.label7 = new System.Windows.Forms.Label();
this.EquityLabel = new System.Windows.Forms.Label();
this.label9 = new System.Windows.Forms.Label();
this.PositionsNumberLabel = new System.Windows.Forms.Label();
this.label11 = new System.Windows.Forms.Label();
this.OrdersNumberLabel = new System.Windows.Forms.Label();
this.label13 = new System.Windows.Forms.Label();
this.NetProfitLabel = new System.Windows.Forms.Label();
this.label15 = new System.Windows.Forms.Label();
this.UpdateButton = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label1.Location = new System.Drawing.Point(12, 33);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(72, 20);
this.label1.TabIndex = 0;
this.label1.Text = "Number:";
//
// NumberLabel
//
this.NumberLabel.AutoSize = true;
this.NumberLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.NumberLabel.Location = new System.Drawing.Point(188, 33);
this.NumberLabel.Name = "NumberLabel";
this.NumberLabel.Size = new System.Drawing.Size(0, 20);
this.NumberLabel.TabIndex = 1;
//
// BrokerLabel
//
this.BrokerLabel.AutoSize = true;
this.BrokerLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.BrokerLabel.Location = new System.Drawing.Point(188, 67);
this.BrokerLabel.Name = "BrokerLabel";
this.BrokerLabel.Size = new System.Drawing.Size(0, 20);
this.BrokerLabel.TabIndex = 3;
//
// label3
//
this.label3.AutoSize = true;
this.label3.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label3.Location = new System.Drawing.Point(12, 67);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(56, 20);
this.label3.TabIndex = 2;
this.label3.Text = "Broker";
//
// CurrencyLabel
//
this.CurrencyLabel.AutoSize = true;
this.CurrencyLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.CurrencyLabel.Location = new System.Drawing.Point(188, 101);
this.CurrencyLabel.Name = "CurrencyLabel";
this.CurrencyLabel.Size = new System.Drawing.Size(0, 20);
this.CurrencyLabel.TabIndex = 5;
//
// label5
//
this.label5.AutoSize = true;
this.label5.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label5.Location = new System.Drawing.Point(12, 101);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(75, 20);
this.label5.TabIndex = 4;
this.label5.Text = "Currency";
//
// BalanceLabel
//
this.BalanceLabel.AutoSize = true;
this.BalanceLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.BalanceLabel.Location = new System.Drawing.Point(188, 135);
this.BalanceLabel.Name = "BalanceLabel";
this.BalanceLabel.Size = new System.Drawing.Size(0, 20);
this.BalanceLabel.TabIndex = 7;
//
// label7
//
this.label7.AutoSize = true;
this.label7.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label7.Location = new System.Drawing.Point(12, 135);
this.label7.Name = "label7";
this.label7.Size = new System.Drawing.Size(63, 20);
this.label7.TabIndex = 6;
this.label7.Text = "Balance";
//
// EquityLabel
//
this.EquityLabel.AutoSize = true;
this.EquityLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.EquityLabel.Location = new System.Drawing.Point(188, 173);
this.EquityLabel.Name = "EquityLabel";
this.EquityLabel.Size = new System.Drawing.Size(0, 20);
this.EquityLabel.TabIndex = 9;
//
// label9
//
this.label9.AutoSize = true;
this.label9.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label9.Location = new System.Drawing.Point(12, 173);
this.label9.Name = "label9";
this.label9.Size = new System.Drawing.Size(54, 20);
this.label9.TabIndex = 8;
this.label9.Text = "Equity";
//
// PositionsNumberLabel
//
this.PositionsNumberLabel.AutoSize = true;
this.PositionsNumberLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.PositionsNumberLabel.Location = new System.Drawing.Point(188, 210);
this.PositionsNumberLabel.Name = "PositionsNumberLabel";
this.PositionsNumberLabel.Size = new System.Drawing.Size(0, 20);
this.PositionsNumberLabel.TabIndex = 11;
//
// label11
//
this.label11.AutoSize = true;
this.label11.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label11.Location = new System.Drawing.Point(12, 210);
this.label11.Name = "label11";
this.label11.Size = new System.Drawing.Size(85, 20);
this.label11.TabIndex = 10;
this.label11.Text = "Positions #";
//
// OrdersNumberLabel
//
this.OrdersNumberLabel.AutoSize = true;
this.OrdersNumberLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.OrdersNumberLabel.Location = new System.Drawing.Point(188, 250);
this.OrdersNumberLabel.Name = "OrdersNumberLabel";
this.OrdersNumberLabel.Size = new System.Drawing.Size(0, 20);
this.OrdersNumberLabel.TabIndex = 13;
//
// label13
//
this.label13.AutoSize = true;
this.label13.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label13.Location = new System.Drawing.Point(12, 250);
this.label13.Name = "label13";
this.label13.Size = new System.Drawing.Size(69, 20);
this.label13.TabIndex = 12;
this.label13.Text = "Orders #";
//
// NetProfitLabel
//
this.NetProfitLabel.AutoSize = true;
this.NetProfitLabel.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.NetProfitLabel.Location = new System.Drawing.Point(188, 285);
this.NetProfitLabel.Name = "NetProfitLabel";
this.NetProfitLabel.Size = new System.Drawing.Size(0, 20);
this.NetProfitLabel.TabIndex = 15;
//
// label15
//
this.label15.AutoSize = true;
this.label15.Font = new System.Drawing.Font("Segoe UI Variable Display", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.label15.Location = new System.Drawing.Point(12, 285);
this.label15.Name = "label15";
this.label15.Size = new System.Drawing.Size(79, 20);
this.label15.TabIndex = 14;
this.label15.Text = "Net Profit";
//
// UpdateButton
//
this.UpdateButton.Location = new System.Drawing.Point(273, 340);
this.UpdateButton.Name = "UpdateButton";
this.UpdateButton.Size = new System.Drawing.Size(75, 23);
this.UpdateButton.TabIndex = 16;
this.UpdateButton.Text = "Update";
this.UpdateButton.UseVisualStyleBackColor = true;
this.UpdateButton.Click += new System.EventHandler(this.UpdateButton_Click);
//
// AccountInfoForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(360, 375);
this.Controls.Add(this.UpdateButton);
this.Controls.Add(this.NetProfitLabel);
this.Controls.Add(this.label15);
this.Controls.Add(this.OrdersNumberLabel);
this.Controls.Add(this.label13);
this.Controls.Add(this.PositionsNumberLabel);
this.Controls.Add(this.label11);
this.Controls.Add(this.EquityLabel);
this.Controls.Add(this.label9);
this.Controls.Add(this.BalanceLabel);
this.Controls.Add(this.label7);
this.Controls.Add(this.CurrencyLabel);
this.Controls.Add(this.label5);
this.Controls.Add(this.BrokerLabel);
this.Controls.Add(this.label3);
this.Controls.Add(this.NumberLabel);
this.Controls.Add(this.label1);
this.Name = "AccountInfoForm";
this.Text = "Account Info";
this.Load += new System.EventHandler(this.AccountInfoForm_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label NumberLabel;
private System.Windows.Forms.Label BrokerLabel;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label CurrencyLabel;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.Label BalanceLabel;
private System.Windows.Forms.Label label7;
private System.Windows.Forms.Label EquityLabel;
private System.Windows.Forms.Label label9;
private System.Windows.Forms.Label PositionsNumberLabel;
private System.Windows.Forms.Label label11;
private System.Windows.Forms.Label OrdersNumberLabel;
private System.Windows.Forms.Label label13;
private System.Windows.Forms.Label NetProfitLabel;
private System.Windows.Forms.Label label15;
private System.Windows.Forms.Button UpdateButton;
}
}
|
上述代码由 WinForms 设计器生成。 我们现在将为新表单编写后端代码。 为此,右键单击表单并选择 查看代码。

复制并粘贴以下示例:
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 | using System;
using System.Linq;
using System.Windows.Forms;
using cAlgo.API;
namespace WinForms_Test
{
public partial class AccountInfoForm : Form
{
private int _accountNumber;
private string _brokerName;
private string _currency;
private double _balance;
private double _equity;
private int _positionsCount;
private int _ordersCount;
private double _netProfit;
private readonly Indicator _indicator;
public AccountInfoForm(Indicator indicator)
{
_indicator = indicator;
InitializeComponent();
}
private void GetDataFromIndicator()
{
_indicator.BeginInvokeOnMainThread(() =>
{
_accountNumber = _indicator.Account.Number;
_brokerName = _indicator.Account.BrokerName;
_currency = _indicator.Account.Asset.Name;
_balance = _indicator.Account.Balance;
_equity = _indicator.Account.Equity;
_positionsCount = _indicator.Positions.Count;
_ordersCount = _indicator.PendingOrders.Count;
_netProfit = _indicator.History.Sum(trade => trade.NetProfit);
UpdateData();
});
}
private void UpdateData()
{
_ = BeginInvoke(() =>
{
NumberLabel.Text = _accountNumber.ToString();
BrokerLabel.Text = _brokerName;
CurrencyLabel.Text = _currency;
BalanceLabel.Text = Math.Round(_balance, 2).ToString();
EquityLabel.Text = Math.Round(_equity, 2).ToString();
PositionsNumberLabel.Text = _positionsCount.ToString();
OrdersNumberLabel.Text = _ordersCount.ToString();
NetProfitLabel.Text = Math.Round(_netProfit, 2).ToString();
});
}
private void UpdateButton_Click(object sender, EventArgs e) => GetDataFromIndicator();
private void AccountInfoForm_Load(object sender, EventArgs e) => GetDataFromIndicator();
}
}
|
值得注意的是,我们的代码使用了 BeginInvokeOnMainThread() 和 BeginInvoke() 方法。 我们将在下一节中解释我们这样做的理由。
现在,我们将通过从指标中调用其 ShowDialog() 方法来使用我们的自定义表单。 将以下代码复制并粘贴到您的指标源代码中:
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 WinForms_Test;
namespace WinFormsTest
{
[Indicator(IsOverlay = true, AccessRights = AccessRights.FullAccess)]
public class WinFormsTest : Indicator
{
protected override void Initialize()
{
var thread = new Thread(() =>
{
var form = new AccountInfoForm(this);
_ = form.ShowDialog();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
public override void Calculate(int index)
{
}
}
}
|
当您添加新表单时,它将自动使用您的指标或 cBot 命名空间(cAlgo)。 为避免错误,请更改添加的表单的命名空间,然后将其添加到您的指标或 cBot 源代码文件中。 在我们的案例中,WinForms_Test 是命名空间名称。
构建指标并创建实例后,您应该会看到以下表单出现。

从 UI 专用线程访问 API 成员
正如我们之前解释的那样,您必须使用一个单独的专用线程来运行您的 WinForms。 只有在此线程上执行的代码才能访问窗体控件和属性。
对于所有算法 API 成员也是如此。 由于 API 不是线程安全的,您无法从 Form 线程访问 API 成员。 相反,您必须通过在您的指标或 cBot 代码中使用 BeginInvokeOnMainThread() 方法来分派此任务。
如果您希望在指标或 cBot 与 WinForm 之间交换数据,这可能会带来复杂性。 为了解决这个问题,您可以创建一个代理类来管理您的 cBot 或指标与 Form 线程之间的交互。