English Русский Español Deutsch 日本語 Português
图形界面 X: 多行文本框控件 (集成编译 8)

图形界面 X: 多行文本框控件 (集成编译 8)

MetaTrader 5示例 | 16 三月 2017, 10:00
8 040 0
Anatoli Kazharski
Anatoli Kazharski

目录

 

概论

为了更好地理解这个函数库的目的, 请参阅首篇文章: 图形界面 I: 函数库结构的准备 (第 1 章)。您将在每章末尾找到一个包含文章链接的列表。从那里, 您可以下载一个当前开发阶段的函数库完整版。这些文件必须放置在与存档相同的目录中。

本文研究一个新的控件: 多行文本框。不同于终端提供的 OBJ_EDIT 类型图形对象, 这一版本没有输入字符数量的限制。它允许将文本框转换为一个简单的文本编辑器。也就是说, 可以输入多行, 且文本光标可通过鼠标和键盘移动。如果行的宽度溢出控件的可见区域, 则会出现一个滚动条。多行文本框控件将会完全渲染, 并且它的质量将尽可能接近操作系统中的类似控件。


按键组和键盘布局

在描述 CTextBox (文本框) 类型控件的代码之前, 应该简要地论及键盘, 因为它是数据输入的装置。而且, 哪些键被按下将在控制类的第一版中处理。 

键盘按键可以划分成若干组 (参见图例. 1 中的示意):

  • 控制键 (橙色)
  • 功能键 (紫色)
  • 字母数字键 (蓝色)
  • 导航键 (绿色)
  • 数字键盘 (红色)

 图例. 1. 按键组 (QWERTY 键盘布局)。

图例. 1. 按键组 (QWERTY 键盘布局)。


对于英语来说, 有多种拉丁语键盘布局。最流行的一种就是 QWERTY。在我们的情况中, 主要语言是俄语, 因此我们使用俄语布局 - ЙЦУКЕНQWERTY 保留给英语, 选择作为附加语言。 

从集成编译 1510 版开始, MQL 语言包括 ::TranslateKey() 函数。它可从按键传递的代码中获取字符, 字符对应于操作系统中设置的语言和布局。以前, 必须为每种语言手工生成字符阵列, 但由于工作量巨大而导致困难重重。现在一切都容易得多。


处理按键事件

按键事件可在 ::OnChartEvent() 系统函数里使用 CHARTEVENT_KEYDOWN 标识符进行跟踪。

//+------------------------------------------------------------------+
//| ChartEvent 函数                                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 按键 
   if(id==CHARTEVENT_KEYDOWN)
     {
      ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam,"; symbol: ",::ShortToString(::TranslateKey((int)lparam)));
      return;
     }
  }

以下数值作为其它三个参数转到该函数:

  • long 参数 (lparam) – 所按键代码, 即字符或控制键的 ASCII 代码。 
  • dparam 参数 (dparam) – 按键次数。此值永远等于 1。如需获取从按下键那一刻开始的调用次数, 则需进行独立计算。
  • sparam 参数 (sparam) – 位掩码的字符串值, 其为键盘按键的状态描述。当按下一个键时, 事件将会立即生成。如果按下键并立即释放, 而不保持, 则在此接收扫描码的数值。如果按键之后并保持, 则生成的数值将基于扫描码 + 16384 位。

例如, 下面的清单 (在终端日志中输出) 显示按下 Esc 键并保持的结果。此键的代码为 27(lparam), 按下时扫描码为 1 (sparam), 且保持按下大约 500 毫秒时, 终端开始生成数值 16385 (扫描码 + 16384 位)。

2017.01.20 17:53:33.240 id: 0; lparam: 27; dparam: 1.0; sparam: 1
2017.01.20 17:53:33.739 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.772 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.805 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.837 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.870 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
...

并非所有键都会使用 CHARTEVENT_KEYDOWN 标识符引发事件。它们当中的一些被分配给终端满足所需, 而有些则简单地不产生按键事件。它们在下图中用蓝色高亮显示:

图例. 2. 终端占用的键, 不生成 CHARTEVENT_KEYDOWN 事件。 

图例. 2. 终端占用的键, 不生成 CHARTEVENT_KEYDOWN 事件。


字符和控制键的 ASCII 码

来自维基百科的信息 (更多): 

ASCII, 美国信息交换标准代码的缩写, 是一种字符编码标准。ASCII 代码用于在计算机, 电信设备和其它设备中表述文本。该标准的第一版于 1963 年发表。

下图显示了键盘按键的 ASCII 码:

 图例. 3. 字符和控制键的 ASCII 码。

图例. 3. 按键的 ASCII 码。


所有 ASCII 码已放在 KeyCodes.mqh 文件里, 均为宏定义形式 (#define)。下表显示了这些代码的一部分:

//+------------------------------------------------------------------+
//|                                                     KeyCodes.mqh |
//|                                 版权所有 2016, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 字符和控制键的 ASCII 码                                           |
//| 用于处理按键事件 (事件的 long 参数)                                |
//+------------------------------------------------------------------+
#define KEY_BACKSPACE          8
#define KEY_TAB                9
#define KEY_NUMPAD_5           12
#define KEY_ENTER              13
#define KEY_SHIFT              16
#define KEY_CTRL               17
#define KEY_BREAK              19
#define KEY_CAPS_LOCK          20
#define KEY_ESC                27
#define KEY_SPACE              32
#define KEY_PAGE_UP            33
#define KEY_PAGE_DOWN          34
#define KEY_END                35
#define KEY_HOME               36
#define KEY_LEFT               37
#define KEY_UP                 38
#define KEY_RIGHT              39
#define KEY_DOWN               40
#define KEY_INSERT             45
#define KEY_DELETE             46
...

 


按键扫描码

来自维基百科的信息 (更多):  

扫描码是大多数键盘发送到计算机用以报告哪些键被按下的数据。键盘上的每个键均会被分配一个数字或数字序列。

下图显示了按键扫描码:

图例. 4. 按键扫面码。 

图例. 4. 按键扫面码。


与 ASCII 码类似, 扫描码也包含在 KeyCodes.mqh 文件中。下表显示了列表的一部分:

//+------------------------------------------------------------------+
//|                                                     KeyCodes.mqh |
//|                                 版权所有 2016, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
...
//--- Bit
#define KEYSTATE_ON            16384
//+------------------------------------------------------------------+
//| 按键扫描码 (事件的字符串参数)                                      |
//+------------------------------------------------------------------+
//| 按下一次: KEYSTATE_XXX                                            |
//| 按下不放: KEYSTATE_XXX + KEYSTATE_ON                              |
//+------------------------------------------------------------------+
#define KEYSTATE_ESC           1
#define KEYSTATE_1             2
#define KEYSTATE_2             3
#define KEYSTATE_3             4
#define KEYSTATE_4             5
#define KEYSTATE_5             6
#define KEYSTATE_6             7
#define KEYSTATE_7             8
#define KEYSTATE_8             9
#define KEYSTATE_9             10
#define KEYSTATE_0             11
//---
#define KEYSTATE_MINUS         12
#define KEYSTATE_EQUALS        13
#define KEYSTATE_BACKSPACE     14
#define KEYSTATE_TAB           15
//---
#define KEYSTATE_Q             16
#define KEYSTATE_W             17
#define KEYSTATE_E             18
#define KEYSTATE_R             19
#define KEYSTATE_T             20
#define KEYSTATE_Y             21
#define KEYSTATE_U             22
#define KEYSTATE_I             23
#define KEYSTATE_O             24
#define KEYSTATE_P             25
...

 


操控键盘的辅助类

为了便捷地操控键盘, 已实现了 CKeys 类。它包含在 Keys.mqh 类中, 且在 KeyCodes.mqh 文件 中包含了所有按键和字符的代码。 

//+------------------------------------------------------------------+
//|                                                         Keys.mqh |
//|                                 版权所有 2016, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\KeyCodes.mqh>
//+------------------------------------------------------------------+
//| 操控键盘的类                                                      |
//+------------------------------------------------------------------+
class CKeys
  {
public:
                     CKeys(void);
                    ~CKeys(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                           |
//+------------------------------------------------------------------+
CKeys::CKeys(void)
  {
  }
//+------------------------------------------------------------------+
//| 析构器                                                           |
//+------------------------------------------------------------------+
CKeys::~CKeys(void)
  {
  }

确定按键则需:

(1) 字母数字字符 (包括空格)

(2) 数字板字符

或 (3) 特殊字符,

使用 CKeys::KeySymbol() 方法。它利用 CHARTEVENT_KEYDOWN 标识符传递事件的 long 参数值, 它将返回字符串格式的字符, 或者按键不属于指定范围时返回空字符串 ('')。 

class CKeys
  {
public:
   //--- 返回按键的字符
   string            KeySymbol(const long key_code);
  };
//+------------------------------------------------------------------+
//| 返回按键的字符                                                    |
//+------------------------------------------------------------------+
string CKeys::KeySymbol(const long key_code)
  {
   string key_symbol="";
//--- 若有必要输入一个空格 (空格键)
   if(key_code==KEY_SPACE)
     {
      key_symbol=" ";
     }
//--- 如果需要输入 (1) 一个字母字符, 或 (2) 一个数字板字符, 或 (3) 一个特殊字符
   else if((key_code>=KEY_A && key_code<=KEY_Z) ||
           (key_code>=KEY_0 && key_code<=KEY_9) ||
           (key_code>=KEY_SEMICOLON && key_code<=KEY_SINGLE_QUOTE))
     {
      key_symbol=::ShortToString(::TranslateKey((int)key_code));
     }
//--- 返回字符
   return(key_symbol);
  }

最后, 需要一个方法来确定 Ctrl 键的当前状态。当在文本框中移动文本光标时, 它用于同时按下两个键的各种组合。

若要获取 Ctrl 键的当前状态, 请使用终端的系统函数 ::TerminalInfoInteger()。此函数具有多个标识符, 用来检测按键的当前状态。标识符 TERMINAL_KEYSTATE_CONTROL 即用于 Ctrl 键。此类别的所有其它标识符都可以在 MQL5 语言参考中找到。

使用标识符可以很容易地确定是否有键按下。如果按下一个键, 返回值将小于零

class CKeys
  {
public:
   //--- 返回 Ctrl 键的状态
   bool              KeyCtrlState(void);
  };
//+------------------------------------------------------------------+
/| 返回 Ctrl 键的状态                                                 |
//+------------------------------------------------------------------+
bool CKeys::KeyCtrlState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0);
  }

现在, 创建文本框控件的准备全部就绪。 

 


多行文本框控件

多行文本框控件也可以在组合控件中使用。它属于复合控件组, 因为它包含滚动条。此外, 多行文本框控件可用于输入文本, 以及显示先前保存在文件中的文本。

用来输入数值的编辑框控件 (CSpinEdit 类), 或者自定义文本 (CTextEdit) 已在早前研究过。它们使用 OBJ_EDIT 类型的图形对象。它有一个严重的限制: 只能输入 63 个字符, 且它们必须适于单行。因此, 当前任务是创建没有此类限制的文本编辑框。  


 

图例. 5. 多行文本框控件。

现在, 我们来近距离观察用 CTextBox 类创建此控件。

 

开发 CTextBox 类用来创建控件

利用 CTextBox 类创建 TextBox.mqh 文件, 类中包含用于函数库所有控件的标准方法, 并 在其内包含以下文件:

  • 控件的基类 — Element.mqh
  • 滚动条类 — Scrolls.mqh
  • 操控键盘的类 — Keys.mqh
  • 操控时间计数器的类 — TimeCounter.mqh
  • 操控图表的类, MQL 位于 — Chart.mqh。 
//+------------------------------------------------------------------+
//|                                                      TextBox.mqh |
//|                                 版权所有 2016, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Scrolls.mqh"
#include "..\Keys.mqh"
#include "..\Element.mqh"
#include "..\TimeCounter.mqh"
#include <Charts\Chart.mqh>
//+------------------------------------------------------------------+
//| 用于创建多行文本框的类                                            |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- 键盘操控类的实例
   CKeys             m_keys;
   //--- 图表管理类的实例
   CChart            m_chart;
   //--- 时间计数器操控类的实例
   CTimeCounter      m_counter;
   //---
public:
                     CTextBox(void);
                    ~CTextBox(void);
   //--- 图表事件处理器
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- 计时器
   virtual void      OnEventTimer(void);
   //--- 移动元素
   virtual void      Moving(const int x,const int y,const bool moving_mode=false);
   //--- (1) 显示, (2) 隐藏, (3) 重置, (4) 删除
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) 设置, (2) 重置鼠标左键点击优先权
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
   //--- 颜色清零
   virtual void      ResetColors(void) {}
   //---
