优化管理 (第一部分): 创建一个GUI(图形用户界面)·进阶篇
📘

优化管理 (第一部分): 创建一个GUI(图形用户界面)·进阶篇

第 2/3 篇

◍ 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。

MQL5 / C++
<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”,相当于从优化结果表里直接拉起那一笔历史测试。外汇与贵金属品种波动大、杠杆高,这类面板只解决查看效率,不代表任何参数组合未来必盈利。

MQL5 / C++
<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 不会抛错但双击无响应。外汇与贵金属品种做这类前向优化回测时样本外过拟合风险偏高,双击跑出的结果仅作概率参考。

MQL5 / C++
<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 事件订阅。外汇与贵金属插件开发涉及实盘交互,扩展出错可能导致终端卡死,改造前建议在模拟环境先验证。

MQL5 / C++
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 路径认不到。

MQL5 / C++
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 命令,点一下就把当前选中终端推进集合。外汇与贵金属多终端跑回测时,这种自动发现 + 手动追加的组合能少点不少重复操作,但多账户并发仍有连接失败的概率,属正常高风险环节。

MQL5 / C++
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}"

常见问题

先在 Shell 里建好主 VM 并绑定两个 Tab 的 DataContext,再用 UserControl 分别承载前向与后向优化页,避免直接在 XAML 写死逻辑。
ListView 放结果列表、右侧留设置面板容器,列表用 Binding 接 VM 集合,方便后面把双击事件桥接成命令。
小布可根据你描述的面板结构直接产出 WPF 外壳与附加属性写法草稿,你贴进项目改绑定即可省去手写样板。
建一个 AttachedProperty 监听 MouseDoubleClick,把 EventArgs 转成命令参数 Execute,XAML 里绑定 Command 和 CommandParameter 即可。
在命令执行方法里取选中项对象,赋给设置 VM 的当前项属性,面板用 Binding 自动显示对应字段供编辑。