繁簡切換您正在訪問的是FX168財經網,本網站所提供的內容及信息均遵守中華人民共和國香港特別行政區當地法律法規。

FX168财经网>人物频道>帖子

MQL 作为 MQL 程序图形界面的标记工具。 第二部分

作者/Tango 2020-07-29 19:00 0 来源: FX168财经网人物频道

在第一部分当中,我们研究了以 MQL 来描述 MQL 程序图形界面布局的基本原理。 为了实现它们,我们必须创建一些类,它们直接负责初始化界面元素,在一个通用层次结构中将它们组合,并调整其属性。 现在,我们将介绍一些更复杂的示例,且为避免受到实际事物所干扰,仅简要关注我们的标准组件库,我们将利用这些库来构建示例。

自定义标准控件库

在早期阐述窗口界面 OLAP 的文章里,也基于标准库和 CBox 容器,我们必须更正标准库的组件。 正如事实证明,为了集成提议的布局系统,控件库需要更多的调整 — 部分涉及功能的扩展,部分涉及错误纠正。 由于此原因,我们决定为所有类制作完整副本(版本分支),并将它们放在 ControlsPlus 文件夹中,以后仅会利用它们操控。

此为主要更新。

实际上,在所有类中,私密访问级别都会修改为受保护级别,从而确保函数库的可扩展性。

为了方便调试包含 GUI 元素的项目,在 CWind 类里添加字符串区域 _rtti,并用 RTTI 宏在每个派生类的构造函数中用特定类的名称填充该区域。

  #define RTTI _rtti = StringFormat("%s %d", typename(this), &this);

它能够在调试器窗口中看到因由基类链接而取消引用的真实对象类(在这种情况下,调试器将显示基类)。

类 CWnd 中元素的区域和对齐方式的信息,可利用两个新的重载方法进行访问。 甚至,其变化能够分别修改对齐和区域。

    ENUM_WND_ALIGN_FLAGS Alignment(void) const
    {
      return (ENUM_WND_ALIGN_FLAGS)m_align_flags;
    }
    CRect Margins(void) const
    {
      CRectCreator rect(m_align_left, m_align_top, m_align_right, m_align_bottom);
      return rect;
    }
    void Alignment(const int flags)
    {
      m_align_flags = flags;
    }
    void Margins(const int left, const int top, const int right, const int bottom)
    {
      m_align_left = left;
      m_align_top = top;
      m_align_right = right;
      m_align_bottom = bottom;
    }

方法 CWnd::Align 依据所有对齐方式的预期行为进行了重写。 如果定义了拉伸(两个维度都易于发生),则标准实现无法确保平移至预定义区域的边界。

方法 DeleteAll 现已被添加到 CWndContainer 类之中,可在删除容器时删除所有子元素。 如果指向所传递“控件”的指针包含一个容器对象,则从 Delete(CWnd * control) 里调用它。

在 CWndClient 类的不同位置,我们添加了调节滚动条可见性的代码,这滚动条可能会由于调整大小而发生变化。

现在,为界面元素分配标识符时,类 CAppDialog 会考虑窗口的 instance_id。 如果不进行此调整,则当不同窗口取了相同名称时,控件会发生冲突(彼此影响)。

在“控件”组(即 CRadioGroup,CCheckGroup 和 CListView)中,对于 “rubber” 子类,将 Redraw 方法设为虚拟的,以便能够正确响应大小调整。 我们还略微修正了对其子元素宽度的重新计算。

出于相同目的,虚拟方法 OnResize 已添加到 CDatePicker、CCheckBox 和 CRadioButton 类中。 在 CDatePicker 类中,弹出日历的低优先级错误已被修复(传递给它的鼠标单击)。

方法 CEdit::OnClick 不会“吃掉”鼠标单击。

甚至,我们之前已经开发了一些“控件”类,它们支持调整大小;并在该特定项目中扩展了 “rubber” 类的数量。 它们的文件位于 Layouts 文件夹中。

  • ComboBoxResizable
  • SpinEditResizable
  • ListViewResizable
  • CheckGroupResizable
  • RadioGroupResizable

应当提醒的是,某些“控件”(例如按钮或输入字段)原生支持拉伸。

类的示意图中给出了标准元素库的一般结构,其中考虑了支持 “rubber” 性质和第三方容器的适配版本。

控件的层次

控件的层次


生成和缓存元素