private:
   //--- 改变窗口右边缘的宽度
   virtual void      ChangeWidthByRightWindowSide(void);
   //--- 改变窗口底部的高度
   virtual void      ChangeHeightByBottomWindowSide(void);
  };



属性和外观

所需结构, 名为 KeySymbolOptions, 具有字符数组和它们的属性。在当前版本中, 它将包含两个动态数组: 

  • m_symbol[] 分别包含字符串的所有字符。
  • m_width[] 数组分别包含字符串所有字符的宽度。

这个类的实例也将被声明为动态数组。其大小始终等于文本框内的行数。

class CTextBox : public CElement
  {
private:
   //--- 字符及其属性
   struct KeySymbolOptions
     {
      string            m_symbol[]; // 字符
      int               m_width[];  // 字符宽度
     };
   KeySymbolOptions  m_lines[];
  };

在第一版的控件中, 文本将会整行输出。因此, 在行输出之前, 需要从 m_symbol[] 数组里将其组合到一起。CTextBox::CollectString() 方法为此目的服务, 其需要传递行索引:

class CTextBox : public CElement
  {
private:
   //--- 使用字符串的变量
   string            m_temp_input_string;
   //---
private:
   //--- 从字符构建字符串
   string            CollectString(const uint line_index);
  };
//+------------------------------------------------------------------+
//| 从字符构建字符串                                                  |
//+------------------------------------------------------------------+
string CTextBox::CollectString(const uint line_index)
  {
   m_temp_input_string="";
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
   for(uint i=0; i<symbols_total; i++)
      ::StringAdd(m_temp_input_string,m_lines[line_index].m_symbol[i]);
//---
   return(m_temp_input_string);
  }

接下来, 列举文本编辑框的属性, 可用于自定义控件的外观, 以及它的状态和可工作的模式:

  • 不同状态的背景颜色
  • 不同状态的文本颜色
  • 不同状态的边框颜色
  • 省缺文本
  • 省缺文本颜色
  • 多行模式
  • 只读模式
class CTextBox : public CElement
  {
private:
   //--- 背景颜色
   color             m_area_color;
   color             m_area_color_locked;
   //--- 文本颜色
   color             m_text_color;
   color             m_text_color_locked;
   //--- 边框颜色
   color             m_border_color;
   color             m_border_color_hover;
   color             m_border_color_locked;
   color             m_border_color_activated;
   //--- 省缺文本
   string            m_default_text;
   //--- 省缺颜色
   color             m_default_text_color;
   //--- 多行模式
   bool              m_multi_line_mode;
   //--- 只读模式
   bool              m_read_only_mode;
   //---
public:
   //--- 不同状态的背景颜色
   void              AreaColor(const color clr)                { m_area_color=clr;                 }
   void              AreaColorLocked(const color clr)          { m_area_color_locked=clr;          }
   //--- 不同状态的文本颜色
   void              TextColor(const color clr)                { m_text_color=clr;                 }
   void              TextColorLocked(const color clr)          { m_text_color_locked=clr;          }
   //--- 不同状态的边框颜色
   void              BorderColor(const color clr)              { m_border_color=clr;               }
   void              BorderColorHover(const color clr)         { m_border_color_hover=clr;         }
   void              BorderColorLocked(const color clr)        { m_border_color_locked=clr;        }
   void              BorderColorActivated(const color clr)     { m_border_color_activated=clr;     }
   //--- (1) 省缺文本, 和 (2) 省缺文本颜色
   void              DefaultText(const string text)            { m_default_text=text;              }
   void              DefaultTextColor(const color clr)         { m_default_text_color=clr;         }
   //--- (1) 多行模式, (2) 只读模式
   void              MultiLineMode(const bool mode)            { m_multi_line_mode=mode;           }
   bool              ReadOnlyMode(void)                  const { return(m_read_only_mode);         }
   void              ReadOnlyMode(const bool mode)             { m_read_only_mode=mode;            }
  };

文本框本身 (背景, 文本, 边框和闪烁文本光标) 将会在 OBJ_BITMAP_LABEL 类型的单一图形对象上完整绘制。实质上, 这只是一张照片。在两种情况下将会重绘:

  • 当与控件交互时
  • 在指定的时间间隔, 当文本框被激活时, 光标闪烁。 

当鼠标光标悬浮在文本框区域时, 其边框将改变颜色。为了避免过于频繁的重绘图片, 有必要跟踪光标穿越文本框边架的时刻。也就是说, 当光标进入或离开文本框区域时, 控件应重绘一次。出于这些目的, CElementBase::IsMouseFocus() 方法已添加到控件的基类中。它们用于设置和获取代表穿越的标志:

//+------------------------------------------------------------------+
//| 基准控件类                                                       |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- 判断鼠标光标穿越控件边框的时刻
   bool              m_is_mouse_focus;
   //---
public:
   //--- 进入/离开控件焦点的时刻
   bool              IsMouseFocus(void)                        const { return(m_is_mouse_focus);             }
   void              IsMouseFocus(const bool focus)                  { m_is_mouse_focus=focus;               }
  };

为了使代码简单易读, 在其中实现了额外的简单方法, 它有助于获得文本框背景, 边框和文本相对于控件当前状态的颜色: 

class CTextBox : public CElement
  {
private:
   //--- 返回当前背景色
   uint              AreaColorCurrent(void);
   //--- 返回当文本景色
   uint              TextColorCurrent(void);
   //--- 返回当边框景色
   uint              BorderColorCurrent(void);
  };
//+------------------------------------------------------------------+
//| 返回相对于当前控件状态的背景色                                     |
//+------------------------------------------------------------------+
uint CTextBox::AreaColorCurrent(void)
  {
   uint clr=::ColorToARGB((m_text_box_state)? m_area_color : m_area_color_locked);
//--- 返回颜色
   return(clr);
  }
//+------------------------------------------------------------------+
//| 返回相对于当前控件状态的文本色                                     |
//+------------------------------------------------------------------+
uint CTextBox::TextColorCurrent(void)
  {
   uint clr=::ColorToARGB((m_text_box_state)? m_text_color : m_text_color_locked);
//--- 返回颜色
   return(clr);
  }
//+------------------------------------------------------------------+
//| 返回相对于当前控件状态的边框色                                     |
//+------------------------------------------------------------------+
uint CTextBox::BorderColorCurrent(void)
  {
   uint clr=clrBlack;
//--- 如果元素未阻塞
   if(m_text_box_state)
     {
      //--- 如果文本框已激活
      if(m_text_edit_state)
         clr=m_border_color_activated;
      //--- 如果未激活, 检查控件焦点
      else
         clr=(CElementBase::IsMouseFocus())? m_border_color_hover : m_border_color;
     }
//--- 如果控件被阻塞
   else
      clr=m_border_color_locked;
//--- 返回颜色
   return(::ColorToARGB(clr));
  }

在类的许多方法中, 有必要获得相对于指定字体和字号的文本框高度的像素值。为此目的, 使用 CTextBox::LineHeight() 方法:

class CTextBox : public CElement
  {
private:
   //--- 返回线高度
   uint              LineHeight(void);
  };
//+------------------------------------------------------------------+
//| 返回线高度                                                        |
//+------------------------------------------------------------------+
uint CTextBox::LineHeight(void)
  {
//--- 设置在画板上显示的字体 (获取行高所需的字体)
   m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- 返回线高度
   return(m_canvas.TextHeight("|"));
  }

现在研究绘制控件的方法。从用于绘制文本框边框的 CTextBox::DrawBorder() 方法开始。如果文本框的总尺寸大于其可见部分, 则可见区域能够偏移 (使用滚动条或光标)。所以, 边框应考虑这些偏移量。 

class CTextBox : public CElement
  {
private:
   //--- 绘制边框
   void              DrawBorder(void);
  };
//+------------------------------------------------------------------+
//| 绘制文本框的边框                                                  |
//+------------------------------------------------------------------+
void CTextBox::DrawBorder(void)
  {
//--- 获取相对于当前控件状态的边框颜色
   uint clr=BorderColorCurrent();
//--- 获取沿 X 轴的偏移量
   int xo=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int yo=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- 边界
   int x_size =m_canvas.X_Size()-1;
   int y_size =m_canvas.Y_Size()-1;
//--- 坐标: 顶/右/底/左
   int x1[4]; x1[0]=x;         x1[1]=x_size+xo; x1[2]=xo;        x1[3]=x;
   int y1[4]; y1[0]=y;         y1[1]=y;         y1[2]=y_size+yo; y1[3]=y;
   int x2[4]; x2[0]=x_size+xo; x2[1]=x_size+xo; x2[2]=x_size+xo; x2[3]=x;
   int y2[4]; y2[0]=y;         y2[1]=y_size+yo; y2[2]=y_size+yo; y2[3]=y_size+yo;
//--- 按指定坐标绘制边框
   for(int i=0; i<4; i++)
      m_canvas.Line(x1[i],y1[i],x2[i],y2[i],clr);
  }

CTextBox::DrawBorder() 方法也会在 CTextBox::ChangeObjectsColor() 方法里用到, 当鼠标光标悬浮在文本框上时, 有必要简单地改变边框颜色 (参看如下代码)。为此, 只需重绘边框 (而不是整个文本框) 并刷新图像。在控件的事件处理器里将会调用 CTextBox::ChangeObjectsColor()。此处是跟踪到的鼠标光标穿越控件边界的动作, 以避免过于频繁的重画。

class CTextBox : public CElement
  {
private:
   //--- 改变对象颜色
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| 改变对象颜色                                                      |
//+------------------------------------------------------------------+
void CTextBox::ChangeObjectsColor(void)
  {
//--- 如果非焦点
   if(!CElementBase::MouseFocus())
     {
      //--- 如果并未指明非焦点
      if(CElementBase::IsMouseFocus())
        {
         //--- 设置标志
         CElementBase::IsMouseFocus(false);
         //--- 改变颜色
         DrawBorder();
         m_canvas.Update();
        }
     }
   else
     {
      //--- 如果并未指明处于焦点
      if(!CElementBase::IsMouseFocus())
        {
         //--- 设置标志
         CElementBase::IsMouseFocus(true);
         //--- 改变颜色
         DrawBorder();
         m_canvas.Update();
        }
     }
  }

CTextBox::TextOut() 方法设计用于将文本输出到画板。此此, 在开始时, 填充指定颜色来清除画布。接下来, 程序可以有两种途径可走:

  • 如果多行模式被禁用, 并且在同一行中没有字符, 则应显示默认文本 (如果指定)。它将显示在编辑框的中心。
  • 如果多行模式被禁用, 或者行内包含至少一个字符, 则获取行的高度并循环显示所有行, 首先从字符数组构建它们。省缺定义文本框区域左上角的文本偏移量。这些是沿 X5 像素, 以及沿 Y4 像素。这些数值可使用 CTextBox::TextXOffset()CTextBox::TextYOffset() 方法进行覆盖。 
class CTextBox : public CElement
  {
private:
   //--- 文本从文本框边缘偏移
   int               m_text_x_offset;
   int               m_text_y_offset;
   //---
public:
   //--- 文本从文本框边缘偏移
   void              TextXOffset(const int x_offset)           { m_text_x_offset=x_offset;         }
   void              TextYOffset(const int y_offset)           { m_text_y_offset=y_offset;         }
   //---
private:
   //--- 输出到画板
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                           |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_text_x_offset(5),
                           m_text_y_offset(4)
  {
...
  }
//+------------------------------------------------------------------+
//| 输出到画板                                                        |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- 清理画板
   m_canvas.Erase(AreaColorCurrent());
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 如果启用多行模式或字符数大于零
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- 获取线高度
      int line_height=(int)LineHeight();
      //--- 获取行数组大小
      uint lines_total=::ArraySize(m_lines);
      //---
      for(uint i=0; i<lines_total; i++)
        {
         //--- 获取文本坐标
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
         //--- 从字符数组中构建字符串
         CollectString(i);
         //--- 绘制文本
         m_canvas.TextOut(x,y,m_temp_input_string,TextColorCurrent(),TA_LEFT);
        }
     }
//--- 如果多行模式被禁用, 且没有字符, 则显示默认文本
   else
     {
      //--- 绘制文本, 如果指定
      if(m_default_text!="")
         m_canvas.TextOut(m_area_x_size/2,m_area_y_size/2,m_default_text,::ColorToARGB(m_default_text_color),TA_CENTER|TA_VCENTER);
     }
  }

