MQL 作为 MQL 程序图形界面的标记工具(第三部)。 窗体设计师

9 九月 2020, 11:35
Stanislav Korotky
0
1 128

在前两篇文章( 1 2 )中,我们研究了在 MQL 中构建界面标记系统的一般概念,并为表达类的层次结构的界面元素实现了基本类的初始化,对其进行缓存,样式化,为其设置属性,以及响应事件。 动态创建所需元素能够即时修改简单对话框布局,而已创建元素的单个存储的可用性通常能用建议的 MQL 语法来创建它,随后可以将其按原样插入到需要 GUI 的 MQL 程序之中。 故此,我们已着手创建窗体的图形编辑器。 我们会在本文中密切关注此任务。

问题陈述

编辑器必须确保在窗口中布置元素,并调整其基本属性。 以下是可支持属性的常规列表,但并非所有属性均适用于所有类型的元素。

  • 类型,
  • 名称,
  • 宽度,
  • 高度,
  • 内部内容对齐样式,
  • 文字或标题,
  • 背景颜色,
  • 在父容器里的对齐方式, 以及
  • 距容器边界的偏移量/区域。

此处未包括许多其他属性,例如字体名称和大小,或各种类型“控件”的特定属性(尤其是“粘贴”按钮的属性)。 如此做意在简化这个主要针对概念验证(POC)的项目。 如有必要,稍后可以在编辑器中添加其他属性的支持。

通过偏移量可以间接完成绝对坐标定位,但是不建议这样做。 建议 CBox 容器采用本身的对齐设置自动执行定位。

编辑器是为标准函数库界面元素类而设计的。 若要为其他函数库创建类似的工具,您必须依据提议的标记系统为所有抽象实体编写特定实现。 同时,您应该遵照标准库的标记类实现作为指导。

应当注意,“标准组件库”定义实际上并不正确,因为关乎之前文章的上下文,我们必须对其进行大量修改,并将其放置在并行版本分支的 ControlsPlus 文件夹中。 于此,我们将继续使用和修改它。

我们列出编辑器支持的元素类型。

  • 带有水平(CBoxH)和垂直(CBoxV)方向的容器 CBox,
  • CButton,
  • CEdit 输入框,
  • CLabel,
  • SpinEditResizable,
  • 日历选择器 CDatePicker,
  • 下拉菜单 ComboBoxResizable,
  • 列表 ListViewResizable,
  • CheckGroupResizable, 和
  • RadioGroupResizable.

所有类都确保能够自适应调整大小(某些标准类型在一开始就可以做到,而我们必须对其他类型进行相当大的更改)。

该程序由两个窗口组成:“Inspector” 对话框,用户在其中选择要创建的控件所需属性;在 “Designer” 中,创建这些元素,从而按设计形成图形界面的外观。

GUI MQL 设计器程序界面草案

GUI MQL 设计器程序界面草案

就 MQL 而言,该程序将在各自名称的头文件中定义 2 个基类,即 InspectorDialog 和 DesignerForm。

  #include "InspectorDialog.mqh"
  #include "DesignerForm.mqh"
  
  InspectorDialog inspector;
  DesignerForm designer;
  
  int OnInit()
  {
      if(!inspector.CreateLayout(0, "Inspector", 0, 20, 20, 200, 400)) return (INIT_FAILED);
      if(!inspector.Run()) return (INIT_FAILED);
      if(!designer.CreateLayout(0, "Designer", 0, 300, 50, 500, 300)) return (INIT_FAILED);
      if(!designer.Run()) return (INIT_FAILED);
      return (INIT_SUCCEEDED);
  }

这两个窗口都是由 MQL 标记技术形成的 AppDialogResizable(以下称为 CAppDialog)的子类。 因此,我们看到调用 CreateLayout,替代了 Create。

每个窗口都有自己的界面元素缓存。 然而,在 Inspector 当中,它从一开始就填充了“控件”,它们以相当复杂的布局(我们尝试以一般术语进行研究)进行描述,而在 Designer 里则是空的。 这很容易解释:几乎所有程序的业务逻辑都存储在 Inspector 之中,而 Designer 则是一个虚拟对象,Inspector 将根据用户的命令逐个实现新元素。

PropertySet

上面列出的每个属性都由特定类型的值表示。 例如,元素名称是一个字符串,而宽度和高度则是整数。 全部的数值集合完整定义了必须出现在 Designer 中的对象。 将集合存储在一个位置是合理的,为此引入了特殊类 PropertySet。 但是其中必须包含哪些成员变量呢?

乍一看,使用简单嵌入式类型的变量似乎是一个显而易见的解决方案。 不过,它们缺乏将来会用到的重要功能。 MQL 不支持指向简单变量的链接。 于此同时,链接在处理用户界面的算法当中又非常重要。 这通常意味着针对数值变化的复杂反应。 例如,在一个字段中输入的数值若是超界,其必然会阻塞某些依赖它的“控件”。如果这些“控件”能够通过检查存储在单一位置处的数值,并遵其指导控制自己的状态,将会很方便。 最简单的方式是利用链接“指路”到同一变量。 因此,我们将用近似如下所示的模板包装器类,替代简单的嵌入式类型,临时将其命名为 Value。

  template<typename V>
  class Value
  {
    protected:
      V value;
      
    public:
      V operator~(void) const // getter
      {
        return value;
      }
      
      void operator=(V v)     // setter
      {
        value = v;
      }
  };

加上单词“大约”是个好主意。 实际上,会在类中添加更多功能,下面将会进行研究。

对象包装器能够拦截重载运算符 '=' 作为赋新值,而在使用简单类型时是不可能的。 我们将需要它。

研究该类,可以大致如下描述新界面对象的属性集合。

  class PropertySet
  {
    public:
      Value<string> name;
      Value<int> type;
      Value<int> width;
      Value<int> height;
      Value<int> style; // VERTICAL_ALIGN / HORIZONTAL_ALIGN / ENUM_ALIGN_MODE
      Value<string> text;
      Value<color> clr;
      Value<int> align; // ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT
      Value<ushort> margins[4];
  };

在 “Inspector” 对话框里,我们将引入该类的变量,从 Inspector 控件输入的当前设置会集中存储于此。

显然,在 Inspector 里每个属性都会用合适的控件来定义。 例如,为了选择要创建的“控件”类型,将用下拉列表 CComboBox,而将 CEdit 输入框则用于名称。 属性代表类型的单个值,例如列表中的行、数字或索引。 即使是复合属性,例如为 4 个边中的每一个分别定义的偏移量,也应独立考虑(左,上、等等),因为需要保留 4 个字段用于输入它们,因此,每个值都将连接到为其分配的控件。