迄今为止,在对象窗口内元素自动由实例构造。 实际上,这些是“假人”,之后由诸如 Create 之类的方法进行初始化。 GUI 元素布局系统可独立创建这些元素,而不必从窗口中获取它们。 为此,您只需要一个存储单元。 我们将其命名为 LayoutCache。

  template<typename C>
  class LayoutCache
  {
    protected:
      C *cache[];   // autocreated controls and boxes
      
    public:
      virtual void save(C *control)
      {
        const int n = ArraySize(cache);
        ArrayResize(cache, n + 1);
        cache[n] = control;
      }
      
      virtual C *get(const long m)
      {
        if(m < 0 || m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual C *get(const string name) = 0;
      virtual bool find(C *control);
      virtual int indexOf(C *control);
      virtual C *findParent(C *control) = 0;
      virtual bool revoke(C *control) = 0;
      virtual int cacheSize();
  };

实际上,这是一个基类指针的数组(所有元素共用),此处可利用 “save” 方法将它们保存在其内。 在该界面中,我们还实现(如果可在抽象级别上)或声明(以供进一步重新定义)了一些方法,从而可按编号、名称、链接或“潜在”关系(容器内嵌套元素的反馈)的事实搜索元素。

我们添加缓存作为 LayoutBase 类的静态成员。

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    protected:
      ...
      static LayoutCache<C> *cacher;
      
    public:
      static void setCache(LayoutCache<C> *c)
      {
        cacher = c;
      }

每个窗口都必须为其自己创建一个缓存实例,并在方法的开头(例如 CreateLayout)利用 setCache 将其设置为正操作实例。 由于 MQL 程序是单线程的,因此可保证不会同时形成窗口(如果需要多个),也不会竞争 “cacher” 指针。 我们将在析构函数 LayoutBase 中自动清除指针; 当堆叠完成时,这意味着我们已在布局描述中保留了最后一个外部容器,且无需保存任何其他内容。

      ~LayoutBase()
      {
        ...
        if(stack.size() == 0)
        {
          cacher = NULL;
        }
      }

重置链接并不意味着我们正在清除缓存。 这种方式只是确保下一个潜在布局不会将另一个窗口的“控件”错误地加在其内。

为了填充缓存,我们将在 LayoutBase 内添加一种新的 init 方法 — 这次,在参数中没有指针或指向 GUI 的“第三方”元素的链接。

      // nonbound layout, control T is implicitly stored in internal cache
      template<typename T>
      T *init(const string name, const int m = 1, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        T *temp = NULL;
        for(int i = 0; i < m; i++)
        {
          temp = new T();
          if(save(temp))
          {
            init(temp, name + (m > 1 ? (string)(i + 1) : ""), x1, y1, x2, y2);
          }
          else return NULL;
        }
        return temp;
      }
      
      virtual bool save(C *control)
      {
        if(cacher != NULL)
        {
          cacher.save(control);
          return true;
        }
        return false;
      }

运用模板,我们可以编写新的 T(模板), 并在布局中生成对象(默认情况下,每次创建 1 个对象,但也可以选择若干个对象)。

对于标准库元素,我们编写了一个特定的缓存实现 StdLayoutCache(此处仅显示节略,完整的代码附于文后)。

  // CWnd implementation specific!
  class StdLayoutCache: public LayoutCache<CWnd>
  {
    public:
      ...
      virtual CWnd *get(const long m) override
      {
        if(m < 0)
        {
          for(int i = 0; i < ArraySize(cache); i++)
          {
            if(cache[i].Id() == -m) return cache[i];
            CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
            if(container != NULL)
            {
              for(int j = 0; j < container.ControlsTotal(); j++)
              {
                if(container.Control(j).Id() == -m) return container.Control(j);
              }
            }
          }
          return NULL;
        }
        else if(m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual CWnd *findParent(CWnd *control) override
      {
        for(int i = 0; i < ArraySize(cache); i++)
        {
          CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
          if(container != NULL)
          {
            for(int j = 0; j < container.ControlsTotal(); j++)
            {
              if(container.Control(j) == control)
              {
                return container;
              }
            }
          }
        }
        return NULL;
      }
      ...
  };

请注意,方法 get 通过其索引号(如果输入为正)或标识符(若品名前为负号)来搜索“控件”。 此处,标识符应为标准组件库分配的唯一编号,该标识符可用来分派事件。 在事件中,它由参数 lparam 传递。

在窗口的应用程序类中,我们可以直接使用 StdLayoutCache 类,或编写该类的派生类。

我们将在下面的示例中看到,缓存是如何减少窗口类的描述。 然而,在进行讨论之前,我们考虑一下缓存带来的其他机会。 我们还会在示例中用到它们。

样式

由于缓存是一个以集中方式处理元素的对象,因此用它来解决布局以外的其他许多任务很方便。 特别是对于元素,我们可以利用单一样式规则(例如颜色,字体或缩进)进行统一。 同时,在一个地方设置样式就足够了,不必为每个“控件”分别编写相同的属性。 甚至,缓存可以承担缓存元素的处理消息。 潜在地,我们可以动态地构造、缓存并与所有元素进行绝对地交互。 这样就根本不需要声明任何“显式”元素。 稍后,我们将看到动态创建的元素相对于自动化元素具有哪些明显的优势。

为了支持 StdLayoutCache 类的集中式样式,提供了一个 stub 方法:

    virtual LayoutStyleable<C> *getStyler() const
    {
      return NULL;
    }

如果您不打算使用样式,则无需其他编码。 不过,如果您意识到集中式样式管理的优势,您可实现 LayoutStyleable 的衍生类。 界面十分简单。

  enum STYLER_PHASE
  {
    STYLE_PHASE_BEFORE_INIT,
    STYLE_PHASE_AFTER_INIT
  };
  
  template<typename C>
  class LayoutStyleable
  {
    public:
      virtual void apply(C *control, const STYLER_PHASE phase) {};
  };

对于每个“控件”,调用两次方法 apply:在初始化阶段(STYLE_PHASE_BEFORE_INIT),以及在容器中注册阶段(STYLE_PHASE_AFTER_INIT)。 故此,在方法 LayoutBase::init 里,在第一阶段添加了一次调用:

      if(cacher != NULL)
      {
        LayoutStyleable<C> *styler = cacher.getStyler();
        if(styler != NULL)
        {
          styler.apply(object, STYLE_PHASE_BEFORE_INIT);
        }
      }

而在析构函数当中,我们添加了相似的代码,但第二阶段采用的是 STYLE_PHASE_AFTER_INIT。

由于样式目标可能不同,因此需要两个阶段。 在某些元素中,有时必须设置独立属性,其优先级高于在样式器中设置的那些公共属性。 在初始化阶段,“控件”仍然为空,即在布局中未进行任何设置。 在注册阶段,所有属性均已设置,我们能够基于它们附加修改样式。 最明显的例子如下。 标记为“只读”的所有区域最好显示为灰色。 不过,初始化之后,仅在布局时将“只读”属性分配给“控件”。 所以,第一阶段在此不适合,而在第二阶段是必需的。 另一方面,并非所有区域都具有此标志。 在所有其他情况下,它必须设置为默认颜色,然后布局语言才能执行选择性定制。

顺便说一下,可以在 MQL 程序界面的各种语言里运用类似的集中式本地化技术。

处理事件

逻辑上分配给缓存的第二个功能是事件处理。 对于它们,在 LayoutCache 类中添加了一个 stub 方法(C 是类的模板参数):

    virtual bool onEvent(const int event, C *control)
    {
      return false;
    }

同样,我们可以在派生类中实现它,但这并不是必需的。 事件代码由特定的函数库定义。

为了令该方法开始操作,我们需要事件拦截宏定义,类似于标准库中所用的,并编写映射关系,如下所示:

  EVENT_MAP_BEGIN(Dialog)
    ON_EVENT(ON_CLICK, m_button1, OnClickButton1)
    ...
  EVENT_MAP_END(AppDialog)

新的宏会将事件重定向到缓存对象中。 它们当中之一:

  #define ON_EVENT_LAYOUT_ARRAY(event, cache)  if(id == (event + CHARTEVENT_CUSTOM) && cache.onEvent(event, cache.get(-lparam))) { return true; }

在此,我们可以看到在缓存中按 lparam 中的标识符进行搜索(但符号相反),然后把找到的元素发送到上面研究过的 onEvent 应答程序。 基本上,我们可以在处理每个事件时忽略搜索元素,并将元素索引存储在缓存里,然后将特定处理过程与索引相链接。

当前缓存大小就是索引,即刚保存新元素的编号。 我们可以在布局时保存所需的“控件”索引。

          _layout<CButton> button1("Button");
          button1index = cache.cacheSize() - 1;

在此,button1index 是窗口类中的整数型变量。 在另一个按缓存索引处理元素的宏中会用到它:

  #define ON_EVENT_LAYOUT_INDEX(event, cache, controlIndex, handler)  if(id == (event + CHARTEVENT_CUSTOM) && lparam == cache.get(controlIndex).Id()) { handler(); return(true); }

此外,我们可以将事件直接发送到元素本身之中,而不是发送到缓存。 为此目的,该元素必须在其本身中实现由所需“控件”类模板化的 Notifiable 接口。

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

在父参数中,可以传递任何对象,包括对话框。 例如,基于 Notifiable,很容易创建按钮 CButton 的衍生类。

  class NotifiableButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        this.StateFlagsReset(7);
        return true;
      }
  };

有 2 个宏可与“可通知”元素一起操作。 它们仅在参数数量上有所不同:ON_EVENT_LAYOUT_CTRL_ANY 允许将随机对象传递到最后一个参数,而 ON_EVENT_LAYOUT_CTRL_DLG 没有此参数,因为它始终将对话框的 “this” 作为对象发送。

  #define ON_EVENT_LAYOUT_CTRL_ANY(event, cache, type, anything)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, anything)) { return true; }}
  #define ON_EVENT_LAYOUT_CTRL_DLG(event, cache, type)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, &this)) { return true; }}

在第二个示例的上下文中,我们将研究事件应答的各种选项。

情况 2 带有控件的对话框

演示项目包含类 CControlsDialog,其为标准库“控件”的主要类型。 与第一种情况类似,我们将删除所有创建它们的方法,并将其替换为唯一的 CreateLayout。 顺带,在旧项目中有多达 17 种方法,且用复合条件运算符彼此调用它们。

为了在生成“控件”时将其保存到缓存中,我们添加一个简单的缓存类以及一个样式类。 此处首先是缓存。

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      MyLayoutStyleable styler;
      CControlsDialog *parent;
      
    public:
      MyStdLayoutCache(CControlsDialog *owner): parent(owner) {}
      
      virtual StdLayoutStyleable *getStyler() const override
      {
        return (StdLayoutStyleable *)&styler;
      }
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          parent.SetCallbackText(__FUNCTION__ + " " + control.Name());
          return true;
        }
        return false;
      }
  };

