请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3366063 新帖:0

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

Tango发表于:7 月 29 日 19:00回复(1)

在第一部分当中,我们研究了以 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 程序图形界面布局概念的可实现性。 配合运用元素的动态生成与缓存中的集中存储,可以简化组件层次结构的创建和控制。 基于缓存,您可以执行与设计界面有关的大多数任务,尤其是统一的样式、事件处理、实时编辑布局,并为以后的用途保存为合适的格式。

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

全部回复

0/140

量化课程

    移动端课程