因此,我们为 “Inspector” 对话框制定一条显而易见的规则 — 其中的每个控件都定义与之相关的属性,并且始终拥有给定类型的特定值。 这导致我们获得以下架构解决方案。

“控件”的特性

在之前的文章中,我们引入了一个特殊的接口 Notifiable,该接口允许为特定控件定义事件处理。

  template<typename C>
  class Notifiable: public C
  {
    public:
      virtual bool onEvent(const int event, void *parent) { return false; };
  };

在这里,C 是“控件”类之一,譬如 CEdit、CSpinEdit、等等。 布局缓存自动为相关元素和事件类型调用 onEvent 应答程序。 很自然,只有将正确的代码添加到事件映射中,它才会发生。 例如,在上一部分中,依此原理调整了处理 “Inject” 按钮单击的过程(它已被描述为 Notifiable <CButton> 的后代)。

如果用控件来调整预定义类型的属性,那么创建一个更专业的接口 PlainTypeNotifiable 会很令人神往。

  template<typename C, typename V>
  class PlainTypeNotifiable: public Notifiable<C>
  {
    public:
      virtual V value() = 0;
  };

方法 value 旨在从 C 元素里返回最典型的 V-类型值。例如,对于类 CEdit,返回字符串类型值看起来很自然(在某些假设的类 ExtendedEdit 中)。

  class ExtendedEdit: public PlainTypeNotifiable<CEdit, string>
  {
    public:
      virtual string value() override
      {
        return Text();
      }
  };

对于每种“控件”,都有一个单一特征的数据类型,或有限范围(例如,对于整数,您可选的精度有 short、int 或 long)。 所有“控件”都有一个或另一个 “getter” 方法,可以在可重载的 “value” 方法中取值。

故此,我们进入了体系结构解决方案的要点 — 协调 Value 和 PlainTypeNotifiable 类。 它用后代类实现,PlainTypeNotifiable,该类将“控件”值从 Inspector 里移到与其链接的 Value 属性中。

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      Value<V> *property;
      
    public:
      void bind(Value<V> *prop)
      {
        property = prop;     // pointer assignment
        property = value();  // overloaded operator assignment for value of type V
      }
      
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CHANGE || event == ON_END_EDIT)
        {
          property = value();
          return true;
        }
        return false;
      };
  };

由于继承自模板类 PlainTypeNotifiable,因此新类 NotifiableProperty 表示 C “控件”类和 V-类型值的提供者。

方法 bind 能够将“值”的链接保留在“控件”内部,然后自动更改属性值(通过引用),从而响应用户对“控件”的操作。

例如,对于字符串类型的输入字段,引入了 EditProperty,类似于 ExtendedEdit 实例,但继承自 NotifiableProperty:

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      virtual string value() override
      {
        return Text(); // Text() is a standard method of CEdit
      }
  };

对于下拉列表,类似的类定义了含有整数值的属性。

  class ComboBoxProperty: public NotifiableProperty<ComboBoxResizable,int>
  {
    public:
      virtual int value() override
      {
        return (int)Value(); // Value() is a standard method of CComboBox
      }
  };

“控件”属性类描述了程序中所有基本类型的元素。

“可通知属性”类示意图

“可通知属性”类示意图

现在是时候摆脱“近似地”这个称呼,并了解完整的类了。

StdValue:值、监控和依赖

上面已经提到了一种标准情况,其中有必要监视某些“控件”的变化,以便检查其有效性,和其他“控件”的状态变化。 换言之,我们需要一个能够监视“控件”,并通知其他相关“控件”其变化的观察者。

为此目的,引入了接口 StateMonitor(观察者)。

  class StateMonitor
  {
    public:
      virtual void notify(void *sender) = 0;
  };

方法 notify 由变化源调用,从而令观察者能够在必要时做出响应。 变化源可以通过 “sender” 参数加以识别。 当然,变化源必须预先以某种方式知道对其通知感兴趣的特定观察者。 为此,源必须实现接口 Publisher。

  class Publisher
  {
    public:
      virtual void subscribe(StateMonitor *ptr) = 0;
      virtual void unsubscribe(StateMonitor *ptr) = 0;
  };

观察者可以利用 “subscribe” 方法将链接传递给 Publisher。 不难猜测,对于我们而言,变化的来源将是属性,因此,假设类 Value 实际上是从 Publisher 继承的,并如下所示。

  template<typename V>
  class ValuePublisher: public Publisher
  {
    protected:
      V value;
      StateMonitor *dependencies[];
      
    public:
      V operator~(void) const
      {
        return value;
      }
      
      void operator=(V v)
      {
        value = v;
        for(int i = 0; i < ArraySize(dependencies); i++)
        {
          dependencies[i].notify(&this);
        }
      }
      
      virtual void subscribe(StateMonitor *ptr) override
      {
        const int n = ArraySize(dependencies);
        ArrayResize(dependencies, n + 1);
        dependencies[n] = ptr;
      }
      ...
  };

任何已注册的观察者都获得“依赖关系”,且如果数值发生变化,则调用其 “notify” 方法得到通知。

由于属性是我们引入的唯一与“控件”相关联的,我们将为标准库提供保存指向“控件”链接的最终属性类,即 StdValue(它用到了所有 CWind “控件”类型)。

  template<typename V>
  class StdValue: public ValuePublisher<V>
  {
    protected:
      CWnd *provider;
      
    public:
      void bind(CWnd *ptr)
      {
        provider = ptr;
      }
      
      CWnd *backlink() const
      {
        return provider;
      }
  };

该链接在以后会很有用。

这些是填充 PropertySet 的 StdValue 实例。

StdValue 通讯示意图

StdValue 通讯示意图

在上面提到的类 NotifiableProperty 里,实际上还用到了 StdValue,在方法 “bind” 中,我们将属性值绑定到“控件”(this)。

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      StdValue<V> *property;
    public:
      void bind(StdValue<V> *prop)
      {
        property = prop;
        property.bind(&this);        // +
        property = value();
      }
      ...
  };

自动管理“控件”状态 — EnableStateMonitor