在缓存类中,声明事件处理器 onEvent,我们将通过事件映射进行连接。 此处,处理器将消息发送到父窗口,在父窗口里,如同先前的情况版本,该消息将显示在信息字段中。

在样式类中,我们为所有元素设置相同的字段,在所有按钮上设置非标准字体,并用灰色的“只读”属性显示 CEdit(我们只有一个这样的属性,但如果添加了其他属性,则它将自动落入通用设置)。

  class MyLayoutStyleable: public StdLayoutStyleable
  {
    public:
      virtual void apply(CWnd *control, const STYLER_PHASE phase) override
      {
        CButton *button = dynamic_cast<CButton *>(control);
        if(button != NULL)
        {
          if(phase == STYLE_PHASE_BEFORE_INIT)
          {
            button.Font("Arial Black");
          }
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(control);
          if(edit != NULL && edit.ReadOnly())
          {
            if(phase == STYLE_PHASE_AFTER_INIT)
            {
              edit.ColorBackground(clrLightGray);
            }
          }
        }
        
        if(phase == STYLE_PHASE_BEFORE_INIT)
        {
          control.Margins(DEFAULT_MARGIN);
        }
      }
  };

在窗口中保存缓存的链接;它的创建和删除,分别位于构造函数和析构函数当中,在创建时要传递指向窗口的链接作为参数,从而确保之后的反馈。

  class CControlsDialog: public AppDialogResizable
  {
    private:
      ...
      MyStdLayoutCache *cache;
    public:
      CControlsDialog(void)
      {
        cache = new MyStdLayoutCache(&this);
      }

现在我们分阶段研究方法 CreateLayout。 由于阅读了详细的说明,该方法看似很长且很复杂。 但事实并非如此。 如果删除了信息性注释(在实际项目不会用到),则该方法能适合一屏,且不包含任何复杂的逻辑。

在最开始处,通过调用 setCache 激活缓存。 然后,在第一个模块中描述主容器 CControlsDialog。 因为我们传递了已创建的 “this” 链接,所以它不会在缓存中。

  bool CControlsDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    StdLayoutBase::setCache(cache); // assign the cache object to store implicit objects
    
    {
      _layout<CControlsDialog> dialog(this, name, x1, y1, x2, y2);

之后,创建 CBox 类的嵌套容器的隐式实例,作为窗口的客户区域。 它是垂直定向的,因此嵌套的容器会从上到下填充空间。 我们将链接保存到对象的 m_main 变量之中,因为在调整窗口大小时必须调用其方法 Pack。 如果您的对话框不是 “rubber” 对话框,则无需这样做。 最后,对于客户区域,即使在调整大小时,也将零字段和所有方向的对齐方式设置为令面板填充整个窗口。

      {
        // example of implicit object in the cache
        _layout<CBox> clientArea("main", ClientAreaWidth(), ClientAreaHeight(), LAYOUT_STYLE_VERTICAL);
        m_main = clientArea.get(); // we can get the pointer to the object from cache (if required)
        clientArea <= WND_ALIGN_CLIENT <= 0.0; // double type is important

在下一个级别,该容器将作为第一个容器,它填充整个窗口宽度,但略高于输入区域。 此外,将用对齐 WND_ALIGN_TOP(以及 WND_ALIGN_WIDTH)将其“粘合”到窗口的上边缘。

        {
          // another implicit container (we need no access it directly)
          _layout<CBox> editRow("editrow", ClientAreaWidth(), EDIT_HEIGHT * 1.5, (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH));

内部只有唯一的“控件” CEdit 类以“只读”模式存在。 显式变量 m_edit 得以保留,因此不会被缓存。

          {
            // for editboxes default boolean property is ReadOnly
            _layout<CEdit> edit(m_edit, "Edit", ClientAreaWidth(), EDIT_HEIGHT, true);
          }
        }

到此时,我们已经初始化了 3 个元素。 在右括号之后,将销毁 “edit” 布局对象,并在执行其析构函数过程中将 m_edit 添加到容器 “editrow” 之中。不过,紧随其后的是另一个括号。 它破坏了上下文,布局对象 editRow 在其中生存。 故此,此容器又被添加到客户区容器当里,得以保留在堆栈中。 因此,在 m_main 中形成垂直布局的第一行。

然后我们就有了带有三个按钮的一行。 首先,为其创建一个容器。

        {
          _layout<CBox> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          buttonRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);

在此,您应该注意对齐 WND_ALIGN_CONTENT 的非标准方法。 它的含义如下。

对于 CBox 类,添加了针对容器大小缩放嵌套元素的算法。 它在方法 AdjustFlexControls 里执行,且只有在容器对齐标志中指定了 WND_ALIGN_CONTENT 特殊值时才生效。 它不是标准枚举 ENUM_WND_ALIGN_FLAGS 的一部分。 容器分析 “控件” 中有些控件具有固定的大小,有些控件没有固定的大小。 尺寸固定的“控件”是那些未指定距容器边侧(在特定维度中)对齐方式的控件。 对于所有此类“控件”,容器将计算其大小的总和,从容器的总大小中减去它,然后将其余部分按比例分配给其余“控件”。例如,如果容器中有两个“控件”,但它们都没有绑定,则它们在整个容器区域中会彼此对半平分。

这是一种非常方便的模式,但您不应在一组交错的容器上误用它 — 由于计算尺寸是单次算法,内部元素在容器的整个区域上对齐,实际上内容调整会产生不确定性(由此原因,在布局类中会发生一个特殊事件 ON_LAYOUT_REFRESH,窗口可以将其发送给自身以便重新计算尺寸)。

如果我们的行带有三个按钮,则在调整窗口宽度时,它们的长度都会按比例变化。 第一个 CButton 类的按钮是隐式创建的,并存储在缓存中。

          { // 1
            _layout<CButton> button1("Button1");
            button1index = cache.cacheSize() - 1;
            button1["width"] <= BUTTON_WIDTH;
            button1["height"] <= BUTTON_HEIGHT;
          } // 1

第二个按钮是类 NotifiableButton(上面已讲述过)。 该按钮将自行处理消息。

          { // 2
            _layout<NotifiableButton> button2("Button2", BUTTON_WIDTH, BUTTON_HEIGHT);
          } // 2

第三个按钮是基于显式定义的窗口变量 m_button3 创建的,并具有“粘滞”属性。

          { // 3
            _layout<CButton> button3(m_button3, "Button3", BUTTON_WIDTH, BUTTON_HEIGHT, "Locked");
            button3 <= true; // for buttons default boolean property is Locking
          } // 3
        }

请注意,所有按钮都包围在各自的大括号中。 由此,它们会被按顺序添加到行中,并出现闭合大括号,标记为1、2 和 3; 即按照自然顺序。 我们可以省略为每个按钮设置这些“个人”模块的方式,并受容器的常规模块的限制。 但随后按钮应遵照相反的顺序加入,因为对象的析构函数总是遵照与创建它们的相反顺序来调用。 我们可将布局中所描述按钮的顺序反转,从而“解决”这种情况。

在第三行中,有一个容器,其内包含控件、微调器和日历。 容器是“匿名”创建的,并存储在缓存中。

        {
          _layout<CBox> spinDateRow("spindaterow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          spinDateRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);
          
          {
            _layout<SpinEditResizable> spin(m_spin_edit, "SpinEdit", GROUP_WIDTH, EDIT_HEIGHT);
            spin["min"] <= 10;
            spin["max"] <= 1000;
            spin["value"] <= 100; // can set value only after limits (this is how SpinEdits work)
          }
          
          {
            _layout<CDatePicker> date(m_date, "Date", GROUP_WIDTH, EDIT_HEIGHT, TimeCurrent());
          }
        }

最终,最后一个容器填充了窗口的所有剩余区域,并包括两列带有元素的列。 专门分配了亮丽的色彩,以便清晰地展示窗口中的那个容器。

        {
          _layout<CBox> listRow("listsrow", ClientAreaWidth(), LIST_HEIGHT);
          listRow["top"] <= (int)(EDIT_HEIGHT * 1.5 * 3);
          listRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_CLIENT);
          (listRow <= clrMagenta)["border"] <= clrBlue;
          
          createSubList(&m_lists_column1, LIST_OF_OPTIONS);
          createSubList(&m_lists_column2, LIST_LISTVIEW);
          // or vice versa (changed order gives swapped left/right side location)
          // createSubList(&m_lists_column1, LIST_LISTVIEW);
          // createSubList(&m_lists_column2, LIST_OF_OPTIONS);
        }

在此,应特别注意的是,m_lists_column1 和 m_lists_column2 两列不是在方法 CreateLayout 本身之中,而是用助手方法 createSubList 时填充的。 就布局而言,该函数的调用方式与进入下一个大括号没有区别。 这意味着布局不一定包含一个较长的静态列表,但它可能包含根据条件修改的片段。 或者,您可以将相同的片段包含在不同的对话框中。

在我们的例子中,我们可以通过更改函数的第二个参数来更改窗口中列的顺序。

      }
    }

