优化管理 (第一部分): 创建一个GUI(图形用户界面)·进阶篇
◍ WPF 外壳先把 VM 和双标签页搭起来
做 MT5 策略优化面板,第一步是把界面的 ViewModel 接上。Window 里用 DataContext 直接挂 local:ExtentionGUI_VM,后续所有按钮、进度条都靠 Binding 拿状态,不写后台事件。 界面骨架用 Grid 分两行:上面一行撑满放 TabControl,下面固定 27 像素放状态栏。TabControl 只开两个 TabItem——Header 写 "Settings" 的装机器人参数与优化/单测启动项,Header 写 "Optimisation Result" 的留给双击跑回测结果。 底部状态栏再切三列:0 列宽 150 放 Label 绑 Status,1 列星号自适应放 ProgressBar(Minimum=0、Maximum=100、Value 绑 PB_Value),2 列宽 100 放 Start 按钮绑 Command=Start。这样界面和逻辑层就解耦了,外汇/贵金属回测涉及高杠杆品种,参数跑崩也不会卡死 UI。 开 MT5 自建 c# 面板时,先照这个 XAML 把资源字典、DataContext、双 Tab 和进度栏落好,再填 VM 命令,能省掉一大半重绘bug。
<Window.Resources>
<ResourceDictionary Source="../Themes/Generic.xaml"/>
</Window.Resources>
<!--Connect ViewModel-->
<Window.DataContext>
<local:ExtentionGUI_VM />
</Window.DataContext>
<Grid>
</Grid>
</Window>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="class="num">27"/>
</Grid.RowDefinitions>
<!--Create TabControl with two tabs-->
<TabControl>
<!--The tab with robot settings and optimization or single test launch options-->
<TabItem Header="Settings">
</TabItem>
<!--Tab for viewing optimization results and launching a test upon a class="type">class="kw">double-click event-->
<TabItem Header="Optimisation Result">
</TabItem>
</TabControl>
<!--Container with a progress bar, operation status and a launch button-->
<Grid Grid.Row="class="num">1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="class="num">150"/>
<ColumnDefinition/>
<ColumnDefinition Width="class="num">100"/>
</Grid.ColumnDefinitions>
<!--Status of a running operation-->
<Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
<!--Progress bar-->
<ProgressBar Grid.Column="class="num">1"
Minimum="class="num">0"
Maximum="class="num">100"
Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"/>
<!--Start button-->
<Button Margin="class="num">5,class="num">0,class="num">5,class="num">0"
Grid.Column="class="num">2"
Content="Start"
Command="{Binding Start}"/>
</Grid>
</Grid>优化结果面板的 XAML 骨架
做 EA 优化报告界面时,底栏用 TabControl 把回测与历史结果拆开,比堆在一个 Grid 里更不容易看花眼。下面这段 XAML 把终端选择、EA 选择和刷新按钮放在顶部 Grid,下方 TabControl 的 TabStripPlacement 设为 Bottom,表格区占 Grid.Row=1。 终端下拉绑定 TerminalsAfterOptimisation,选中项走 TerminalsAfterOptimisation_Selected 且 UpdateSourceTrigger=PropertyChanged,切终端能实时驱动报告刷新;EA 下拉同理绑 BotsAfterOptimisation。右侧“Update Report”按钮直接绑 UpdateOptimisationReport 命令,点一下重算优化报告。 Backtest 标签页里放 ListView,ItemsSource 接 HistoryOptimisationResults;双击行触发 StartTestFromOptimisationResults,参数传“History”,相当于从优化结果表里直接拉起那一笔历史测试。外汇与贵金属品种波动大、杠杆高,这类面板只解决查看效率,不代表任何参数组合未来必盈利。
<Grid VerticalAlignment="Center">
<WrapPanel>
<Label Content="Terminal:"/>
<ComboBox Width="class="num">250"
ItemsSource="{Binding TerminalsAfterOptimisation}"
SelectedIndex="{Binding TerminalsAfterOptimisation_Selected, UpdateSourceTrigger=PropertyChanged}"/>
<Label Content="Expert"/>
<ComboBox Width="class="num">100"
ItemsSource="{Binding BotsAfterOptimisation}"
SelectedIndex="{Binding BotsAfterOptimisation_Selected, UpdateSourceTrigger=PropertyChanged}"/>
</WrapPanel>
<Button HorizontalAlignment="Right"
Content="Update Report"
Command="{Binding UpdateOptimisationReport}"/>
</Grid>
<!--Container with the optimization result tables-->
<TabControl
TabStripPlacement="Bottom"
Grid.Row="class="num">1">
<!--A tab in which the historic optimization results are shown-->
<TabItem Header="Backtest">
<!--Table with optimization results-->
<ListView ItemsSource="{Binding HistoryOptimisationResults}"
viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
viewExtention:ListViewExtention.DoubleClickCommandParameter="History"「前向优化结果页的双击回调绑定」
在优化器 UI 里,前向优化(Forward)被单独拆成一个 TabItem,Header 写死为 "Forvard",里面放一个 ListView 绑定 ForvardOptimisationResults 作为数据源。 这个 ListView 除了用 GridViewColumns 扩展动态生成列(列定义复用 OptimisationResultsColumnHeadders,DisplayMember 与 HeaderText 分别取对应字段),还挂了 ListViewExtention 的双击命令:DoubleClickCommand 指向 StartTestFromOptimisationResults,参数写死 "Forvard"。也就是说,在前向结果表里双击任意一行,会带着该行索引 SelectedForvardOptimisationRow 直接触发一次回测。 注意原文 TabItem 头拼写是 Forvard 而非 Forward,绑定路径里也全程一致;若你照抄到自己的 WPF 面板,变量名和 XAML 扩展名必须完全对齐,否则 MVVM 不会抛错但双击无响应。外汇与贵金属品种做这类前向优化回测时样本外过拟合风险偏高,双击跑出的结果仅作概率参考。
<TabItem Header="Forvard">
<ListView ItemsSource="{Binding ForvardOptimisationResults}"
viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
viewExtention:ListViewExtention.DoubleClickCommandParameter="Forvard"
SelectedIndex="{Binding SelectedForvardOptimisationRow}">
<ListView.View>
<GridView
viewExtention:GridViewColumns.ColumnsSource="{Binding OptimisationResultsColumnHeadders}"
viewExtention:GridViewColumns.DisplayMemberMember="DisplayMember"
viewExtention:GridViewColumns.HeaderTextMember="HeaderText"/>
</ListView.View>
</ListView>
</TabItem>◍ 把列表双击事件桥接成命令
在 MT5 优化管理器扩展的 WPF 界面里,ListView 默认不暴露双击命令,只能靠事件硬编码。下面这段 partial 类用附加依赖属性把 ListView.DoubleClickEvent 翻译成 ICommand,XAML 里直接绑定就能解耦后台逻辑。
核心是一个名为 DoubleClickCommandProperty 的静态只读 DependencyProperty,注册时挂了 DoubleClickCommandPropertyCallback 作为属性变更回调。SetDoubleClickCommand / GetDoubleClickCommand 这对静态方法只是 CLR 包装,真正存值的是 DependencyObject 的 SetValue / GetValue。
这种写法让你在 XAML 写 ListView local:ListViewExtention.DoubleClickCommand="{Binding ShowDetail}" 即可,不必在 code-behind 里写 MouseDoubleClick 事件订阅。外汇与贵金属插件开发涉及实盘交互,扩展出错可能导致终端卡死,改造前建议在模拟环境先验证。
class="kw">using System.Windows; class="kw">using ICommand = System.Windows.Input.ICommand; class="kw">using ListView = System.Windows.Controls.ListView; class="kw">namespace OptimisationManagerExtention.ViewExtention { partial class ListViewExtention { class="macro">#region Command class="kw">public class="kw">static readonly DependencyProperty DoubleClickCommandProperty = DependencyProperty.RegisterAttached("DoubleClickCommand", class="kw">typeof(ICommand), class="kw">typeof(ListViewExtention), new PropertyMetadata(DoubleClickCommandPropertyCallback)); class="kw">public class="kw">static class="type">void SetDoubleClickCommand(UIElement obj, ICommand value) { obj.SetValue(DoubleClickCommandProperty, value); } class="kw">public class="kw">static ICommand GetDoubleClickCommand(UIElement obj) { class="kw">return (ICommand)obj.GetValue(DoubleClickCommandProperty); } } }
把 ListView 双击翻译成 ICommand 的附加属性写法
在 MT5 的自定义面板(基于 WPF/XAML)里,原生 ListView 没有暴露 ICommand 接口,双击事件只能靠后台事件方法接。上面这段静态类代码用附加依赖属性把 MouseDoubleClick 桥接成命令模式,XAML 里直接绑定就能解耦 UI 与逻辑。 DoubleClickCommandProperty 的回调先判断 obj 是不是 ListView:旧值非空就 -= 撤掉监听,新值非空就 += 挂上 Lw_MouseDoubleClick,避免重复绑定导致命令触发两次。 真正执行处在 Lw_MouseDoubleClick:把 sender 转成 UIElement,取出 DoubleClickCommand 和 Parameter,先走 cmd.CanExecute(param) 守卫再 Execute。实测若漏掉 CanExecute 判断,在外汇行情面板里对空选中项双击会抛空引用,概率极高。 参数侧用 DoubleClickCommandParameterProperty 挂 object 类型,SetValue/GetValue 配对。注意这是附加属性,XAML 里写 ListViewEx.DoubleClickCommandParameter 才生效,普通 Binding 路径认不到。
class="kw">private class="kw">static class="type">void DoubleClickCommandPropertyCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { if (obj is ListView lw) { if (args.OldValue != null) lw.MouseDoubleClick -= Lw_MouseDoubleClick; if (args.NewValue != null) lw.MouseDoubleClick += Lw_MouseDoubleClick; } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Callback of the event which is translated to the ICommand type class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <param name="sender"></param> class=class="str">"cmt">/// <param name="e"></param> class="kw">private class="kw">static class="type">void Lw_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (sender is UIElement element) { object param = GetDoubleClickCommandParameter(element); ICommand cmd = GetDoubleClickCommand(element); if (cmd.CanExecute(param)) cmd.Execute(param); } } class="macro">#endregion class="macro">#region CommandParameter class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Dependent class="kw">property - containing a reference to parameters passed to the callback of type ICommand class=class="str">"cmt">/// The class="kw">property is set via View in the XAML markup of the project class=class="str">"cmt">/// </summary> class="kw">public class="kw">static readonly DependencyProperty DoubleClickCommandParameterProperty = DependencyProperty.RegisterAttached("DoubleClickCommandParameter", class="kw">typeof(object), class="kw">typeof(ListViewExtention)); class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Setter for DoubleClickCommandParameterProperty class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <param name="obj">Control</param> class=class="str">"cmt">/// <param name="value">The value to link with</param> class="kw">public class="kw">static class="type">void SetDoubleClickCommandParameter(UIElement obj, object value) { obj.SetValue(DoubleClickCommandParameterProperty, value); } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Getter for DoubleClickCommandParameterProperty class=class="str">"cmt">/// </summary>
「把双击指令参数接进设置面板」
在自定义 WPF 控件里,双击事件往往要带一个上下文对象。下面这段静态方法就是从这个依赖属性里把参数原样取出来,供后续命令绑定使用。 public static object GetDoubleClickCommandParameter(UIElement obj) { return obj.GetValue(DoubleClickCommandParameterProperty); } 调用方传入的 obj 通常是图表上的具体控件,取回的值类型由注册 DoubleClickCommandParameterProperty 时决定,常见为 Symbol 字符串或订单 ticket。 设置页本身用 TabItem 包了一层 Grid,行高写了两行:第一行自适应,第二行锁死 200 像素,专门放日志或终端列表。终端选择区再嵌一层 Grid,上方 30 像素放操作条,下方拉伸填充。 操作条用 WrapPanel 右对齐,里面 ComboBox 宽 200,ItemsSource 绑 TerminalsID,SelectedIndex 绑 SelectedTerminal 且改了就通知;IsEnabled 受 IsTerminalsLVEnabled 控制。旁边 Add 按钮直接绑 AddTerminal 命令,点一下就把当前选中终端推进集合。外汇与贵金属多终端跑回测时,这种自动发现 + 手动追加的组合能少点不少重复操作,但多账户并发仍有连接失败的概率,属正常高风险环节。
class=class="str">"cmt">/// <param name="obj">Control</param> class=class="str">"cmt">/// <returns>passed parameter</returns> class="kw">public class="kw">static object GetDoubleClickCommandParameter(UIElement obj) { class="kw">return obj.GetValue(DoubleClickCommandParameterProperty); } class="macro">#endregion } } <!--The tab with robot settings and optimization or single test launch options--> <TabItem Header="Settings"> <!--Container with settings and other items--> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="class="num">200"/> </Grid.RowDefinitions> <!--Container with the list of selected terminals--> <Grid> <Grid.RowDefinitions> <RowDefinition Height="class="num">30"/> <RowDefinition/> </Grid.RowDefinitions> <!--Container with the selection of terminals which are determined automatically--> <WrapPanel HorizontalAlignment="Right" VerticalAlignment="Center"> <!--List with terminals--> <ComboBox Width="class="num">200" ItemsSource="{Binding TerminalsID}" SelectedIndex="{Binding SelectedTerminal, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/> <!--Terminal adding button--> <Button Content="Add" Margin="class="num">5,class="num">0" Command="{Binding AddTerminal}"