响应某些设置变化的最相关方法是阻塞/解锁其他依赖的“控件”。这种自适应的每个“控件”状态可能取决于若干设置(并非仅限于一种设置)。 为了监控它们,开发了一个特殊的抽象类 EnableStateMonitorBase。

  template<typename C>
  class EnableStateMonitorBase: public StateMonitor
  {
    protected:
      Publisher *sources[];
      C *control;
      
    public:
      EnableStateMonitorBase(): control(NULL) {}
      
      virtual void attach(C *c)
      {
        control = c;
        for(int i = 0; i < ArraySize(sources); i++)
        {
          if(control)
          {
            sources[i].subscribe(&this);
          }
          else
          {
            sources[i].unsubscribe(&this);
          }
        }
      }
      
      virtual bool isEnabled(void) = 0;
  };

“控件”状态由给定观察者监控,并放置在 “control” 字段中。 数组 “sources” 包含影响状态的变化源。 该数组必须在后代类中加以填充。 当我们通过调用 “attach” 将观察者连接到特定的“控件”时,观察者即可订阅所有变化源。 然后,它将通过调用其 “notify” 方法,开始接收有关源变化的通知。

isEnabled 方法将决定是否应“阻塞”或“解锁”控件,但此处将其声明为抽象,并在其后代类中实现。

对于标准库类,已知一种启用/禁用“控件”的机制。 我们用它们来实现特定的类 EnableStateMonitor。

  class EnableStateMonitor: public EnableStateMonitorBase<CWnd>
  {
    public:
      EnableStateMonitor() {}
      
      void notify(void *sender) override
      {
        if(control)
        {
          if(isEnabled())
          {
            control.Enable();
          }
          else
          {
            control.Disable();
          }
        }
      }
  };

在实践中,该类经常在程序中用到,但我们只打算研究一个示例。 若要在 Designer 中创建新对象,或使用修改后的属性,在 “Inspector” 对话框中找到 “Apply” 按钮(为此定义了从 Notifiable<CButton> 派生出的 ApplyButton 类)。

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          ...
        }
      };
  };

如果未定义对象名称,或未选择其类型,则该按钮必须被锁定。 因此,我们通过两个变化源(“publishers”)实现 ApplyButtonStateMonitor:名称和类型。

  class ApplyButtonStateMonitor: public EnableStateMonitor
  {
    // what's required to detect Apply button state
    const int NAME;
    const int TYPE;
    
    public:
      ApplyButtonStateMonitor(StdValue<string> *n, StdValue<int> *t): NAME(0), TYPE(1)
      {
        ArrayResize(sources, 2);
        sources[NAME] = n;
        sources[TYPE] = t;
      }
      
      virtual bool isEnabled(void) override
      {
        StdValue<string> *name = sources[NAME];
        StdValue<int> *type = sources[TYPE];
        return StringLen(~name) > 0 && ~type != -1 && ~name != "Client";
      }
  };

类的构造函数采用两个指向相关属性的参数。 它们会被保存在 “sourcees” 数组之中。 方法 isEnabled 检查名称是否填写,以及是否选择了类型(是否不为 -1)。 如果满足条件,则按钮可以按下。 此外,还会检查名称中是否包含特殊字符串,Client,这是标准库对话框中为客户区域保留的字符串,因此不能在用户元素的名称里出现。

在 inspector 对话框类中,有一个 ApplyButtonStateMonitor 类型的变量,该变量在构造函数中通过指向存储名称和类型的 StdValue 对象链接进行初始化。

  class InspectorDialog: public AppDialogResizable
  {
    private:
      PropertySet props;
      ApplyButtonStateMonitor *applyMonitor;
    public:
      InspectorDialog::InspectorDialog(void)
      {
        ...
        applyMonitor = new ApplyButtonStateMonitor(&props.name, &props.type);
      }

在对话框布局中,名称和类型的属性绑定到相关的“控件”,而观察者绑定到 “Apply” 按钮。

          ...
          _layout<EditProperty> edit("NameEdit", BUTTON_WIDTH, BUTTON_HEIGHT, "");
          edit.attach(&props.name);
          ...
          _layout<ComboBoxProperty> combo("TypeCombo", BUTTON_WIDTH, BUTTON_HEIGHT);
          combo.attach(&props.type);
          ...
          _layout<ApplyButton> button1("Apply", BUTTON_WIDTH, BUTTON_HEIGHT);
          button1["enable"] <= false;
          applyMonitor.attach(button1.get());

我们已经知道 applyMonitor 对象中的 “attach” 方法,而 _layout 对象中的 “attach” 是新事物。 在我们的第二篇文章中已深入讨论了 _layout 类,与该版本相比,“attach” 方法是唯一的变化。 此中间方法仅对检查器对话框内 _layout 对象生成的控件调用 “bind”。

  template<typename T>
  class _layout: public StdLayoutBase
  {
      ...
      template<typename V>
      void attach(StdValue<V> *v)
      {
        ((T *)object).bind(v);
      }
      ...
  };

应当注意,所有属性“控件”(包括本示例中的 EditProperty 和 ComboBoxProperty)都是 NotifiableProperty 类的后代,在该类中,存在一个 “bind” 方法,可将“控件”与存储该控件相关属性的 StdValue 变量绑定。 因此,inspector 窗口中的“控件”实际上已与相关属性绑定,而后者又由观察者 ApplyButtonStateMonitor 监视。 一旦用户更改了两个字段之一的数值,它就会显示在 PropertySet 中(记住 NotifiableProperty 中的 ON_CHANGE和ON_END_EDIT 事件应答程序 onEvent)并通知已注册的观察者,包括 ApplyButtonStateMonitor。 这样会自动更改当前按钮的状态。

在 inspector 对话框中,我们将需要若干个监视器,它们以类似的方式监视“控件”的状态。 我们将在用户手册的某个章节里讲述阻塞的特殊规则。

StateMonitor 类

StateMonitor 类

好了,我们在检查器对话框中表示要创建的对象,和“控件”的所有属性的最终相关性。

  • name — EditProperty, 字符串;
  • type — ComboBoxProperty, 整数, 受支持元素列表中的类型编号;
  • width — SpinEditPropertySize, 整数, 像素;
  • height — SpinEditPropertySize, 整数, 像素;
  • style — ComboBoxProperty, 等于枚举之一值(取决于元素类型)的整数:VERTICAL_ALIGN(CBoxV),HORIZONTAL_ALIGN(CBoxH)和 ENUM_ALIGN_MODE(CEdit);
  • text — EditProperty, 字符串;
  • background color — ComboBoxColorProperty, 列表中的颜色值;
  • boundary alignment — AlignCheckGroupProperty,位掩码,独立标志组(ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT); 和
  • indents — 四个 SpinEditPropertyShort, 整数;

与“简单的” SpinEditProperty、ComboBoxProperty、CheckGroupProperty 等提供的基本实现相比,某些“属性”元素的类名称更加专业化,即功能扩展。 从用户手册中可以清楚地了解它们的用途。

为了准确、清晰地显示这些“控件”,对话框标记当然包括其他容器和数据标签。 完整代码可在附件中找到。

处理事件

事件映射中定义了所有“控件”的可应答事件:

  EVENT_MAP_BEGIN(InspectorDialog)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_END_EDIT, cache, EditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditPropertyShort)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxColorProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, AlignCheckGroupProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, ApplyButton)
    ...
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache) // default (stub)
  EVENT_MAP_END(AppDialogResizable)