直至闭合所有括号后,所有 GUI 元素都将被初始化,并相互连接。 我们调用 Pack 方法(直接或通过 SelfAdjustment,在此也称为对请求 “rubber” 对话框的响应)。

    // m_main.Pack();
    SelfAdjustment();
    return true;
  }

我们不打算涉及方法 createSubList 的详细信息。 在内部,能够生成一组 3 个“控件”(组合框,选项组和无线列组)或列表(ListView)的可能性已被实现,所有这些控件都能作为 “rubber” 控件。 有趣的是,“控件”是用另一类生成器 ItemGenerator 填充的。

  template<typename T>
  class ItemGenerator
  {
    public:
      virtual bool addItemTo(T *object) = 0;
  };

该类的唯一方法是从对象“控件”的布局里调用的,直止该方法返回 false(数据结束的标志)。

默认情况下,为标准库提供了一些简单的生成器(它们用“控件”方法,AddItem):StdItemGenerator,StdGroupItemGenerator,SymbolsItemGenerator 和 ArrayItemGenerator。 特别是,SymbolsItemGenerator 能够用来自市场观察里的品种填充“控件”。

  template<typename T>
  class SymbolsItemGenerator: public ItemGenerator<T>
  {
    protected:
      long index;
      
    public:
      SymbolsItemGenerator(): index(0) {}
      
      virtual bool addItemTo(T *object) override
      {
        object.AddItem(SymbolName((int)index, true), index);
        index++;
        return index < SymbolsTotal(true);
      }
  };