为绘制文本光标, 需要计算其坐标的方法。为计算 X 坐标, 有必要指定行的索引以及光标位置的字符索引。这可通过使用 CTextBox::LineWidth() 方法完成: 因为每个字符的宽度保存在 KeySymbolOptions 结构的 m_width[] 动态数组内, 它仅保留 最大到指定位置的字符累计宽度。 

class CTextBox : public CElement
  {
private:
   //--- 返回线高度的像素
   uint              LineWidth(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| 返回从开始到指定位置的行宽                                         |
//+------------------------------------------------------------------+
uint CTextBox::LineWidth(const uint line_index,const uint symbol_index)
  {
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 防止超出数组大小
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- 获取指定行的字符数组大小
   uint symbols_total=::ArraySize(m_lines[l].m_width);
//--- 防止超出数组大小
   uint s=(symbol_index<symbols_total)? symbol_index : symbols_total;
//--- 累计所有字符的宽度
   uint width=0;
   for(uint i=0; i<s; i++)
      width+=m_lines[l].m_width[i];
//--- 返回行宽
   return(width);
  }

获取文本光标坐标的方法 采取非常简单的形式 (参阅以下代码)。坐标保存在 m_text_cursor_xm_text_cursor_y 字段。坐标的计算使用光标的当前位置, 以及光标将要移至的线和字符索引。m_text_cursor_x_posm_text_cursor_y_pos 字段用于保存这些数值。

class CTextBox : public CElement
  {
private:
   //--- 文本光标的当前坐标
   int               m_text_cursor_x;
   int               m_text_cursor_y;
   //--- 文本光标的当前位置
   uint              m_text_cursor_x_pos;
   uint              m_text_cursor_y_pos;
   //---
private:
   //--- 计算文本光标的坐标
   void              CalculateTextCursorX(void);
   void              CalculateTextCursorY(void);
  };
//+------------------------------------------------------------------+
//| 计算文本光标的 X 坐标                                             |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextCursorX(void)
  {
//--- 获取行宽
   int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
//--- 计算并保存光标的 X 坐标
   m_text_cursor_x=m_text_x_offset+line_width;
  }
//+------------------------------------------------------------------+
//| 计算文本光标的 Y 坐标                                             |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextCursorY(void)
  {
//--- 获取线高度
   int line_height=(int)LineHeight();
//--- 获取光标的 Y 坐标
   m_text_cursor_y=m_text_y_offset+int(line_height*m_text_cursor_y_pos);
  }

实现绘制文本光标的 CTextBox::DrawCursor() 方法一切准备就绪。在许多其它文本编辑器中, 可注意到文本光标与一些字符的像素部分重叠。可以看到, 文本光标并非简单地阻挡它们。字符的被覆盖像素以不同的颜色绘制。这样做是为了保持字符的可读性。 

例如, 下面的屏幕截图显示了文本编辑器中的 'd' 和 'д' 字符重叠, 但不会与光标重叠。

 图例. 6. 文本光标与 'd' 字符像素重叠的示例。

图例. 6. 文本光标与 'd' 字符像素重叠的示例。

 图例. 7. 文本光标与 'д' 字符像素重叠的示例。

图例. 7. 文本光标与 'д' 字符像素重叠的示例。


为了使光标和重叠字符始终在任何颜色的背景上可见, 将光标覆盖像素的颜色反相就足够了。 

现在, 研究绘制文本光标的 CTextBox::DrawCursor() 方法。光标宽度将等于一个像素, 其高度将与行高匹配。在最开始, 获取光标绘制的 X 坐标, 以及行高度。Y 坐标将在循环里计算, 因为它将在每个像素的基础上绘制。记住, 一个处理颜色的 CColors 类的实例已在 CElementBase 控件的基类中预先声明。因此, 在每次迭代中计算 Y 坐标之后即可得到指定坐标的当前像素颜色。然后, CColors::Negative() 方法 将颜色反相, 并 将其设置在相同位置。 

class CTextBox : public CElement
  {
private:
   //--- 绘制文本光标
   void              DrawCursor(void);
  };
//+------------------------------------------------------------------+
//| 绘制文本光标                                                      |
//+------------------------------------------------------------------+
void CTextBox::DrawCursor(void)
  {
//--- 获取线高度
   int line_height=(int)LineHeight();
//--- 获取光标的 X 坐标
   CalculateTextCursorX();
//--- 绘制文本光标
   for(int i=0; i<line_height; i++)
     {
      //--- 获取像素的 Y 坐标
      int y=m_text_y_offset+((int)m_text_cursor_y_pos*line_height)+i;
      //--- 获取像素的当前颜色
      uint pixel_color=m_canvas.PixelGet(m_text_cursor_x,y);
      //--- 光标颜色反相
      pixel_color=m_clr.Negative((color)pixel_color);
      m_canvas.PixelSet(m_text_cursor_x,y,::ColorToARGB(pixel_color));
     }
  }

已经实现了用文本绘制文本框的两种方法: CTextBox::DrawText() 和 CTextBox::DrawTextAndCursor()。 

当只需要更新非活动文本框中的文本时, 使用 CTextBox::DrawText() 方法。一切都很简单。如果控件未消隐, 则显示文本, 绘制边框并更新图片。  

class CTextBox : public CElement
  {
private:
   //--- 绘制文本
   void              DrawText(void);
  };
//+------------------------------------------------------------------+
//| 绘制文本                                                          |
//+------------------------------------------------------------------+
void CTextBox::DrawText(void)
  {
//--- 如果控件被消隐, 离开
   if(!CElementBase::IsVisible())
      return;
//--- 输出文本
   CTextBox::TextOut();
//--- 绘制边框
   DrawBorder();
//--- 更新文本框
   m_canvas.Update();
  }

如果文本框处于激活状态, 除文本外, 还需要显示闪烁的文本光标 - CTextBox::DrawTextAndCursor() 方法。为了闪烁, 有必要判断显示/隐藏光标的状态。每次调用此方法时, 状态将改为相反。当 true 值 (show_state 参数) 被传递到方法时, 它还提供强制显示能力。当文本框中的光标处于激活状态时, 需要强制显示。实际上, 光标闪烁将在计时器控件里按照时间计数器类构造器中指定的间隔执行。在此, 其数值为 200 毫秒。每次调用 CTextBox::DrawTextAndCursor() 方法之后, 计数器必须要重置。 

class CTextBox : public CElement
  {
private:
   //--- 显示文本和闪烁光标
   void              DrawTextAndCursor(const bool show_state=false);
  };
//+------------------------------------------------------------------+
//| 构造器                                                           |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void)
  {
//--- 设置时间计数器参数
   m_counter.SetParameters(16,200);
  }
//+------------------------------------------------------------------+
//| 计时器                                                           |
//+------------------------------------------------------------------+
void CTextBox::OnEventTimer(void)
  {
...
//--- 在文本光标更新之间暂停
   if(m_counter.CheckTimeCounter())
     {
      //--- 如果控件可见, 且文本框已激活, 则更新文本光标
      if(CElementBase::IsVisible() && m_text_edit_state)
         DrawTextAndCursor();
     }
  }
//+------------------------------------------------------------------+
//| 显示文本和闪烁光标                                                |
//+------------------------------------------------------------------+
void CTextBox::DrawTextAndCursor(const bool show_state=false)
  {
//--- 判断文本光标的状态 (显示/隐藏)
   static bool state=false;
   state=(!show_state)? !state : show_state;
//--- 输出文本
   CTextBox::TextOut();
//--- 绘制文本光标
   if(state)
      DrawCursor();
//--- 绘制边框
   DrawBorder();
//--- 更新文本框
   m_canvas.Update();
//--- 重置计数器
   m_counter.ZeroTimeCounter();
  }

若要创建多行文本框控件, 需要三个 private 方法, 需要两个创建滚动条, 以及一个 public 方法用来在所需的自定义类中进行外部调用: 

class CTextBox : public CElement
  {
private:
   //--- 用于创建元素的对象
   CRectCanvas       m_canvas;
   CScrollV          m_scrollv;
   CScrollH          m_scrollh;
   //---
public:
   //--- 创建控件的方法
   bool              CreateTextBox(const long chart_id,const int subwin,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateCanvas(void);
   bool              CreateScrollV(void);
   bool              CreateScrollH(void);
   //---
public:
   //--- 返回指向滚动条的指针
   CScrollV         *GetScrollVPointer(void)                   { return(::GetPointer(m_scrollv));  }
   CScrollH         *GetScrollHPointer(void)                   { return(::GetPointer(m_scrollh));  }
  };

在调用 CTextBox::CreateCanvas() 方法创建文本框之前, 需要计算其大小。在此将应用类似于在 CCanvasTable 类型的渲染表格中实现的方法。我们来简略地重温一下。有图像的总大小, 且有其可见部分的大小。控件大小等于图像可见部分的大小。当移动文本光标或滚动条时, 图像的坐标将会改变, 而可见部分的坐标 (也是控件坐标) 将保持不变。

沿 Y 轴的尺寸可以简单地按照行号乘以它们的高度来计算。此处也考虑了文本框边缘的边距和滚动条的大小。若要计算沿 X 轴的大小, 必须知道整个数组的最大行宽。这可通过使用 CTextBox::MaxLineWidth() 方法完成。此处, 在一个循环中遍历行数组, 如果它大于前一个, 则保存该行的全宽, 并返回该值。

class CTextBox : public CElement
  {
private:
   //--- 返回最大行宽
   uint              MaxLineWidth(void);
  };
//+------------------------------------------------------------------+
//| 返回最大行宽                                                      |
//+------------------------------------------------------------------+
uint CTextBox::MaxLineWidth(void)
  {
   uint max_line_width=0;
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
   for(uint i=0; i<lines_total; i++)
     {
      //--- 获取字符数组的大小
      uint symbols_total=::ArraySize(m_lines[i].m_symbol);
      //--- 获取行宽
      uint line_width=LineWidth(symbols_total,i);
      //--- 保存最大行宽
      if(line_width>max_line_width)
         max_line_width=line_width;
     }
//--- 返回最大行宽
   return(max_line_width);
  }

用于计算控件大小的 CTextBox::CalculateTextBoxSize() 方法的代码如下所示。此方法还将会从 CTextBox::ChangeWidthByRightWindowSide() 和 CTextBox::ChangeHeightByBottomWindowSide() 方法里调用。这些方法的目的是根据表单大小自动调整控件大小, 如果这些属性由开发人员定义。 

class CTextBox : public CElement
  {
private:
   //--- 控件大小和可见部分大小
   int               m_area_x_size;
   int               m_area_y_size;
   int               m_area_visible_x_size;
   int               m_area_visible_y_size;
   //---
private:
   //--- 计算文本框的宽度
   void              CalculateTextBoxSize(void);
  };
//+------------------------------------------------------------------+
//| 计算文本框的大小                                                  |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextBoxSize(void)
  {
//--- 从文本框获取最大行宽
   int max_line_width=int((m_text_x_offset*2)+MaxLineWidth()+m_scrollv.ScrollWidth());
//--- 确定总宽度
   m_area_x_size=(max_line_width>m_x_size)? max_line_width : m_x_size;
//--- 确定可见宽度
   m_area_visible_x_size=m_x_size;
//--- 获取线高度
   int line_height=(int)LineHeight();
//--- 获取行数组大小
   int lines_total=::ArraySize(m_lines);
//--- 计算控件总高度
   int lines_height=int((m_text_y_offset*2)+(line_height*lines_total)+m_scrollh.ScrollWidth());
//--- 确定总高度
   m_area_y_size=(m_multi_line_mode && lines_height>m_y_size)? lines_height : m_y_size;
//--- 确定可见高度
   m_area_visible_y_size=m_y_size;
  }

大小已计算。现在需要应用它们。这可通过使用 CTextBox::ChangeTextBoxSize() 方法来完成。如果需要将可见区域移动到开始或将其保留在相同位置, 在此处指定方法参数。此外, 此方法 调整滚动条, 并相对于滚动条滑块执行最后的 可见区域调整。这些方法的代码将不在此处赘述, 因为在前面的文章中已经描述过类似的情况。 

class CTextBox : public CElement
  {
private:
   //--- 调整文本框的大小
   void              ChangeTextBoxSize(const bool x_offset=false,const bool y_offset=false);
  };
//+------------------------------------------------------------------+
//| 调整文本框的大小                                                  |
//+------------------------------------------------------------------+
void CTextBox::ChangeTextBoxSize(const bool is_x_offset=false,const bool is_y_offset=false)
  {
//--- 调整表格的大小
   m_canvas.XSize(m_area_x_size);
   m_canvas.YSize(m_area_y_size);
   m_canvas.Resize(m_area_x_size,m_area_y_size);
//--- 设置可见区域的大小
   m_canvas.SetInteger(OBJPROP_XSIZE,m_area_visible_x_size);
   m_canvas.SetInteger(OBJPROP_YSIZE,m_area_visible_y_size);
//--- 总宽度和可见区域之间的差值
   int x_different=m_area_x_size-m_area_visible_x_size;
   int y_different=m_area_y_size-m_area_visible_y_size;
//--- 设置图像内边框沿 X 轴和 Y 轴的偏移
   int x_offset=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int y_offset=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
   m_canvas.SetInteger(OBJPROP_XOFFSET,(!is_x_offset)? 0 : (x_offset<=x_different)? x_offset : x_different);
   m_canvas.SetInteger(OBJPROP_YOFFSET,(!is_y_offset)? 0 : (y_offset<=y_different)? y_offset : y_different);
//--- 调整滚动条的大小
   ChangeScrollsSize();
//--- 调整数据
   ShiftData();
  }

以下字段和方法旨在用于管理控件的状态和获取其当前状态:

  • CTextBox::TextEditState() 方法恢复控件的状态。 
  • 调用 CTextBox::TextBoxState() 方法阻塞/解封控件。阻塞控件被转至只读模式。为背景, 边框和文本设置相应的颜色 (这可由用户在创建控件之前完成)。 
class CTextBox : public CElement
  {
private:
   //--- 只读模式
   bool              m_read_only_mode;
   //--- 文本编辑框的状态
   bool              m_text_edit_state;
   //--- 控件状态
   bool              m_text_box_state;
   //---
public:
   //--- (1) 文本编辑框状态, (2) 获取/设置控件可用状态
   bool              TextEditState(void)                 const { return(m_text_edit_state);        }
   bool              TextBoxState(void)                  const { return(m_text_box_state);         }
   void              TextBoxState(const bool state);
  };
//+------------------------------------------------------------------+
//| 设置控件可用状态                                                  |
//+------------------------------------------------------------------+
void CTextBox::TextBoxState(const bool state)
  {
   m_text_box_state=state;
//--- 相对于当前状态的设置
   if(!m_text_box_state)
     {
      //--- 优先级
      m_canvas.Z_Order(-1);
      //--- 只读模式下的编辑框
      m_read_only_mode=true;
     }
   else
     {
      //--- 优先级
      m_canvas.Z_Order(m_text_edit_zorder);
      //--- 编辑模式下的编辑控件
      m_read_only_mode=false;
     }
//--- 更新文本框
   DrawText();
  }

 


管理文本光标

文本编辑框在单击时激活。点击位置的坐标立即确定, 并且文本光标移至那里。这是通过 CTextBox::OnClickTextBox() 方法完成的。但在继续描述之前, 首先考虑一些其内调用的辅助方法, 以及 CTextBox 类的许多其它方法。

CTextBox::SetTextCursor() 方法用于更新文本光标位置的值。在单行模式中, 其位置在 Y 轴上永远等于 0

class CTextBox : public CElement
  {
private:
   //--- 文本光标的当前位置
   uint              m_text_cursor_x_pos;
   uint              m_text_cursor_y_pos;
   //---
private:
   //--- 将光标置于指定位置
   void              SetTextCursor(const uint x_pos,const uint y_pos);
  };
//+------------------------------------------------------------------+
//| 将光标置于指定位置                                                |
//+------------------------------------------------------------------+
void CTextBox::SetTextCursor(const uint x_pos,const uint y_pos)
  {
   m_text_cursor_x_pos=x_pos;
   m_text_cursor_y_pos=(!m_multi_line_mode)? 0 : y_pos;
  }

控制滚动条的方法。类似的方法已经在本系列的前一篇文章中介绍过, 因此代码将不会在这里显示。简要提醒: 如果未传递参数, 则缩略图将移至最后一个位置, 即到列表/文本/文档的末尾。 

class CTextBox : public CElement
  {
public:
   //--- 表格滚动: (1) 垂直, 和 (2) 水平
   void              VerticalScrolling(const int pos=WRONG_VALUE);
   void              HorizontalScrolling(const int pos=WRONG_VALUE);
  };

CTextBox::DeactivateTextBox() 是文本框所失活所需的。这里应该提一提终端开发人员提供的一个新功能。另一个图表标识符 (CHART_KEYBOARD_CONTROL) 已被添加到 ENUM_CHART_PROPERTY 枚举中。它启用或禁用通过 '左', '右', '起始', '结束', '上一页', '下一页' 按键来管理图表, 以及图表缩放键 - '+' 和 '-'。因此, 当文本框被激活时, 需要禁用图表管理特征, 使得所列出的键不被拦截, 且又不会打断文本框的操作。当文字框失活时, 必须 重新启用 通过键盘管理图表。 

此处, 必须 重绘文本框, 若非多行模式, 请将文本光标和滚动条滑块移至行首。 

class CTextBox : public CElement
  {
private:
   //--- 文本框失活
   void              DeactivateTextBox(void);
  };
//+------------------------------------------------------------------+
//| 文本框失活                                                        |
//+------------------------------------------------------------------+
void CTextBox::DeactivateTextBox(void)
  {
//--- 如果已失活, 离开
   if(!m_text_edit_state)
      return;
//--- 失活
   m_text_edit_state=false;
//--- 启用图表管理
   m_chart.SetInteger(CHART_KEYBOARD_CONTROL,true);
//--- 绘制文本
   DrawText();
//--- 如果多行模式被禁用
   if(!m_multi_line_mode)
     {
      //--- 将光标移至行首
      SetTextCursor(0,0);
      //--- 将滚动条移至行首
      HorizontalScrolling(0);
     }
  }

当管理文本光标时, 需要跟踪它是否已经越过可见区域的边界。如果交汇发生, 光标必须再次返回到可见区域。为此目的, 需要额外的可重用方法。必须计算文本框的允许边界, 同时考虑多行模式和滚动条的存在。 

为了计算可见区域要移动多少, 必须首先找到 当前偏移 值: 

class CTextBox : public CElement
  {
private:
   //--- 用于计算文本框的可见区域边界
   int               m_x_limit;
   int               m_y_limit;
   int               m_x2_limit;
   int               m_y2_limit;
   //---
private:
   //--- 计算文本框边界
   void              CalculateBoundaries(void);
   void              CalculateXBoundaries(void);
   void              CalculateYBoundaries(void);
  };
//+------------------------------------------------------------------+
//| 计算文本框沿两个轴的边界                                           |
//+------------------------------------------------------------------+
void CTextBox::CalculateBoundaries(void)
  {
   CalculateXBoundaries();
   CalculateYBoundaries();
  }
//+------------------------------------------------------------------+
//| 计算文本框沿 X 轴的边界                                           |
//+------------------------------------------------------------------+
void CTextBox::CalculateXBoundaries(void)
  {
//--- 获取沿 X 轴的 X 坐标和偏移量
   int x       =(int)m_canvas.GetInteger(OBJPROP_XDISTANCE);
   int xoffset =(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
//--- 计算文本框可见部分的边界
   m_x_limit  =(x+xoffset)-x;
   m_x2_limit =(m_multi_line_mode)? (x+xoffset+m_x_size-m_scrollv.ScrollWidth()-m_text_x_offset)-x : (x+xoffset+m_x_size-m_text_x_offset)-x;
  }
//+------------------------------------------------------------------+
//| 计算文本框沿 Y 轴的边界                                           |
//+------------------------------------------------------------------+
void CTextBox::CalculateYBoundaries(void)
  {
//--- 如果禁用多行模式, 则离开
   if(!m_multi_line_mode)
      return;
//--- 获取 Y 轴的 Y 坐标和偏移量
   int y       =(int)m_canvas.GetInteger(OBJPROP_YDISTANCE);
   int yoffset =(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- 计算文本框可见部分的边界
   m_y_limit  =(y+yoffset)-y;
   m_y2_limit =(y+yoffset+m_y_size-m_scrollh.ScrollWidth())-y;
  }

为了精确地定位滚动条相对于当前光标的位置, 将使用以下方法: 

class CTextBox : public CElement
  {
private:
   //--- 计算文本框左边缘上滚动条滑块的 X 位置
   int               CalculateScrollThumbX(void);
   //--- 计算文本框右边缘上滚动条滑块的 X 位置
   int               CalculateScrollThumbX2(void);
   //--- 计算文本框顶部滚动条滑块的 Y 位置
   int               CalculateScrollThumbY(void);
   //--- 计算文本框底部滚动条滑块的 Y 位置
   int               CalculateScrollThumbY2(void);
  };
//+------------------------------------------------------------------+
//| 计算文本框左边缘上滚动条滑块的 X 位置                              |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbX(void)
  {
   return(m_text_cursor_x-m_text_x_offset);
  }
//+------------------------------------------------------------------+
//| 计算文本框右边缘上滚动条滑块的 X 位置                              |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbX2(void)
  {
   return((m_multi_line_mode)? m_text_cursor_x-m_x_size+m_scrollv.ScrollWidth()+m_text_x_offset : m_text_cursor_x-m_x_size+m_text_x_offset*2);
  }
//+------------------------------------------------------------------+
//| 计算文本框顶部滚动条滑块的 Y 位置                                  |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbY(void)
  {
   return(m_text_cursor_y-m_text_y_offset);
  }
//+------------------------------------------------------------------+
//| 计算文本框底部滚动条滑块的 Y 位置                                  |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbY2(void)
  {
//--- 设置在画板上显示的字体 (获取行高所需的字体)
   m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- 获取线高度
   int line_height=m_canvas.TextHeight("|");
//--- 计算并返回数值
   return(m_text_cursor_y-m_y_size+m_scrollh.ScrollWidth()+m_text_y_offset+line_height);
  }

我们来点击文本框生成事件, 这明确表示文本框被激活。还需要接收文本框内光标移动相应的事件。添加新标识符至 Defines.mqh 文件: 

  • ON_CLICK_TEXT_BOX 指定激活文本框的事件。
  • ON_MOVE_TEXT_CURSOR 指定移动文本光标的事件。 
//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_CLICK_TEXT_BOX          (31) // 激活文本框
#define ON_MOVE_TEXT_CURSOR        (32) // 移动文本光标

文本光标的当前位置将作为使用这些标识符的附加信息放置到 string 参数。这已经在许多其它文本编辑器中实现, 包括 MetaEditor。下面的屏幕截图显示生成字符串并显示在代码编辑器状态栏中的示例。


 

图例. 8. 文本光标在 MetaEditor 中的位置。

下面的列表显示了 CTextBox::TextCursorInfo() 方法的代码, 该代码返回如上面截图所示的一个格式化字符串。还展示了可用于获得行数, 指定行内字符数, 以及文本光标当前位置的附加方法。 

class CTextBox : public CElement
  {
private:
   //--- 返回的索引 (1) 行, (2) 文本光标所在的字符,
   //    (3) 行数, (4) 指定行内字符数
   uint              TextCursorLine(void)                      { return(m_text_cursor_y_pos);      }
   uint              TextCursorColumn(void)                    { return(m_text_cursor_x_pos);      }
   uint              LinesTotal(void)                          { return(::ArraySize(m_lines));     }
   uint              ColumnsTotal(const uint line_index);
   //--- 有关文本光标的信息(行/行数, 列/列数)
   string            TextCursorInfo(void);
  };
//+------------------------------------------------------------------+
//| 返回指定行内字符数                                                |
//+------------------------------------------------------------------+
uint CTextBox::ColumnsTotal(const uint line_index)
  {
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 防止超出数组大小
   uint check_index=(line_index<lines_total)? line_index : lines_total-1;
//--- 获取行内字符数组的大小
   uint symbols_total=::ArraySize(m_lines[check_index].m_symbol);
//--- 返回字符数
   return(symbols_total);
  }
//+------------------------------------------------------------------+
//| 关于文本光标的信息                                                |
//+------------------------------------------------------------------+
string CTextBox::TextCursorInfo(void)
  {
//--- 字符串组件
   string lines_total        =(string)LinesTotal();
   string columns_total      =(string)ColumnsTotal(TextCursorLine());
   string text_cursor_line   =string(TextCursorLine()+1);
   string text_cursor_column =string(TextCursorColumn()+1);
//--- 生成字符串
   string text_box_info="Ln "+text_cursor_line+"/"+lines_total+", "+"Col "+text_cursor_column+"/"+columns_total;
//--- 返回字符串
   return(text_box_info);
  }

现在提供 CTextBox::OnClickTextBox() 方法描述的一切准备均已就绪, 这在本节开头已经提到 (见下面的代码)。此处, 在最开始, 检查鼠标左键点击处对象的名称。若结果并非点击文本框, 则传递编辑已结束的消息 (ON_END_EDIT 事件识别码), 以防止文本框仍然激活。然后, 令文本框失活并离开方法。

如果点击在文本框中, 则还有两个检查项。如果启用只读模式或如果控件被阻塞, 则程序退出该方法。如果其中一个条件为假, 则转至方法的主代码。

首先, 使用键盘管理图表已禁用。然后 (1) 获得控件可见区域的当前偏移, (2) 确定发生点击处的相对坐标。在方法的主循环中计算还需要行高。 

首先, 在一个循环中搜索点击发生的行。仅当 计算出的点击处 Y 坐标位于行的上、下边界之间时, 才开始搜索字符。若结果是这行未包含字符, 文本光标和水平滚动条应移至行的开头。这将导致循环停止。

如果该行包含字符, 第二个循环开始, 搜索发生点击的字符。这里的搜索原理几乎与行的情况相同。仅有的区别是, 在每次迭代中获得字符宽度, 因为并非所有字体对于所有字符具有相同的宽度。如果找到被点击的字符, 将文本光标设置到字符位置并完成搜索。如果未找到此行中的字符, 且到达最后一个字符, 则将光标移至该行的最后一个位置, 此处尚未存在字符, 并完成搜索。 

下一步, 如果启用多行模式, 则需要检查文本光标 (至少部分地) 是否超过 Y 轴的文本框可见区域的边界。如果是, 则相对于文本光标的位置调整可见区域。然后, 将文本框标记为已激活, 并 重绘。 

并且在 CTextBox::OnClickTextBox() 方法的最末尾生成一个事件, 指示文本框已激活 (ON_CLICK_TEXT_BOX 事件标识符)。为了提供明确的标识, 还发送控件的 (1) 标识符, (2) 控件的索引, 和 — 额外的 — (3) 光标位置的信息。 

class CTextBox : public CElement
  {
private:
   //--- 处理元素上的按压
   bool              OnClickTextBox(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| 点击控件处理                                                      |
//+------------------------------------------------------------------+
bool CTextBox::OnClickTextBox(const string clicked_object)
  {
//--- 如果它有不同的对象名称, 离开
   if(m_canvas.Name()!=clicked_object)
     {
      //--- 如果文本框已激活, 则发送有关文本框中行编辑模式结束的消息
      if(m_text_edit_state)
         ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
      //--- 文本框失活
      DeactivateTextBox();
      return(false);
     }
//--- 如果 (1) 只读模式被启用, 或(2) 控件被阻塞, 离开
   if(m_read_only_mode || !m_text_box_state)
      return(true);
//--- 禁用图表管理
   m_chart.SetInteger(CHART_KEYBOARD_CONTROL,false);
//--- 获取 X 和 Y 轴上的偏移量
   int xoffset=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- 确定鼠标光标之下的文本编辑框坐标
   int x =m_mouse.X()-m_canvas.X()+xoffset;
   int y =m_mouse.Y()-m_canvas.Y()+yoffset;
//--- 获取线高度
   int line_height=(int)LineHeight();
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 确定点击的字符
   for(uint l=0; l<lines_total; l++)
     {
      //--- 设置条件检查的初始坐标
      int x_offset=m_text_x_offset;
      int y_offset=m_text_y_offset+((int)l*line_height);
      //--- 检查 Y 轴条件
      bool y_pos_check=(l<lines_total-1)?(y>=y_offset && y<y_offset+line_height) : y>=y_offset;
      //--- 如果点击不在此行, 则转到下一行
      if(!y_pos_check)
         continue;
      //--- 获取字符数组的大小
      uint symbols_total=::ArraySize(m_lines[l].m_width);
      //--- 如果这是一个空行, 将光标移至指定位置并离开循环
      if(symbols_total<1)
        {
         SetTextCursor(0,l);
         HorizontalScrolling(0);
         break;
        }
      //--- 查找被点击的字符
      for(uint s=0; s<symbols_total; s++)
        {
         //--- 如果找到该字符, 将光标移至指定位置并离开循环
         if(x>=x_offset && x<x_offset+m_lines[l].m_width[s])
           {
            SetTextCursor(s,l);
            l=lines_total;
            break;
           }
         //--- 添加当前字符的宽度以便进行下一项检查
         x_offset+=m_lines[l].m_width[s];
         //--- 如果这是最后一个字符, 将光标移至行结尾并离开循环
         if(s==symbols_total-1 && x>x_offset)
           {
            SetTextCursor(s+1,l);
            l=lines_total;
            break;
           }
        }
     }
//--- 如果启用了多行文本框模式
   if(m_multi_line_mode)
     {
      //--- 获取文本框可见部分的边界
      CalculateYBoundaries();
      //--- 获取光标的 Y 坐标
      CalculateTextCursorY();
      //--- 如果文本光标离开可见区域, 则移动滚动条
      if(m_text_cursor_y<=m_y_limit)
         VerticalScrolling(CalculateScrollThumbY());
      else
        {
         if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
            VerticalScrolling(CalculateScrollThumbY2());
        }
     }
//--- 激活文本框
   m_text_edit_state=true;
//--- 更新文本和光标
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_CLICK_TEXT_BOX,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }



输入字符

现在来研究 CTextBox::OnPressedKey() 方法。它处理按键, 如果被按下的键包含一个字符, 则必须将其插入到文本光标当前位置的行内。需要额外方法来增加 KeySymbolOptions 结构中数组的大小, 将文本框中输入的字符添加到数组, 以及将字符的宽度作为元素添加到数组。

CTextBox 类的众多方法中, 一个相当简单的 CTextBox::ArraysResize() 方法将会用来调整数组大小:

class CTextBox : public CElement
  {
private:
   //--- 调整指定行的属性数组大小
   void              ArraysResize(const uint line_index,const uint new_size);
  };
//+------------------------------------------------------------------+
//| 调整指定行的属性数组大小                                           |
//+------------------------------------------------------------------+
void CTextBox::ArraysResize(const uint line_index,const uint new_size)
  {
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 防止超出数组大小
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- 设置结构数组的大小
   ::ArrayResize(m_lines[line_index].m_width,new_size);
   ::ArrayResize(m_lines[line_index].m_symbol,new_size);
  }

CTextBox::AddSymbol() 方法用于将新输入的字符添加到文本框中。让我们更仔细地看看它。当输入新字符时, 数组的大小应增加一个元素。文本光标的当前位置可以是字符串的任何字符。因此, 在向数组添加字符之前, 首先需要将当前文本光标位置右侧的所有字符向右移动一个索引。之后, 将输入的字符保存在文本光标的位置。在方法结束时, 将文本光标向右移动一个字符。 

class CTextBox : public CElement
  {
private:
   //--- 将字符及其属性添加到结构的数组内
   void              AddSymbol(const string key_symbol);
  };
//+------------------------------------------------------------------+
//| 将字符及其属性添加到结构的数组内                                   |
//+------------------------------------------------------------------+
void CTextBox::AddSymbol(const string key_symbol)
  {
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 调整数组大小
   ArraysResize(m_text_cursor_y_pos,symbols_total+1);
//--- 从数组末尾到所添加字符处索引的所有字符将被平移
   for(uint i=symbols_total; i>m_text_cursor_x_pos; i--)
     {
      m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i-1];
      m_lines[m_text_cursor_y_pos].m_width[i]  =m_lines[m_text_cursor_y_pos].m_width[i-1];
     }
//--- 获取字符的宽度
   int width=m_canvas.TextWidth(key_symbol);
//--- 将字符添加到空元素
   m_lines[m_text_cursor_y_pos].m_symbol[m_text_cursor_x_pos] =key_symbol;
   m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos]  =width;
//--- 增加光标位置计数器
   m_text_cursor_x_pos++;
  }

下面的列表显示了 CTextBox::OnPressedKey() 方法的代码。如果文本框被激活, 则 尝试通过传递给方法的键代码获取字符。如果按下的键不包含字符, 则程序退出该方法。如果对应一个字符, 那么将其与属性一起添加到数组。当输入字符时, 文本框大小可能已更改, 因此将计算新值并设置。之后, 获取文本框的边界和文本光标的当前坐标。如果光标超出文本框的右边缘, 则调整水平滚动条滑块的位置。然后 文本框重绘并强制显示 (true) 文本光标。在 CTextBox::OnPressedKey() 方法的最末尾, 移动文本光标事件 (ON_MOVE_TEXT_CURSOR) 生成, 其会带有控件标识符、控件索引和附加信息。 

class CTextBox : public CElement
  {
private:
   //--- 处理按键
   bool              OnPressedKey(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按键                                                          |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKey(const long key_code)
  {
//--- 如果文本框未激活, 离开
   if(!m_text_edit_state)
      return(false);
//--- 获取按键对应字符
   string pressed_key=m_keys.KeySymbol(key_code);
//--- 如果非字符, 离开
   if(pressed_key=="")
      return(false);
//--- 添加字符及其属性
   AddSymbol(pressed_key);
//--- 计算文本框的大小
   CalculateTextBoxSize();
//--- 为文本框设置新大小
   ChangeTextBoxSize(true,true);
//--- 获取文本框可见部分的边界
   CalculateXBoundaries();
//--- 获取光标的 X 坐标
   CalculateTextCursorX();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


处理按退格键

现在研究通过按退格键删除字符时的情况。在此情况下, 多行文本框控件的事件处理程序将调用 CTextBox::OnPressedKeyBackspace() 方法。其操作将需要额外的方法, 这在以前没有研究过。首先, 它们的代码将被表现。

字符会使用 CTextBox::DeleteSymbol() 方法删除。在开始时, 它检查当前行是否包含至少一个字符。如果没有, 则文本光标放置在行的开始处, 并退出该方法。如果仍然有一些字符, 则获取上一个字符的位置。这是索引, 从这里起所有字符向左移动一个元素。之后, 文本光标也向左移动一个位置。在方法结束时, 数组的大小减少一个元素

class CTextBox : public CElement
  {
private:
   //--- 删除一个字符
   void              DeleteSymbol(void);
  };
//+------------------------------------------------------------------+
//| 删除一个字符                                                      |
//+------------------------------------------------------------------+
void CTextBox::DeleteSymbol(void)
  {
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 如果数组为空
   if(symbols_total<1)
     {
      //--- 将光标设置到光标所在行的零位置
      SetTextCursor(0,m_text_cursor_y_pos);
      return;
     }
//--- 获取上一个字符的位置
   int check_pos=(int)m_text_cursor_x_pos-1;
//--- 如果超出范围, 离开
   if(check_pos<0)
      return;
//--- 从删除字符的索引处到数组的结尾, 将所有字符向左移动一个元素
   for(uint i=check_pos; i<symbols_total-1; i++)
     {
      m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i+1];
      m_lines[m_text_cursor_y_pos].m_width[i]  =m_lines[m_text_cursor_y_pos].m_width[i+1];
     }
//--- 减少光标位置计数器
   m_text_cursor_x_pos--;
//--- 调整数组大小
   ArraysResize(m_text_cursor_y_pos,symbols_total-1);
  }

如果文本光标位于行的开头, 并且不是第一行, 则需要删除当前行并将下面所有行向上移动一个位置。如果删除的行有字符, 它们需要追加到更高的一行内。另一个附加方法是用于此操作 — CTextBox::ShiftOnePositionUp()。还需要辅助的 CTextBox::LineCopy() 方法以便略微灵活地复制行。 

class CTextBox : public CElement
  {
private:
   //--- 将指定 (源) 行的副本复制到新位置 (目标)
   void              LineCopy(const uint destination,const uint source);
  };
//+------------------------------------------------------------------+
//| 调整指定行的属性数组大小                                           |
//+------------------------------------------------------------------+
void CTextBox::LineCopy(const uint destination,const uint source)
  {
   ::ArrayCopy(m_lines[destination].m_width,m_lines[source].m_width);
   ::ArrayCopy(m_lines[destination].m_symbol,m_lines[source].m_symbol);
  }

下面给出了 CTextBox::ShiftOnePositionUp() 方法的代码。方法的第一次循环将光标当前位置以下的所有行向上移动一个位置。在第一次迭代中, 需要检查行中是否包含字符, 如果是, 则 将它们追加保存到上一行中。一旦行被移动, 行数组减少一个元素。文本光标移至上一行的结尾。 

CTextBox::ShiftOnePositionUp() 方法的最后一块意在将删除行的字符追加到上一行。如果有一行要追加, 则使用 ::StringToCharArray() 函数以字符编码的形式将其传送到 uchar 类型的临时数组中。然后, 将当前行的数组增加所添加的字符数。为了完成操作, 交替地将字符及其属性添加到数组。uchar 类型的数组转换为字符编码要执行 ::CharToString() 函数。 

class CTextBox : public CElement
  {
private:
   //--- 将行向上移动一个位置
   void              ShiftOnePositionUp(void);
  };
//+------------------------------------------------------------------+
//| 将行向上移动一个位置                                              |
//+------------------------------------------------------------------+
void CTextBox::ShiftOnePositionUp(void)
  {
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 从下一个元素开始将行向上移动一个位置
   for(uint i=m_text_cursor_y_pos; i<lines_total-1; i++)
     {
      //--- 在第一次迭代内
      if(i==m_text_cursor_y_pos)
        {
         //--- 获取字符数组的大小
         uint symbols_total=::ArraySize(m_lines[i].m_symbol);
         //--- 如果此行内有字符, 则保存它们以便追加到上一行
         m_temp_input_string=(symbols_total>0)? CollectString(i) : "";
        }
      //--- 行数组的下一个元素索引
      uint next_index=i+1;
      //--- 获取字符数组的大小
      uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
      //--- 调整数组大小
      ArraysResize(i,symbols_total);
      //--- 开始行的复制
      LineCopy(i,next_index);
     }
//--- 调整行数组大小
   uint new_size=lines_total-1;
   ::ArrayResize(m_lines,new_size);
//--- 减少行计数器
   m_text_cursor_y_pos--;
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 光标移至末尾
   m_text_cursor_x_pos=symbols_total;
//--- 获取光标的 X 坐标
   CalculateTextCursorX();
//--- 如果有一行必须追加到前一行
   if(m_temp_input_string!="")
     {
      //--- 将行传送到数组
      uchar array[];
      int total=::StringToCharArray(m_temp_input_string,array)-1;
      //--- 获取字符数组的大小
      symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //--- 调整数组大小
      new_size=symbols_total+total;
      ArraysResize(m_text_cursor_y_pos,new_size);
      //--- 将数据添加到结构的数组
      for(uint i=m_text_cursor_x_pos; i<new_size; i++)
        {
         m_lines[m_text_cursor_y_pos].m_symbol[i] =::CharToString(array[i-m_text_cursor_x_pos]);
         m_lines[m_text_cursor_y_pos].m_width[i]  =m_canvas.TextWidth(m_lines[m_text_cursor_y_pos].m_symbol[i]);
        }
     }
  }

一旦所有辅助方法准备就绪, CTextBox::OnPressedKeyBackspace() 方法的主代码似乎不太复杂。在最开始处, 检查是否按下删除键并激活文本框。如果检查通过, 则查看文本光标当前所在的位置。如果此时不在行的开头, 则 删除前一个字符。然而, 如果它在行的开始处并且不是第一行, 则 将后面所有行向上移动一个位置, 删除当前行。 

之后, 计算和设置文本框的新大小。获得文本光标的边界和坐标。如果文本光标离开可见区域, 则调整滚动条滑块。并且, 最后, 控件重绘并强制显示文本光标, 且生成 关于光标平移的消息。 

class CTextBox : public CElement
  {
private:
   //--- 处理按退格键
   bool              OnPressedKeyBackspace(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按退格键                                                      |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyBackspace(const long key_code)
  {
//--- 如果不是删除键, 或如果文本框没有激活, 离开
   if(key_code!=KEY_BACKSPACE || !m_text_edit_state)
      return(false);
//--- 如果位置大于零, 则删除字符
   if(m_text_cursor_x_pos>0)
      DeleteSymbol();
//--- 如果位置为零, 并且不是第一行, 删除行
   else if(m_text_cursor_y_pos>0)
     {
      //--- 将行向上移动一个位置
      ShiftOnePositionUp();
     }
//--- 计算文本框的大小
   CalculateTextBoxSize();
//--- 为文本框设置新大小
   ChangeTextBoxSize(true,true);
//--- 获取文本框可见部分的边界
   CalculateBoundaries();
//--- 获取光标的 X 和 Y 坐标
   CalculateTextCursorX();
   CalculateTextCursorY();  
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      if(m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
   else
      VerticalScrolling(m_scrollv.CurrentPos());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


处理按回车键

如果启用多行模式并按下回车键, 则需要添加新行, 并且文本光标当前位置下面的所有行必须向下移动一个位置。行的移动需要一个单独的辅助 CTextBox::ShiftOnePositionDown() 方法, 以及一个额外的方法清除行 - CTextBox::ClearLine()。

class CTextBox : public CElement
  {
private:
   //--- 清除指定行
   void              ClearLine(const uint line_index);
  };
//+------------------------------------------------------------------+
//| 清除指定行                                                        |
//+------------------------------------------------------------------+
void CTextBox::ClearLine(const uint line_index)
  {
   ::ArrayFree(m_lines[line_index].m_width);
   ::ArrayFree(m_lines[line_index].m_symbol);
  }

现在, 我们来详细检查 CTextBox::ShiftOnePositionDown() 方法的算法。首先, 需要在回车键按下之处在行中保存字符数。如此, 以及文本光标所在行的位置定义了如何处理 CTextBox::ShiftOnePositionDown() 方法的算法。之后, 将文本光标移至一个新行, 并将行数组的大小增加一个元素。然后, 从当前行开始的所有行必须在一个循环中从数组结尾开始向下移动一个位置。在最后一次迭代中, 按回车键的行中不包含任何字符, 则需要清除文本光标当前所在的行。清除的行是行的副本, 向下移动一个位置的行, 其内容已经存在于下一行上。

在方法开始时, 我们将字符数保存在按回车键的行中。若结果是该行包含字符, 则有必要找出文本光标此刻所处的位置。若结果表明它不在行的结尾, 那么有必要计算从文本光标的当前位置开始到行尾要移到新行的字符数。为此目的, 在此使用一个 临时数组, 在其内 复制字符, 稍后将会 移至新的一行

class CTextBox : public CElement
  {
private:
   //--- 将行向下移动一个位置
   void              ShiftOnePositionDown(void);
  };
//+------------------------------------------------------------------+
//| 将行向下移动一个位置                                              |
//+------------------------------------------------------------------+
void CTextBox::ShiftOnePositionDown(void)
  {
//--- 从按下回车键之处开始, 获取行中字符数组的大小
   uint pressed_line_symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 增加行计数器
   m_text_cursor_y_pos++;
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 将数组增加一个元素
   uint new_size=lines_total+1;
   ::ArrayResize(m_lines,new_size);
//--- 从当前位置开始向下移动一个项目 (从数组的末尾开始)
   for(uint i=lines_total; i>m_text_cursor_y_pos; i--)
     {
      //--- 行数组里前一个元素的索引
      uint prev_index=i-1;
      //--- 获取字符数组的大小
      uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
      //--- 调整数组大小
      ArraysResize(i,symbols_total);
      //--- 开始行的复制
      LineCopy(i,prev_index);
      //--- 清除新行
      if(prev_index==m_text_cursor_y_pos && pressed_line_symbols_total<1)
         ClearLine(prev_index);
     }
//--- 如果回车键并非在空行上按下
   if(pressed_line_symbols_total>0)
     {
      //--- 行的索引, 按回车键的位置
      uint prev_line_index=m_text_cursor_y_pos-1;
      //--- 从光标的当前位置开始到行尾复制字符用的数组
      string array[];
      //--- 设置数组的大小等于必须移动到新行的字符数
      uint new_line_size=pressed_line_symbols_total-m_text_cursor_x_pos;
      ::ArrayResize(array,new_line_size);
      //--- 将要移动到新行的字符复制到数组中
      for(uint i=0; i<new_line_size; i++)
         array[i]=m_lines[prev_line_index].m_symbol[m_text_cursor_x_pos+i];
      //--- 为按下回车键的行调整结构数组的大小
      ArraysResize(prev_line_index,pressed_line_symbols_total-new_line_size);
      //--- 调整新行的结构数组大小
      ArraysResize(m_text_cursor_y_pos,new_line_size);
      //--- 将数据添加到新行的结构数组
      for(uint k=0; k<new_line_size; k++)
        {
         m_lines[m_text_cursor_y_pos].m_symbol[k] =array[k];
         m_lines[m_text_cursor_y_pos].m_width[k]  =m_canvas.TextWidth(array[k]);
        }
     }
  }

处理按回车键的一切都已准备就绪。现在研究 CTextBox::OnPressedKeyEnter() 方法。在最开始时, 检查是否按下回车键, 且所在文本框是否激活。然后, 如果所有检查都通过, 并且它是一个单行文本框, 简单地完成工作。为达此目的, 通过发送带有 ON_END_EDIT 标识符的事件接触激活, 并离开方法。

如果启用了多行模式, 则从文本光标的当前位置起 将所有下面的行向下移动一个位置。调整文本框大小之后, 则进行检查文本光标是否超过可见区域的下边界。此外, 文本光标被放置在行首。在方法结束时, 将重绘文本框并发送一条消息, 告知文本光标已移动。 

class CTextBox : public CElement
  {
private:
   //--- 处理按下回车键
   bool              OnPressedKeyEnter(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按下回车键                                                    |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyEnter(const long key_code)
  {
//--- 如果不是回车键, 或如果文本框未激活, 离开
   if(key_code!=KEY_ENTER || !m_text_edit_state)
      return(false);
//--- 如果多行模式被禁用
   if(!m_multi_line_mode)
     {
      //--- 文本框失活
      DeactivateTextBox();
      //--- 发送有关它的消息
      ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
      return(false);
     }
//--- 将行向下移动一个位置
   ShiftOnePositionDown();
//--- 计算文本框的大小
   CalculateTextBoxSize();
//--- 为文本框设置新大小
   ChangeTextBoxSize();
//--- 获取文本框可见部分的边界
   CalculateYBoundaries();
//--- 获取光标的 Y 坐标
   CalculateTextCursorY();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
      VerticalScrolling(CalculateScrollThumbY2());
//--- 将光标移至行首
   SetTextCursor(0,m_text_cursor_y_pos);
//--- 将滚动条移至开头
   HorizontalScrolling(0);
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


处理按左、右键

按左或右键, 文本光标在相应方向移动一个字符。要完成此操作, 首先需要一个附加的 CTextBox::CorrectingTextCursorXPos() 方法, 它将调整文本光标的位置。此方法也将用于类的其它方法。

class CTextBox : public CElement
  {
private:
   //--- 沿 X 轴调整文本光标
   void              CorrectingTextCursorXPos(const int x_pos=WRONG_VALUE);
  };
//+------------------------------------------------------------------+
//| 沿 X 轴调整文本光标                                               |
//+------------------------------------------------------------------+
void CTextBox::CorrectingTextCursorXPos(const int x_pos=WRONG_VALUE)
  {
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width);
//--- 确定光标位置
   uint text_cursor_x_pos=0;
//--- 如果位置可用
   if(x_pos!=WRONG_VALUE)
      text_cursor_x_pos=(x_pos>(int)symbols_total-1)? symbols_total : x_pos;
//--- 如果位置不可用, 将光标设置为行的结尾
   else
      text_cursor_x_pos=symbols_total;
//--- 零位置, 如果该行不包含字符
   m_text_cursor_x_pos=(symbols_total<1)? 0 : text_cursor_x_pos;
//--- 获取光标的 X 坐标
   CalculateTextCursorX();
  }

下面的代码显示了处理按下左键的 TextBox::OnPressKeyLeft() 方法的代码。如果按下其它键, 或者文本框未激活, 以及此时若是按下 Ctrl 键, 程序将退出此方法。在本文的另一章节中将研究同时按下 Ctrl 键的处理。 

如果第一次检查通过, 则查看文本光标的位置。如果它的 位置不在行首, 则将其移至上一个字符。如果它在行首, 且若此行不是第一行, 则文本光标必须移动到上一行的末尾。之后, 调整水平和垂直滚动条的滑块, 重绘文本框, 并发送有关移动文本光标的消息。

class CTextBox : public CElement
  {
private:
   //--- 处理按下左键
   bool              OnPressedKeyLeft(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按下左键                                                      |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- 如果不是左键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- 如果文本光标位置大于零
   if(m_text_cursor_x_pos>0)
     {
      //--- 将其移动到上一个字符
      m_text_cursor_x-=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos-1];
      //--- 减少字符计数器
      m_text_cursor_x_pos--;
     }
   else
     {
      //--- 如果这不是第一行
      if(m_text_cursor_y_pos>0)
        {
         //--- 移动到上一行的末尾
         m_text_cursor_y_pos--;
         CorrectingTextCursorXPos();
        }
     }
//--- 获取文本框可见部分的边界
   CalculateBoundaries();
//--- 获取光标的 Y 坐标
   CalculateTextCursorY();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      //--- 获取字符数组的大小
      uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //---
      if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

现在研究用于处理按下右键的 CTextBox::OnPressedKeyRight() 方法的代码。此处, 必须在方法开头检查并通过: 所按键的代码, 文本框状态和 Ctrl 键的状态。然后看看, 是否 文本光标处于行的末尾。如果不是, 则移动文本光标至右一个字符。如果文本光标位于行的末尾, 则查看 此行是否为最后一行。如果不是, 则将文本光标移动到下一行的开始处。

然后 (1) 在文本光标超出文本框可见区域的情况下调整滚动条滑块, (2) 重绘控件, 并 (3) 发送关于移动文本光标的消息。  

class CTextBox : public CElement
  {
private:
   //--- 处理按下右键
   bool              OnPressedKeyRight(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按下右键                                                      |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyRight(const long key_code)
  {
//--- 如果不是右键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开
   if(key_code!=KEY_RIGHT || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width);
//--- 如果这是行的末尾
   if(m_text_cursor_x_pos<symbols_total)
     {
      //--- 将文本光标的位置移动到下一个字符
      m_text_cursor_x+=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos];
      //--- 增加字符计数器
      m_text_cursor_x_pos++;
     }
   else
     {
      //--- 获取行数组大小
      uint lines_total=::ArraySize(m_lines);
      //--- 如果这不是最后一行
      if(m_text_cursor_y_pos<lines_total-1)
        {
         //--- 将光标移动到下一行的开始处
         m_text_cursor_x=m_text_x_offset;
         SetTextCursor(0,++m_text_cursor_y_pos);
        }
     }
//--- 获取文本框可见部分的边界
   CalculateBoundaries();
//--- 获取光标的 Y 坐标
   CalculateTextCursorY();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
   else
     {
      if(m_text_cursor_x_pos==0)
         HorizontalScrolling(0);
     }
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
      VerticalScrolling(CalculateScrollThumbY2());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


处理按上、下键

按上和下键使文本光标在行间上下移动。CTextBox::OnPressedKeyUp() 和 CTextBox::OnPressedKeyDown() 方法设计用来处理按这些键。这里只提供其中一个的代码, 因为它们之间的唯一区别仅在于两行代码。

在代码的开头, 有必要通过三次检查。如果 (1) 是单行文本框, 或者如果 (2) 按下其它键, 或者 (3) 文本框未激活, 程序离开该方法。如果文本光标的当前位置不在第一行, 则将其移动到上一行 (到 CTextBox::OnPressedKeyDown() 方法中的下一行), 并调整超出行数组范围情况的字符。

之后, 检查文本光标是否位于文本框的可见区域之外, 并根据需要调整滚动条滑块。此处, 两个方法之间的唯一区别是 CTextBox::OnPressedKeyUp() 方法 检查超出上边界, 而 CTextBox::OnPressedKeyDown() 方法 — 超出下边界。在最结尾时, 重新绘制文本框, 并发送关于移动文本光标的消息。

class CTextBox : public CElement
  {
private:
   //--- 处理按向上键
   bool              OnPressedKeyUp(const long key_code);
   //--- 处理按向下键
   bool              OnPressedKeyDown(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按向上键                                                      |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyUp(const long key_code)
  {
//--- 如果禁用多行模式, 则离开
   if(!m_multi_line_mode)
      return(false);
//--- 如果不是向上键, 或如果文本框未激活, 离开
   if(key_code!=KEY_UP || !m_text_edit_state)
      return(false);
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 如果未超出数组范围
   if(m_text_cursor_y_pos-1<lines_total)
     {
      //--- 移动到上一行
      m_text_cursor_y_pos--;
      //--- 沿 X 轴调整文本光标
      CorrectingTextCursorXPos(m_text_cursor_x_pos);
     }
//--- 获取文本框可见部分的边界
   CalculateBoundaries();
//--- 获取光标的 Y 坐标
   CalculateTextCursorY();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }



处理按起始和结尾键

按起始和结尾键分别将文本光标移动到行的开始和结束。CTextBox::OnPressedKeyHome() 和 CTextBox::OnPressedKeyEnd() 方法设计用来处理这些事件。它们的代码在下面的列表中提供, 它不需要任何进一步的解释, 因为它很简单, 并有详细的注释。 

class CTextBox : public CElement
  {
private:
   //--- 处理按下起始键
   bool              OnPressedKeyHome(const long key_code);
   //--- 处理按下结尾键
   bool              OnPressedKeyEnd(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理按下起始键                                                    |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyHome(const long key_code)
  {
//--- 如果不是起始键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开
   if(key_code!=KEY_HOME || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- 将光标移动到当前行的开始处
   SetTextCursor(0,m_text_cursor_y_pos);
//--- 将滚动条移至第一个位置
   HorizontalScrolling(0);
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }
//+------------------------------------------------------------------+
//| 处理按下结尾键                                                    |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyEnd(const long key_code)
  {
//--- 如果不是结尾键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开
   if(key_code!=KEY_END || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- 获取当前行中的字符数
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 将光标移动到当前行的结尾处
   SetTextCursor(symbols_total,m_text_cursor_y_pos);
//--- 获取光标的 X 坐标
   CalculateTextCursorX();
//--- 获取文本框可见部分的边界
   CalculateXBoundaries();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


处理并发按键和 Ctrl 键组合

现在我们来研究处理以下组合键的方法:

  • 'Ctrl' + '左' – 将文本光标移至左侧的单词。
  • 'Ctrl' + '右' – 将文本光标移至右侧的单词。
  • 'Ctrl' + '起始' – 将文本光标移至第一行的开始。 
  • 'Ctrl' + '结尾' – 将文本光标移至最后一行的末尾。

作为示例, 只有一个方法需要研究 — CTextBox::OnPressedKeyCtrlAndLeft(), 用于将文本光标从一个单词移动到左侧的一个单词。在方法开始时, 检查是否同时按下 Ctrl 和向左键。若非按下任何这些键, 程序离开该方法。此外, 必须激活文本框。

如果 文本光标的当前位置在行首, 且不是第一行, 则将其移动到上一行的末尾。如果文本光标不在当前行的开始处, 则需要找到不间断字符序列的开始。空格符 (' ') 用作分割字符。此处, 在一个循环中, 沿着当前行从右到左移动, 一旦发现组合, 当 下一个字符是空格且当前是非空格的任意其它字符时, 如果这不是起始点, 则将文本光标设置到该位置。

之后, 如在所有其它方法中, 检查文本光标是否在文本框的可见区域之外, 且如有必要, 调整滚动条滑块。在最末尾, 重新绘制文本框, 并发送一条消息, 表示文本光标已移动。

class CTextBox : public CElement
  {
private:
   //--- 处理按下 Ctrl + 左键
   bool              OnPressedKeyCtrlAndLeft(const long key_code);
   //--- 处理按下 Ctrl + 右键
   bool              OnPressedKeyCtrlAndRight(const long key_code);
   //--- 处理同时按下 Ctrl + 起始键
   bool              OnPressedKeyCtrlAndHome(const long key_code);
   //--- 处理同时按下 Ctrl + 结尾键
   bool              OnPressedKeyCtrlAndEnd(const long key_code);
  };
//+------------------------------------------------------------------+
//| 处理同时按下 Ctrl + 左键                                          |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyCtrlAndLeft(const long key_code)
  {
//--- 如果(1) 不是左键, 或 (2) 如果未按 Ctrl 键, 或 (3) 如果文本框未激活, 离开
   if(!(key_code==KEY_LEFT && m_keys.KeyCtrlState()) || !m_text_edit_state)
      return(false);
//--- 空格符
   string SPACE=" ";
//--- 获取行数组大小
   uint lines_total=::ArraySize(m_lines);
//--- 获取当前行中的字符数
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 如果光标在当前行的开头, 并且这不是第一行,
//   将光标移动到上一行的末尾
   if(m_text_cursor_x_pos==0 && m_text_cursor_y_pos>0)
     {
      //--- 获取上一行的索引
      uint prev_line_index=m_text_cursor_y_pos-1;
      //--- 获取上一行中的字符数
      symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
      //--- 将光标移动到上一行的末尾
      SetTextCursor(symbols_total,prev_line_index);
     }
// --- 如果光标在当前行的开头或光标在第一行
   else
     {
      //--- 找到连续字符序列的开始 (从右到左)
      for(uint i=m_text_cursor_x_pos; i<=symbols_total; i--)
        {
         //--- 如果光标在行的末尾, 转至下一个
         if(i==symbols_total)
            continue;
         //--- 如果这是行的第一个字符
         if(i==0)
           {
            //--- 将光标设置在行首
            SetTextCursor(0,m_text_cursor_y_pos);
            break;
           }
         //--- 如果这不是行的第一个字符
         else
           {
            //--- 如果第一次找到连续序列的开始。
            //   下一索引开始应考虑是空格。
            if(i!=m_text_cursor_x_pos &&  
               m_lines[m_text_cursor_y_pos].m_symbol[i]!=SPACE &&  
               m_lines[m_text_cursor_y_pos].m_symbol[i-1]==SPACE)
              {
               //--- 将光标设置到新连续序列的开头
               SetTextCursor(i,m_text_cursor_y_pos);
               break;
              }
           }
        }
     }
//--- 获取文本框可见部分的边界
   CalculateBoundaries();
//--- 获取光标的 X 坐标
   CalculateTextCursorX();
//--- 获取光标的 Y 坐标
   CalculateTextCursorY();
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      //--- 获取字符数组的大小
      symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //---
      if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- 如果文本光标离开可见区域, 则移动滚动条
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- 刷新文本框内文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

本节开头列表中的所有其它方法的研究留给读者。 

 


函数库引擎中的控件集成

若要多行文本框控件正常工作, 在 CWndContainer 类的 WindowElements 结构中需要一个专用数组。包含 CTextBox 类的文件 WndContainer.mqh:

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Controls\TextBox.mqh"

添加一个 用于新控件的私有数组WindowElements 结构: 

//+------------------------------------------------------------------+
//| 用于存储所有接口对象的类                                           |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- 元素数组的结构
   struct WindowElements
     {
      //--- 多行文本框
      CTextBox         *m_text_boxes[];
     };
   //--- 每个窗口的元素数组
   WindowElements    m_wnd[];
  };

由于 CTextBox 类型控件是复合的, 并且包含其它类型的控件 (在此情况下是滚动条), 因此需要一个方法, 其中指向这些控件的指针将分发到相应的专用数组。下面列表显示的 CWnd Container::AddTextBoxElements() 方法代码, 即为此目的而设计。与其它类似方法一样, 相同的位置调用此方法, 即在 CWndContainer::AddToElementsArray() 中。 

class CWndContainer
  {
private:
   //--- 保存指向多行文本框对象的指针
   bool              AddTextBoxElements(const int window_index,CElementBase &object);
  };
//+------------------------------------------------------------------+
//| 保存指向多行文本框对象的指针                                       |
//+------------------------------------------------------------------+
bool CWndContainer::AddTextBoxElements(const int window_index,CElementBase &object)
  {
//--- 如果此非多行文本框, 离开
   if(dynamic_cast<CTextBox *>(&object)==NULL)
      return(false);
//--- 获取控件的指针
   CTextBox *tb=::GetPointer(object);
   for(int i=0; i<2; i++)
     {
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      if(i==0)
        {
         //--- 获取滚动条指针
         CScrollV *sv=tb.GetScrollVPointer();
         m_wnd[window_index].m_elements[size]=sv;
         AddToObjectsArray(window_index,sv);
         //--- 将指针添加到专用数组
         AddToRefArray(sv,m_wnd[window_index].m_scrolls);
        }
      else if(i==1)
        {
         CScrollH *sh=tb.GetScrollHPointer();
         m_wnd[window_index].m_elements[size]=sh;
         AddToObjectsArray(window_index,sh);
         //--- 将指针添加到专用数组
         AddToRefArray(sh,m_wnd[window_index].m_scrolls);
        }
     }
//--- 将指针添加到专用数组
   AddToRefArray(tb,m_wnd[window_index].m_text_boxes);
   return(true);
  }

现在有必要添加一些额外内容至 CWndEvents::OnTimerEvent() 方法。请记住, 只有鼠标光标移动过, 且在鼠标光标移动停止后暂停一段时间后, 才会重绘图形界面。对于 CTextBox 类型控件应作为一个例外。否则, 当激活文本框时, 文本光标不会闪烁。 

//+------------------------------------------------------------------+
//| 计时器                                                           |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- 如果鼠标光标处于静止 (调用之间的时间差 >300 毫秒), 并释放鼠标左键, 离开
   if(m_mouse.GapBetweenCalls()>300 && !m_mouse.LeftButtonState())
     {
      int text_boxes_total=CWndContainer::TextBoxesTotal(m_active_window_index);
      for(int e=0; e<text_boxes_total; e++)
         m_wnd[m_active_window_index].m_text_boxes[e].OnEventTimer();
      //---
      return;
     }
//--- 如果数组为空,则离开  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- 通过定时器检查所有控件的事件
   CheckElementsEventsTimer();
//--- 重绘图表
   m_chart.Redraw();
  }

现在, 我们来创建一个测试 MQL 应用程序, 它可以测试多行文本框控件。 

 


测试控件的应用

为了测试, 利用图形界面创建包含两个文本框的 MQL 应用程序。其中一个是单行的, 而另一个 — 多行。除了这些文本框, 示例的图形界面将包含具有上下文的主菜单和状态栏。状态栏的第二项将广播多行文本框的文本光标位置。

创建两个 CTextBox 类的实例, 以及声明两个方法来创建文本框:

class CProgram : public CWndEvents
  {
protected:
   //--- 编辑
   CTextBox          m_text_box1;
   CTextBox          m_text_box2;
   //---
protected:
   //--- 编辑
   bool              CreateTextBox1(const int x_gap,const int y_gap);
   bool              CreateTextBox2(const int x_gap,const int y_gap);
  };

下面的列表展示了创建多行文本框的第二种方法的代码。若要启用多行模式, 请使用 CTextBox::MultiLineMode() 方法。对于需要自动调整大小的表单区域, 应使用 CElementBase::AutoXResizeXXX() 方法。例如, 让我们将本文的内容添加到多行文本框中。为此, 准备一个行数组, 稍后可以使用 CTextBox 类的特殊方法将其添加到循环中。 

//+------------------------------------------------------------------+
//| 创建一个多行文本框                                                |
//+------------------------------------------------------------------+
bool CProgram::CreateTextBox2(const int x_gap,const int y_gap)
  {
//--- 保存窗口指针
   m_text_box2.WindowPointer(m_window);
//--- 在创建之前设置属性
   m_text_box2.FontSize(8);
   m_text_box2.Font("Calibri"); // Consolas|Calibri|Tahoma
   m_text_box2.AreaColor(clrWhite);
   m_text_box2.TextColor(clrBlack);
   m_text_box2.MultiLineMode(true);
   m_text_box2.AutoXResizeMode(true);
   m_text_box2.AutoXResizeRightOffset(2);
   m_text_box2.AutoYResizeMode(true);
   m_text_box2.AutoYResizeBottomOffset(24);
//--- 行数组
   string lines_array[]=
     {
      "概论",
      "按键组和键盘布局",
      "处理按键事件",
      "字符的 ASCII 编码和控制键",
      "键位扫描码",
      "操控键盘的辅助类",
      "多行文本框控件",
      "开发 CTextBox 类用于创建控件",
      "属性和外观",
      "管理文本光标",
      "输入一个字符",
      "处理按退格键",
      "处理按回车键",
      "处理按左、右键",
      "处理按上、下键",
      "处理按起始和结尾键",
      "处理并发按键和 Ctrl 键组合",
      "函数库引擎中的控件集成",
      "测试控件的应用",
      "结论"
     };
//--- 在文本框中添加文本
   int lines_total=::ArraySize(lines_array);
   for(int i=0; i<lines_total; i++)
     {
      //--- 在第一行添加文本
      if(i==0)
         m_text_box2.AddText(0,lines_array[i]);
      //--- 在文本框中添加一行
      else
         m_text_box2.AddLine(lines_array[i]);
     }
//--- 创建控件
   if(!m_text_box2.CreateTextBox(m_chart_id,m_subwin,x_gap,y_gap))
      return(false);
//--- 将对象添加到对象组的公用数组
   CWndContainer::AddToElementsArray(0,m_text_box2);
//--- 在状态栏设置文本项目
   m_status_bar.ValueToItem(1,m_text_box2.TextCursorInfo());
   return(true);
  }

将以下代码添加到 MQL 应用程序的事件处理器中, 以便从文本框接收消息:

//+------------------------------------------------------------------+
//| 图表事件处理器                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 事件 (1) 输入值, 或 (2) 激活文本框, 或(3) 移动文本光标
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT ||
      id==CHARTEVENT_CUSTOM+ON_CLICK_TEXT_BOX ||
      id==CHARTEVENT_CUSTOM+ON_MOVE_TEXT_CURSOR)
     {
      ::Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);

      //--- 如果标识符匹配 (来自多行文本框的消息)
      if(lparam==m_text_box2.Id())
        {
         //--- 更新状态栏的第二项
         m_status_bar.ValueToItem(1,sparam);
        }
      //--- 重绘图表
      m_chart.Redraw();
      return;
     }
  }


编译应用程序并将其加载到图表上之后, 可以看到以下内容:

 图例. 9. 图形界面与文本框控件的演示

图例. 9. 图形界面与文本框控件的演示

 

文章中介绍的测试应用程序可从以下链接下载, 以供进一步学习。 

 


结论

目前, 创建图形界面的函数库的一般示意图如下所示:

 图例. 10. 当前开发阶段的函数库结构。

图例. 10. 当前开发阶段的函数库结构。

 

下一个版本的函数库将进一步开发, 新的功能将被添加到已经实现的控件中。您可从下面下载最新版本的函数库和文件进行测试。

如果您对这些文件中提供的材料有疑问, 可以参考本系列文章中函数库开发的详细描述部分, 或在本文的评论中提出您的问题。 


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/3004

附加的文件 |
可视化!类似于 R 语言 "plot (绘图)" 的 MQL5 图形库 可视化!类似于 R 语言 "plot (绘图)" 的 MQL5 图形库
在研究交易逻辑时, 图形形式的直观表达是非常重要的。科学界中流行的一些编程语言 (如 R 和 Python) 拥有可视化的特殊 "plot (绘图)" 功能。它能够以直观方式绘制线, 点分布和直方图。在 MQL5 中, 您可以使用 CGraphics 类完成相同的操作。
ZUP - 通用之字折线构造 Pesavento 形态。图形界面 ZUP - 通用之字折线构造 Pesavento 形态。图形界面
自 ZUP 平台的第一版本发布以来已过了十年时间, 期间它经历了多次变化和改进。结果就是, 如今我们为 MetaTrader 4 提供了一个独特的图形插件, 您可以快速、便捷地分析行情数据。本文介绍如何使用 ZUP 指标平台的图形界面。
图形界面 X: 渲染表格的新功能 (集成编译 9) 图形界面 X: 渲染表格的新功能 (集成编译 9)
时至今日, CTable 是函数库中所含的最先进类型表格。表格由 OBJ_EDIT 类型的编辑框汇集而成, 但其进一步开发成为问题。因此, 在最大性能方面, 即使是在函数库的当前开发阶段, 也最好开发 CCanvasTable 类型的渲染表格。它的当前版本毫无生气, 但从本文开始, 我们将尝试解决这个问题。
在MQL5中的三维建模 在MQL5中的三维建模
时间序列是一种动态的系统,它会按时持续收到随机变量的值或者连续等差的数值,把市场分析从2D(二维)转换到3D(三维),给我们提供了对于这种复杂过程和研究对象的新的视野。本文描述了提供二维数据的三维表现形式的可视化方法,