为了在高速缓存中提高处理事件的效率,已采取了一些特殊步骤。 我们在第二篇文章中引入的宏 ON_EVENT_LAYOUT_CTRL_ANY 和 ON_EVENT_LAYOUT_CTRL_DLG,其操作是基于系统从参数 lparam 里接收到的唯一编号,据其在缓存数组里搜索“控件”。 于此同时,基本缓存实现遍历数组执行线性搜索。

为了加快此过程,将 buildIndex 方法添加到 MyStdLayoutCache 类(StdLayoutCache 的后代)之中,在 Inspector 中存储和使用该类的一个实例。 其中实现的便捷索引功能基于标准库的特殊功能,可为所有元素分配唯一编号。 在方法 CAppDialog::Run 中,我们已知里面有一个随机数,即 m_instance_id,自该数字开始窗口创建的所有图表对象都进行编号。 依此方式,我们就可知道能得到的数值范围。 减去 m_instance_id,每次随事件携带的 lparam 数值,直接变为对象的编号。 不过,该程序在图表中创建的对象要比缓存中存储的对象多得多,因为许多“控件”(以及与此相关的窗口本身,是为框架、标题、最小化按钮、等等的聚集)由多个低级对象构成。 因此,缓存中的索引永远不会与对象标识符减去m_instance_id一致。 故此,我们必须分配一个特殊的索引数组(其大小等于窗口里对象的总数),并取高速缓存中可用的“实际”控件数为其编写序号。 结果就是,根据间接寻址的原理,可立即提供访问。

仅在基本 CAppDialog::Run 实现被分配了唯一编号之后再填充数组,但要在 OnInit 应答程序完成操作之前。 为此目的,最好的解决方案是把方法 Run 设为虚拟(在标准库中不是这样),并在 InspectorDialog 中重写它,举例如下所示。

  bool InspectorDialog::Run(void)
  {
    bool result = AppDialogResizable::Run();
    if(result)
    {
      cache.buildIndex();
    }
    return result;
  }

方法 buildIndex 本身很简单。

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      InspectorDialog *parent;
      // fast access
      int index[];
      int start;
      
    public:
      MyStdLayoutCache(InspectorDialog *owner): parent(owner) {}
      
      void buildIndex()
      {
        start = parent.GetInstanceId();
        int stop = 0;
        for(int i = 0; i < cacheSize(); i++)
        {
          int id = (int)get(i).Id();
          if(id > stop) stop = id;
        }
        
        ArrayResize(index, stop - start + 1);
        ArrayInitialize(index, -1);
        for(int i = 0; i < cacheSize(); i++)
        {
          CWnd *wnd = get(i);
          index[(int)(wnd.Id() - start)] = i;
        }
      ...
  };

现在,我们可以编写该方法的快速实现,从而可按编号搜索“控件”。

      virtual CWnd *get(const long m) override
      {
        if(m < 0 && ArraySize(index) > 0)
        {
          int offset = (int)(-m - start);
          if(offset >= 0 && offset < ArraySize(index))
          {
            return StdLayoutCache::get(index[offset]);
          }
        }
        
        return StdLayoutCache::get(m);
      }

但对于 Inspector 的内部结构来说已经足够了。

这是正在运行的程序里窗口的外观。

 Inspector 对话框和 Form 设计器

Inspector 对话框和 Form 设计器

除了属性,我们还可以在此处看到一些未知的元素。 它们都将在后面讲述。 现在我们来看一下按钮 Apply。 用户设置属性值之后,可按此按钮在 “Designer” 窗体中生成请求的对象。 派生自 Notifiable 的类,可令其自身的 onEvent 方法处理。

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          Properties p = inspector.getProperties().flatten();
          designer.inject(p);
          ChartRedraw();
          return true;
        }
        return false;
      };
  };

应当注意,变量检查器和设计器是全局对象,分别带有检查器对话框和设计器窗体。 在其程序界面中,Inspector 拥有方法 getProperties,可提供当前的属性集合,PropertySet,如上所述:

    PropertySet *getProperties(void) const
    {
      return (PropertySet *)&props;
    }

PropertySet 可将自身封包为一个平面(常规)结构,Properties,以便传递给 Designer 的方法,inject。 在此,我们去看看 “Designer” 窗口。

Designer(设计器)

除了进行额外的检查之外,方法 “inject” 的本质与第二篇文章的结尾类似:Form 将目标容器放入布局堆栈中(在第二篇文章中是静态设置的,即始终是相同的),并在其中依据所传递属性值生成元素。 在新的窗体里,可单击鼠标来选择所有元素,从而更换插入的上下文。 甚或,这种单击会启动将所选元素的属性转换至 Inspector。 因此,会出现编辑已创建对象的属性,并用相同的 “Apply” 按钮更新它们。 设计器通过比较元素的名称和类型来判断用户是要引入新元素,还是要编辑已有元素。 如果 Designer 缓存中已有此种组合,则意味着进行编辑。

如何添加一个新元素,通常就是这样的。

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        ...
      }
      else
      {
        CBox *box = dynamic_cast<CBox *>(cache.getSelected());
        
        if(box == NULL) box = cache.findParent(cache.getSelected());
        
        if(box)
        {
          CWnd *added;
          StdLayoutBase::setCache(cache);
          {
            _layout<CBox> injectionPanel(box, box.Name());
            
            {
              AutoPtr<StdLayoutBase> base(getPtr(props));
              added = (~base).get();
              added.Id(rand() + ((long)rand() << 32));
            }
          }
          box.Pack();
          cache.select(added);
        }
      }