在布局中,它的指定方式与“控件”的生成器相同。 备选则是允许将生成器动态分布对象的指针链接传递给布局对象,而非指向自动或静态对象的指针(必须在前面代码的某处进行描述)。

        _layout<ListViewResizable> list(m_list_view, "ListView", GROUP_WIDTH, LIST_HEIGHT);
        list <= WND_ALIGN_CLIENT < new SymbolsItemGenerator<ListViewResizable>();

为此目的,用到了 “<” 运算符。 操作完成后,动态分布式生成器会被自动删除。

为了与新事件相关联,在映射中要添加相关的宏。

  EVENT_MAP_BEGIN(CControlsDialog)
    ...
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, NotifiableButton)
    ON_EVENT_LAYOUT_INDEX(ON_CLICK, cache, button1index, OnClickButton1)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
  EVENT_MAP_END(AppDialogResizable)

宏 ON_EVENT_LAYOUT_CTRL_DLG 与任意 NotifyableButton 类的按钮(在我们的情况下,是单个按钮)关联鼠标单击时的通知。 宏 ON_EVENT_LAYOUT_INDEX 按照缓存中的指定索引将相同事件发送给按钮。 不过,编写此宏的步骤我们可以省略,因为宏 ON_EVENT_LAYOUT_ARRAY 会将鼠标单击的最后一个字符串发送到缓存中的任何元素,前提是其标识符与 lparam 一致。

