在第一部分当中,我们研究了以 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 标记语言形成的控件-包含的对话框
请注意,翻译事件的方式可以通过信息字段中显示的函数签名间接识别。 您还可以看到事件同时出现在“控件”和容器中。 显示红色框的容器用来调试,您可以利用宏 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 程序图形界面布局概念的可实现性。 配合运用元素的动态生成与缓存中的集中存储,可以简化组件层次结构的创建和控制。 基于缓存,您可以执行与设计界面有关的大多数任务,尤其是统一的样式、事件处理、实时编辑布局,并为以后的用途保存为合适的格式。
如果我们将这些函数揉在一起,实践证明,简单的可视表单编辑器几乎可以胜任一切。 它可能只支持大多数“控件”共有的最重要的属性,但尽管如此,它仍能够形成界面模板。 不过,我们可以看到,即使出于评估此新概念的初始阶段,也也花费了很多功夫。 所以,新编辑器的实际实现体现出一个相当复杂的问题。 这就是另一个故事了。