变量 “cache” 已在 DesignerForm 里定义,并包含一个从 StdLayoutCache 派生的 DefaultStdLayoutCache 类对象(在前面的文章中已将述)。 StdLayoutCache 能偶用方法 “get” 按名称查找对象。 如果不存在,则意味着这是一个新对象,且 Designer 尝试检测用户选择的当前容器。 为此目的,在新类 DefaultStdLayoutCache 中实现了方法 getSelected。 如何精确执行选择,我们稍后会看到。 在此必须注意,实现的新元素只能放置在一个容器里(在我们的情况下,用的是 CBox 容器)。 如果此刻没有选择一个容器,该算法将调用 findParent 来检测父容器,并将其用作目标容器。 定义插入位置后,含有嵌套模块的常规标记方案开始起作用。 在外部模块里,创建带有目标容器的对象 _layout,然后在其内部以字符串形式生成对象:

  AutoPtr<StdLayoutBase> base(getPtr(props));

所有属性都传递给助手方法 getPtr。 它可以创建所有受支持类型的对象,但是为了简单起见,我们仅展示它在某些对象的样子。

    StdLayoutBase *getPtr(const Properties &props)
    {
      switch(props.type)
      {
        case _BoxH:
          {
            _layout<CBoxH> *temp = applyProperties(new _layout<CBoxH>(props.name, props.width, props.height), props);
            temp <= (HORIZONTAL_ALIGN)props.style;
            return temp;
          }
        case _Button:
          return applyProperties(new _layout<CButton>(props.name, props.width, props.height), props);
        case _Edit:
          {
            _layout<CEdit> *temp = applyProperties(new _layout<CEdit>(props.name, props.width, props.height), props);
            temp <= (ENUM_ALIGN_MODE)LayoutConverters::style2textAlign(props.style);
            return temp;
          }
        case _SpinEdit:
          {
            _layout<SpinEditResizable> *temp = applyProperties(new _layout<SpinEditResizable>(props.name, props.width, props.height), props);
            temp["min"] <= 0;
            temp["max"] <= DUMMY_ITEM_NUMBER;
            temp["value"] <= 1 <= 0;
            return temp;
          }
        ...
      }
    }

模板化的对象 _layout 是由 GUI 元素的预定义类型所创建,它用到了我们所熟知的 MQL 标记静态描述。 对象 _layout 启用重载运算符 “<=” 来定义属性,实际上,这就是 CBoxH 如何填充 HORIZONTAL_ALIGN 样式,文本字段或轮转器范围的 ENUM_ALIGN_MODE 样式。 其他一些常规属性(例如缩进、文本和颜色)的设置委托给辅助方法 applyProperties(您可以在源代码中找到有关它的更多详细信息)。

    template<typename T>
    T *applyProperties(T *ptr, const Properties &props)
    {
      static const string sides[4] = {"left", "top", "right", "bottom"};
      for(int i = 0; i < 4; i++)
      {
        ptr[sides[i]] <= (int)props.margins[i];
      }
      
      if(StringLen(props.text))
      {
        ptr <= props.text;
      }
      else
      {
        ptr <= props.name;
      }
      ...
      return ptr;
    }

如果按名称在缓存里能找到对象,则会发生以下情况(以简化形式):

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        CWnd *sel = cache.getSelected();
        if(ptr == sel)
        {
          update(ptr, props);
          Rebound(Rect());
        }
      }
      ...
    }

助手方法 “update” 将属性从结构 “props” 转移到已找到的 ptr 对象之中。

    void update(CWnd *ptr, const Properties &props)
    {
      ptr.Width(props.width);
      ptr.Height(props.height);
      ptr.Alignment(convert(props.align));
      ptr.Margins(props.margins[0], props.margins[1], props.margins[2], props.margins[3]);
      CWndObj *obj = dynamic_cast<CWndObj *>(ptr);
      if(obj)
      {
        obj.Text(props.text);
      }
      
      CBoxH *boxh = dynamic_cast<CBoxH *>(ptr);
      if(boxh)
      {
        boxh.HorizontalAlign((HORIZONTAL_ALIGN)props.style);
        boxh.Pack();
        return;
      }
      CBoxV *boxv = dynamic_cast<CBoxV *>(ptr);
      if(boxv)
      {
        boxv.VerticalAlign((VERTICAL_ALIGN)props.style);
        boxv.Pack();
        return;
      }
      CEdit *edit = dynamic_cast<CEdit *>(ptr);
      if(edit)
      {
        edit.TextAlign(LayoutConverters::style2textAlign(props.style));
        return;
      }
    }

现在我们回到在窗体里选择 GUI 元素的问题。 由于处理了用户发起的事件,它已由缓存对象解决。 在 StdLayoutCache 类中保留了 onEvent 应答程序,以便利用宏 ON_EVENT_LAYOUT_ARRAY 连接映射的图表事件:

  EVENT_MAP_BEGIN(DesignerForm)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
    ...
  EVENT_MAP_END(AppDialogResizable)

这会将所有缓存元素的鼠标点击发送到我们在派生类 DefaultStdLayoutCache 中定义的应答程序 onEvent。 指向通用窗口类型 CWnd 的 “selected” 指,CWnd,已在类中创建,它必须由 onEvent 应答程序填充。

  class DefaultStdLayoutCache: public StdLayoutCache
  {
    protected:
      CWnd *selected;
      
    public:
      CWnd *getSelected(void) const
      {
        return selected;
      }
      
      ...
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          highlight(selected, CONTROLS_BUTTON_COLOR_BORDER);
          
          CWnd *element = control;
          if(!find(element)) // this is an auxiliary object, not a compound control
          {
            element = findParent(element); // get actual GUI element
          }
          ...
          
          selected = element;
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", element.Id());
          EventChartCustom(CONTROLS_SELF_MESSAGE, ON_LAYOUT_SELECTION, 0, 0.0, NULL);
          return true;
        }
        return false;
      }
  };

在窗体里选择的可见元素,会加上红色边框,作为简易的“高亮”方法(即 ColorBorder)。 处理器首先取消先前选择的元素(设置边框颜色 CONTROLS_BUTTON_COLOR_BORDER),然后找到与所单击图表对象对应的缓存元素,并将其指针保存在 “selected” 变量中。 最后,新选择的对象用红色边框标记,并发送 ON_LAYOUT_SELECTION 事件至图表。 它会通知 Inspector,在窗体里已选择了一个新元素,故此它应该在 Inspector 对话框中显示其属性。