基本上,所有元素都可以传递到缓存,且它们的事件可按新的方式进行处理;不过,旧的方式也可以,并且它们可以组合在一起。

在下面的动画图像中,展示事件如何响应。

利用 MQL 标记语言形成的控件-包含的对话框

利用 MQL 标记语言形成的控件-包含的对话框

请注意,翻译事件的方式可以通过信息字段中显示的函数签名间接识别。 您还可以看到事件同时出现在“控件”和容器中。 显示红色框的容器用来调试,您可以利用宏 LAYOUT_BOX_DEBUG 来禁用它们。

情况 3 DynamicForm 的动态布局

在最后一个示例中,我们将研究一种形式,其中所有元素都会在缓存中动态创建。 这将给我们带来一些新的重要机遇。

与之前的情况一样,缓存将支持样式化元素。 唯一的样式设置是相同的独色区域,能令您查看容器的嵌套,并用鼠标选择它们。

下面的简单界面结构在方法 CreateLayout 中已讲过了。 如往常一样,主容器会填充窗口的整个客户区。 在上部,有一个带有两个按钮的块:注入和导出。 它们下面的所有空间由划分为左右两列的容器所填充。 灰色标记的左列原为空。 在右列中,有一组单选按钮,用来选择控件类型。

      {
        // example of implicit object in the cache
        _layout<CBoxV> clientArea("main", ClientAreaWidth(), ClientAreaHeight());
        m_main = clientArea.get();
        clientArea <= WND_ALIGN_CLIENT <= PackedRect(10, 10, 10, 10);
        clientArea["background"] <= clrYellow <= VERTICAL_ALIGN_TOP;
        
        {
          _layout<CBoxH> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 5);
          buttonRow <= 5.0 <= (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH);
          buttonRow["background"] <= clrCyan;
          
          {
            // these 2 buttons will be rendered in reverse order (destruction order)
            // NB: automatic variable m_button3
            _layout<CButton> button3(m_button3, "Export", BUTTON_WIDTH, BUTTON_HEIGHT);
            _layout<NotifiableButton> button2("Inject", BUTTON_WIDTH, BUTTON_HEIGHT);
          }
        }
        
        {
          _layout<CBoxH> buttonRow("buttonrow2", ClientAreaWidth(), ClientAreaHeight(),
            (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_CONTENT|WND_ALIGN_CLIENT));
          buttonRow["top"] <= BUTTON_HEIGHT * 5;
          
          {
            {
              _layout<CBoxV> column("column1", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
              column <= clrGray;
              {
                // dynamically created controls will be injected here
              }
            }
            
            {
              _layout<CBoxH> column("column2", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
            
              _layout<RadioGroupResizable> selector("selector", GROUP_WIDTH, CHECK_HEIGHT);
              selector <= WND_ALIGN_HEIGHT;
              string types[3] = {"Button", "CheckBox", "Edit"};
              ArrayItemGenerator<RadioGroupResizable,string> ctrls(types);
              selector <= ctrls;
            }
          }
        }
      }

假定,在单选组中选择了元素类型之后,用户按下 “Inject” 按钮,并在窗口的左侧部分创建了相关的“控件”。 当然,您可以逐一创建几个不同的“控件”。 这会根据容器设置自动居中。 为了实现此逻辑,“Inject” 按钮应由 NotifiableButton 类的 onEvent 应答程序处理。

  class NotifiableButton: public Notifiable<CButton>
  {
      static int count;
      
      StdLayoutBase *getPtr(const int value)
      {
        switch(value)
        {
          case 0:
            return new _layout<CButton>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 1:
            return new _layout<CCheckBox>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 2:
            return new _layout<CEdit>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
        }
        return NULL;
      }
      
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        DynamicForm *parent = dynamic_cast<DynamicForm *>(anything);
        MyStdLayoutCache *cache = parent.getCache();
        StdLayoutBase::setCache(cache);
        CBox *box = cache.get("column1");
        if(box != NULL)
        {
          // put target box to the stack by retrieving it from the cache
          _layout<CBox> injectionPanel(box, box.Name());
          
          {
            CRadioGroup *selector = cache.get("selector");
            if(selector != NULL)
            {
              const int value = (int)selector.Value();
              if(value != -1)
              {
                AutoPtr<StdLayoutBase> base(getPtr(value));
                (~base).get().Id(rand() + (rand() << 32));
              }
            }
          }
          box.Pack();
        }
        
        return true;
      }
  };