在 Inspector 里,此事件在 OnRemoteSelection 应答程序中被拦截,该应答程序从 Designer 请求所选择对象的链接,并通过函数库的标准 API 读取该对象的所有属性。

  EVENT_MAP_BEGIN(InspectorDialog)
    ...
    ON_NO_ID_EVENT(ON_LAYOUT_SELECTION, OnRemoteSelection)
  EVENT_MAP_END(AppDialogResizable)

以下是方法 OnRemoteSelection 的开头部分。

  bool InspectorDialog::OnRemoteSelection()
  {
    DefaultStdLayoutCache *remote = designer.getCache();
    CWnd *ptr = remote.getSelected();
    
    if(ptr)
    {
      string purename = StringSubstr(ptr.Name(), 5); // cut instance id prefix
      CWndObj *x = dynamic_cast<CWndObj *>(props.name.backlink());
      if(x) x.Text(purename);
      props.name = purename;
      
      int t = -1;
      ComboBoxResizable *types = dynamic_cast<ComboBoxResizable *>(props.type.backlink());
      if(types)
      {
        t = GetTypeByRTTI(ptr._rtti);
        types.Select(t);
        props.type = t;
      }
      
      // width and height
      SpinEditResizable *w = dynamic_cast<SpinEditResizable *>(props.width.backlink());
      w.Value(ptr.Width());
      props.width = ptr.Width();
      
      SpinEditResizable *h = dynamic_cast<SpinEditResizable *>(props.height.backlink());
      h.Value(ptr.Height());
      props.height = ptr.Height();
      ...
    }
  }

从 Designer 缓存接收选定对象的 ptr 链接后,该算法查找其名称,并从窗口标识符中清除该名称(CAppDialog 类中的 m_instance_id 字段是所有名称的前缀,以避免不同窗口对象之间的冲突,因为我们会有 2 个),并将其写入与名称相关的“控件”之中。 您应当注意,在此我们用的是属性 StdValue<string> 名称到“控件”的反向链接(backlink())。 甚至,由于我们是从内部修改字段的,因此与其变化相关的事件(有些情况是由用户发起的更改)不会生成; 因此,它还需要将新值写入 PropertySet (props.name) 的相关属性。

从技术上讲,以 OOP 的角度来看,为属性“控件”的每种类型重写其虚拟的修改方法,并自动更新与其链接的 StdValue 实例更加正确。 在此举例,这是 CEdit 的实现方式。

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      ...
      virtual bool OnSetText(void) override
      {
        if(CEdit::OnSetText())
        {
          if(CheckPointer(property) != POINTER_INVALID) property = m_text;
          return true;
        }
        return false;
      }    
  };

然后,使用 Text() 方法更改字段内容将导致随后调用 OnSetText,并自动更新属性。 然而,对于复合控件(例如 CCheckGroup),这样做不是很方便。 因此,我们倾向于一种更实际的实现。

与此类似,使用“控件”的反向链接,我们就能更新 Designer 里所选择对象的高度、宽度、类型和其他属性字段中的内容。

为了识别受支持的类型,我们有一个枚举,可以基于特殊变量 _rtti 来检测其元素,我们在之前的几篇文章里,将该变量添加到最底层的 CWnd 类之中,并在所有派生类中用特殊的类名填充它。

快速入门指南

“Inspector” 对话框包含各种类型的输入字段,其中包含当前对象(在 Designer 中选择),或要创建的对象属性。

强制填写的字段是名称(字符串)和类型(将在下拉列表中选择)。

宽度和高度字段允许以像素为单位定义对象大小。 只是,如果在下面指定了特定的拉伸模式,则不会考虑这些设置:例如,与左右边界绑定表示填满容器的宽度。 鼠标点击高度或宽度字段同时按住 Shift 键,可将属性重置为默认值(宽度 100,和高度 20)。

改进了所有 SpinEdit 类型的“控件”(不仅是大小属性),其方式是鼠标在“控件”内部移动(拖动但不放下)的情况下,按住鼠标键向左或向右移动能快速按比例更改 “spinner” 像素所覆盖的距离。 这样做是为了便于编辑,因为按小按钮并不是很方便。 任何程序均可更改来自 ControlsPlus 文件夹中的“控件”。

含有内容对齐样式(Style)的下拉列表仅适用于 CBoxV、CBoxH 和 CEdit 的元素(所有其他类型均被阻塞)。 对于 CBox 容器,所有对齐模式 ("center", "justify", "left/top", "right/bottom", 和 "stack") 均允许。 对于 CEdit,只有与 ENUM_ALIGN_MODE("center", "left", 和 "right")相对应的才起作用。

字段 "Text" 定义 CButton 的标题,CLabel 或 CEdit 的内容。 对于其他类型,该字段则被禁用。

下拉列表 “Color” 旨在从 Web 颜色列表中选择背景颜色。 它仅适用于 XBox、XBox、Button 和 CREdit。 其他类型的“控件”是复合控件,需要更复杂的技术来为其更新所有组件的颜色,故我们决定不支持为它们着色。 为了选择颜色,修改了 CListView 类。 在其内添加了特殊的 “color” 模式,在该模式下,列表项的值被解释为颜色代码,并且每项的背景均以相关颜色绘制。 此模式由 SetColorMode 方法启用,并在新类 ComboBoxWebColors(来自 Layouts 文件夹的 ComboBoxResizable 的特殊化)中调用。

目前无法选择函数库 GUI 的标准颜色,因为定义默认颜色存在问题。 对于我们来说,重要的是要知道每种“控件”类型的默认颜色,以便在用户未选择任何特定颜色的情况下,在列表中不要把它显示为已选颜色。 最简单的方法是创建一个特定类型的空“控件”,并在其中读取 ColorBackground 属性,但它只能在数量非常有限的“控件”时使用。 问题在于,通常不会在类构造函数中分配颜色,而是在 Create 方法中分配颜色,该方法会引发许多不必要的初始化,包括在图表中创建实际对象。 当然,我们不需要任何不必要的对象。 甚至,许多复合对象的背景色就是背景底色,而并非来自基准“控件”。由于考虑到这些细微差别的复杂性,我们决定考虑在标准库“控件”的任何类别中不选择默认颜色。这意味着它们不能包含在列表中,否则的话,用户可能会选择这种颜色,但在 Inspector 中看不到任何对其选择的确认。 Web 颜色和标准 GUI 颜色的列表在 LayoutColors.mqh 文件中提供。

若要将颜色重置为默认值(每种“控件”类型不同),应在列表中选择与 clrNONE 相关的第一个 “empty” 项。

一组独立切换器中的标志 Alignment 与枚举 ENUM_WND_ALIGN_FLAGS 两边侧的对齐方式相对应,加上特殊模式 WND_ALIGN_CONTENT,这在第二篇文章中已有过介绍,且它仅适用于容器。 如果您按下切换器时按住 Shift 键,程序将同步切换 ENUM_WND_ALIGN_FLAGS 的所有 4 个标志。 如果启用了该选项,则还应启用其他选项,反之亦然;如果禁用了该选项,则将重置其他选项。 一键切换整个组,WND_ALIGN_CONTENT 除外。

“Spinners” 边距定义元素的缩进,该缩进与该元素所在的容器矩形的边界有关。 字段顺序:左,上,右和下。 按住 Shift 键的同时单击任意字段,可将所有字段快速重置为零。 按住 Ctrl 键的同时单击含有所需值的字段,可以轻松地将所有字段设置为相等 — 这导致该值被复制到其它 3 个字段中。

我们已经知道 “Apply” 按钮 — 它应用所做的更改,这将导致在Designer中创建新的“控件”或修改现有控件。

新对象将被插入到所选容器对象,或包含所选“控件”的容器当中(如果已选择“控件”)。

若要在 Designer 中选择元素,应使用鼠标单击它。 所选元素以红色框高亮显示。 唯一的例外是 CLabel — 它不支持此功能。

插入后立即自动选择新元素。

只有容器 CBoxV 或 CBoxH 能插入到一个空对话框之中,且不必预先选择客户区。 默认情况下,第一个也是最大的容器会拉伸填满整个窗口。

重复单击已选择的元素则会删除请求。 删除仅在用户确认后才会实际发生。

两个位置的按钮 TestMode 在 Designer 的两个操作模式之间切换。 默认情况下,它是未按下的,测试模式是禁用的,Designer 界面编辑是可操作的 — 用户可以通过单击鼠标来选择元素,或将其删除。 按下时,将启用测试模式。 于此同时,对话框的操作方式与实际程序中的操作大致相同,而布局编辑和元素选择被禁用。

按钮 Export 允许将 Designer 界面的当前配置保存为 MQL 布局。 文件名以前缀 layout 开头,并包含当前的时间掩码和扩展名 txt。 如果您在按 “Export” 时按住 Shift 键,则窗体的配置将以二进制格式(而不是文本)保存到其自身格式的文件中,扩展名为 mql。 这很方便,因为您可以中断布局设计过程,然后过一会儿再恢复它。 若要加载二进制布局 mql 文件,使用同一个 “Export” 按钮,条件是元素的表单和缓存为空,这在启动程序后会立即执行。 当前版本始终尝试导入文件 layout.mql。 如果您愿意,可以在输入或 MQL 中实现文件选择。

在 “Inspector” 对话框的上部,有一个下拉列表,其中包含在 Designer 中创建的所有元素。 在列表中选择一个元素会导致在 Designer 中自动选择并高亮显示该元素。 反之亦然,选择表单中的元素会在列表中成为当前元素。

现在,在编辑中,可能会出现两种类别的错误:这些是可以通过分析 MQL 布局而更正的错误,以及更严重的。 前者包括设置组合,其中“控件”或容器超出了窗口或父容器的边界。 在这种情况下,通常会中断鼠标选择它们,且您只能用 Inspector 中的选择器启用它们。 您可以通过分析文本 MQL 标记来找出哪些属性完全是假的 — 只需按 Export 即可获取其当前状态。 在分析了标记之后,您应该更正 Inspector 中的属性,然后还原表单的正确视图。

程序的目前版本旨在验证概念,且未在源代码中检查所有参数组合,在重新计算自适应容器的大小时可能会发生这种情况。

第二类错误包括错误地将元素插入到错误容器的情况。 在此情况下,您只能删除该元素,然后将其再次添加到别的位置。

建议定期以二进制格式保存表单(按 “Export” 按钮,同时按住 Shift 键),如此即可在出现无法解决的问题时,您可以以最后的正确配置继续工作。

我们来研究一些使用该程序的示例。

示例

首先,我们尝试在 Designer 中重现 Inspector 结构。 在下面的动画图示里,您可从添加四个上部字符串和字段来设置名称、类型和宽度开始的全过程。 用到了不同类型的“控件”、对齐方式、配色方案。 包含字段名称的标签是用 CEdit 的输入字段形成的,因为 CLabel 只具有非常有限的功能(特别是它不支持文本对齐和背景颜色)。 然而,在 Inspector 中未有“只读”属性设置。 因此,将标签表示为不可编辑的唯一方法是为其分配灰色背景(这只是纯粹的视觉效果)。 在 MQL 代码里,此类 CEdit 对象必须确认要相应地调整,即,切换到“只读”模式。 这正是我们在 Inspector 本身中所做的。

编辑窗体

编辑窗体

编辑窗体清晰地表明了标记技术的适应性,并作为一种外部表示形式,它唯与 MQL 标记绑定。 您可以随时按 Export 按钮,查看生成的 MQL 代码。

在最终版本里,我们会得到一个对话框,该对话框与 Inspector 窗口在理论上是完全对应的(某些细节除外)。

在 Designer 中还原 Inspector 对话框标记

在 Designer 中还原 Inspector 对话框标记

不过,应当注意的是,在 Inspector 中,许多“控件”类都是非标准的,因为它们是从某个 x-属性继承而来的,且表示附加的算法工具。 不过,在我们的示例中,仅用到“控件”的标准类(ControlsPlus)。 换言之,结果布局始终只包含程序的外部表示形式,和“控件”的标准行为。 跟踪元素的状态,并针对其变化响应(包括可能的类自定义)编写代码是程序员的特权。 创建的系统能够像是在常规 MQL 中一样更改 MQL 标记中的工件。 也就是,您可以将 ComboBox 替换为 ComboBoxWebColors。 但是,无论如何,必须用 #include 指令将布局中提到的所有类都包含在项目中。

用 “Export” 命令将上面的对话框(Inspector 复本)保存到文本文件和二进制文件当中 — 两者都附带于此,分别名为 layout-inspector.txt 和 layout-inspector.mql。

分析完文本文件后,您无需绑定算法或数据即可感知 Inspector 标记。