首先在缓存中按名称 “column1” 搜索要插入新元素的容器。 创建对象 jectionPanel 时,此容器将作为第一个参数。 在布局算法中,已特别考虑到所传递元素已在缓存中的事实 — 它不会被再次加入缓存,而是如常放入容器堆栈中。 这允许将元素添加到“旧”容器中。

根据用户的选择,利用辅助方法 getPtr 中的运算符 “new” 创建所需类型的对象。 为了令加入的“控件”能够正常工作,要为它们随机生成唯一的标识符。 特殊类 AutoPtr 确保从代码模块退出时将指针删除。

如果添加太多元素,它们会超出容器边界。 发生这种情况是因为我们所用的容器类尚未学会如何响应溢出。 在这种情况下,例如,我们可以显示滚动条,而超出边界的元素就可以被隐藏。

然而,这都不重要。 这种情况的关键是我们可以通过设置表单来生成动态内容,并确保必要内容显示和容器的大小。

除了添加元素外,此对话框还可以删除它们。 可以通过单击鼠标来选择表单中的任何元素。 与此同时,元素所属类和名称得以记录,同时用红框突出显示元素本身。 如果单击已选择的元素,则将显示对话框确认删除的请求,若确认,则删除该元素。 所有这些都在我们的缓存类中实现。

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      DynamicForm *parent;
      CWnd *selected;
      
      bool highlight(CWnd *control, const color clr)
      {
        CWndObj *obj = dynamic_cast<CWndObj *>(control);
        if(obj != NULL)
        {
          obj.ColorBorder(clr);
          return true;
        }
        else
        {
          CWndClient *client = dynamic_cast<CWndClient *>(control);
          if(client != NULL)
          {
            client.ColorBorder(clr);
            return true;
          }
        }
        return false;
      }
      
    public:
      MyStdLayoutCache(DynamicForm *owner): parent(owner) {}
      
      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(control); // get actual GUI element
          }
          
          if(element == NULL)
          {
            Print("Can't find GUI element for ", control._rtti + " / " + control.Name());
            return true;
          }
          
          if(selected == control)
          {
            if(MessageBox("Delete " + element._rtti + " / " + element.Name() + "?", "Confirm", MB_OKCANCEL) == IDOK)
            {
              CWndContainer *container;
              container = dynamic_cast<CWndContainer *>(findParent(element));
              if(container)
              {
                revoke(element); // deep remove of all references (with subtree) from cache
                container.Delete(element); // delete all subtree of wnd-objects
                
                CBox *box = dynamic_cast<CBox *>(container);
                if(box) box.Pack();
              }
              selected = NULL;
              return true;
            }
          }
          selected = control;
          
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", b);
          
          return true;
        }
        return false;
      }
  };