基本上,将标记导出到文件后,其内容可以插入到任何项目之中,包括布局系统的头文件,和所有用到的 GUI 类。 结果就是,我们得到了一个可操作界面。 特别是,含有空 DummyForm 对话框的项目已附加到其中。 如果您愿意,可以在其中找到 CreateLayout,并将 MQL 标记插入其中,以便在 Designer 中进行初步准备。

对于 layout-inspector.txt,也可以轻松完成此操作。 我们将把这个文件的全部内容复制到剪贴板中,并插入到 CreateLayout 方法中的文件 DummyForm.mqh 当中,这里有注释 "// insert exported MQL-layout here"。

请注意,提及的对话框尺寸以布局文本表示(在本例中为 200*350),为此它之前已被创建。 因此,应在以 _layout<DummyForm> dialog(this...) 形式创建对象的代码之后,和已复制的布局之前,将以下代码插入源代码 CreateLayout 之中:

  Width(200);
  Height(350);
  CSize sz = {200, 350};
  SetSizeLimit(sz);

这将为所有“控件”提供足够的空间,并且不允许缩小对话框。

导出时,我们不会自动生成相关的片段,因为布局可能仅代表对话框的一部分,或者最终被其他类别的窗口和容器使用,而这些窗口和容器也许没有这些方法。

如果现在编译并运行示例,我们将获得非常类似的 Inspector 副本。 但仍存在差异。

恢复 Inspector 界面

恢复 Inspector 界面

首先,所有下拉列表都是空的,因此它们不起作用。 没有调整 “spinners”,因此它们也不起作用。 对齐标记组在视觉上是空的,因为我们尚未在布局中生成任何复选框,但是存在相关的“控件”,甚至还有 5 个隐藏的复选框,这些复选框是由标准组件库依据初始大小生成的“控件”(您可以在图表对象列表,命令“对象列表”中查看所有这些对象)。

其次,确实没有带有缩进值的 “spinners” 组:我们没有将其转移到表单中,因为它是由一个布局对象创建的,并在 Inspector 中作为数组。 我们的编辑器无法执行此类操作。 我们可以创建 4 个独立的元素,但随后我们必须在代码里如同其它一样做出调整。

按下任何“控件”,窗体都会将其名称、类和标识符输出到日志中。

我们还可以将二进制文件 layout-inspector.mql(已将其初步重命名为 layout.mql)加载到 Inspector,并继续进行编辑。 为此目的,运行主项目并按 Export 足矣,就如同早前窗体为空。

请注意,出于阐释目的,Designer 会为带有列表或分组的所有“控件”生成一定数量的数据,并设置 “spinners” 的范围。 因此,当切换到 TestMode 时,我们就可以使用元素。 伪数据的大小在 Designer 窗体中由宏 DUMMY_ITEM_NUMBER 定义,默认为 11。

现在,我们看看交易面板如何出现在 Designer 当中。

交易面板布局:彩色立方体交易面板

交易面板布局:彩色立方体交易面板

它并无虚伪的超级功能,但关键在于可以根据特定交易者的偏好轻松地在根本上进行修改。 此窗体如同前一种,使用着色容器来更容易地看到它们的布置。

我们应该再次有所保留,意思是仅在这里出现。 在 Designer 输出中,我们仅得到负责生成窗口和“控件”初始状态的 MQL 代码。像往常一样,所有计算算法,对用户操作的响应,防止输入错误数据,以及发送交易指令的操作都必须手动编程。

在该布局中,某些类型的“控件”应替换为更合适的内容。 故此,挂单的到期日由日历所示,且它不支持时间输入 。 所有下拉列表必须填充相关选项。 举例来说,止损价位可用不同单位输入,例如价格、以点数为单位的距离、盈/亏资金百分比、或绝对值,同时可以将交易量设置为固定、货币或可用保证金百分比,以及尾随等若干种算法之一。

此标记附带于此,分为两个 layout-color-cube-trade-panel 文件:文本文件和二进制文件。 可以将前一个插入到空窗体(例如 DummyForm)当中,并附带数据和事件处理。 后者可以加载到 Designer 当中,并进行编辑。 但是请记住,图形编辑器不是必需的。 标记也可在其文本表述中进行调整。 编辑器的唯一优点是我们可以进行设置,并即时查看变化。 然而,它仅支持最基本的功能。

结束语

在这篇论文中,我们研究了一个简单的编辑器程序,以交互方式开发基于 MQL 标记技术的图形界面。 呈现的实现仅包括基本功能,但这些功能仍足以展现该概念的可操作性,以及对其他类型“控件”的进一步扩展,对各种属性的更完整支持,GUI 组件的其他函数库,以及编辑机制。 特别是,编辑器仍然缺少取消操作、将元素插入容器中任何位置的功能(即,不光是将它们添加到已存在的“控件”列表的末尾)、分组操作、从剪贴板复制并粘贴的功能、等等。 然而,开源代码令您可以补充和调整该技术,从而满足您的需求。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/7795

附加的文件 |
MQL5GUI3.zip (112.66 KB)
DoEasy 函数库中的时间序列(第四十三部分):指标缓冲区对象类 DoEasy 函数库中的时间序列(第四十三部分):指标缓冲区对象类

本文研究开发指标缓冲区对象类,其为抽象缓冲区对象的衍生类,从而可简化声明,并可操控指标缓冲区,同时创建基于 DoEasy 库的自定义指标程序。

监视多币种的交易信号(第五部分):复合信号 监视多币种的交易信号(第五部分):复合信号

在第五篇文章是与创建交易信号监视器有关,我们将研究复合信号,并实现必要的功能。 在早前版本里,我们用到了简单信号,例如 RSI、WPR 和 CCI,并且还引入了自定义指标的可能性。

无需 DLL 的原生 MT4/MT5 推特(Twitter)客户端 无需 DLL 的原生 MT4/MT5 推特(Twitter)客户端

是否曾想访问推文和/或在推特(Twitter)上发布您的交易信号? 无需更多搜索,这些持续更新的系列文章将为您展示如何无需任何 DLL 的情况下进行操作。 畅想 MQL 实现 Twitter API 的旅程。 在第一部分中,我们将在访问 Twitter API 时遵循身份验证和授权的荣耀之路。

原生推特(Twitter)客户端:第二部 原生推特(Twitter)客户端:第二部

一款以 MQL 类实现的推特(Twitter)客户端,允许您发送带照片的推文。 您只需要包含一个独立的包含文件,之后您即可将所有出色的图表和信号发作推文。