我们可以删除缓存中所有的任何界面元素,即,不仅是那些由 “Inject” 按钮添加的元素。 依此方式,您可以删除整个左半部分或右侧的“单选框”。例如,如果我们尝试删除上面含有两个按钮的容器,则会发生很有趣的事情。 这将导致 “Export” 按钮不再与对话框绑定,且将保留在图表当中。

可编辑表单:添加和删除元素

可编辑表单:添加和删除元素

发生这种情况是因为只有元素才会被有意描述为自动,而不是动态变量(在表单类中,有一个 CButton 的实例 m_button3)。

当标准库尝试删除界面元素时,它将其委派给数组类 CArrayObj,后者依次检查指针类型,并仅删除类型为 POINTER_DYNAMIC 的对象。 因此,很明显,为了构建一个自适应界面,即元素可相互替换或完全删除,只能寄希望于动态放置,而缓存提供了一种现成的解决方案。

最后,我们来参考对话框的第二个按钮 ”Export“。 正如我们从名称中所见,它旨在遵照所研究的 MQL-layout 语法将对话框的当前状态保存为文本文件。 当然,该表单仅允许在有限的范围内设置其外观。 但将外观用现成的 MQL 代码导出,即您随后可以轻松地将其复制到程序中,并获得相同界面的可能性本身,很有潜力成为一项非常有价值的技术。 当然,仅是导出界面,而您还必须单独启用事件处理代码,或常规设置。

由 LayoutExporter 类可确保导出;我们不会研究它的所有细节,且源代码随附于后。

结束语

在本文中,我们验证了以 MQL 本身描述 MQL 程序图形界面布局概念的可实现性。 配合运用元素的动态生成与缓存中的集中存储,可以简化组件层次结构的创建和控制。 基于缓存,您可以执行与设计界面有关的大多数任务,尤其是统一的样式、事件处理、实时编辑布局,并为以后的用途保存为合适的格式。

如果我们将这些函数揉在一起,实践证明,简单的可视表单编辑器几乎可以胜任一切。 它可能只支持大多数“控件”共有的最重要的属性,但尽管如此,它仍能够形成界面模板。 不过,我们可以看到,即使出于评估此新概念的初始阶段,也也花费了很多功夫。 所以,新编辑器的实际实现体现出一个相当复杂的问题。 这就是另一个故事了。

分享到:
举报财经168客户端下载

全部回复

0/140

投稿 您想发表你的观点和看法?

更多人气分析师

  • 金算盘

    人气2696文章7761粉丝124

    高级分析师,混过名校,厮杀于股市和期货、证券市场多年,专注...

  • 李冉晴

    人气2296文章3821粉丝34

    李冉晴,专业现贷实盘分析师。

  • 张迎妤

    人气1896文章3305粉丝34

    个人专注于行情技术分析,消息面解读剖析,给予您第一时间方向...

  • 指导老师

    人气1856文章4423粉丝52

    暂无个人简介信息

  • 梁孟梵

    人气2152文章3177粉丝39

    qq:2294906466 了解群指导添加微信mfmacd

  • 刘钥钥1

    人气2016文章3119粉丝34

    专业从事现货黄金、现货白银模似实盘操作分析指导

  • 张亦巧

    人气2144文章4145粉丝45

    暂无个人简介信息

  • 金帝财神

    人气4720文章8329粉丝118

    本文由资深分析师金帝财神微信:934295330,指导黄金,白银,...

  • 金泰铬J

    人气2320文章3925粉丝51

    投资问答解咨询金泰铬V/信tgtg67即可获取每日的实时资讯、行情...