图形界面 X: 在多行文本框中选择文本 (集成构建 13)

Anatoli Kazharski | 29 六月, 2017


内容目录

概述

在首篇文章 图形界面 I: 函数库结构的准备 (第 1 章) 中已经详细研究了函数库的作用。有关最初部分文章的完整链接列表, 请参见每章的结尾。从那里, 您还可以下载当前处于开发阶段的函数库完整版。文件必须放在存档文件所在的目录下。

为了充分利用以下文章中研究的多行文本框, 有必要实现文本选择, 因为一次删除一个字符是很不方便的。

使用组合键选择文本并删除所选文本, 与其它任意文本编辑器中的观感完全相同。此外, 我们将继续优化代码, 并为进入函数库演变第二阶段的最后一个过程准备好类, 其中所有控件均作为单独的图像 (画布) 呈现。 

在此将呈现这个控件的最终版本。仅在发现有关某种算法更有效的解决方案时, 才会进行后续修改。

跟踪 Shift 键的按压

首先, 我们要在以前研究过的设计用于操控键盘的 CKeys 类中添加 CKeys::KeyShiftState() 方法来检测 Shift 键的当前状态。此键将用于选择文本的各种组合。以下列表展示了这个简单方法的代码。如果 ::TerminalInfoInteger() 函数以 TERMINAL_KEYSTATE_SHIFT 标识符调用并返回一个低于零的数值, 则认定 Shift 键已按压。

//+------------------------------------------------------------------+
//| 操控键盘的类                                                       |
//+------------------------------------------------------------------+
class CKeys
  {
public:
   //--- 返回 Shift 键的状态
   bool              KeyShiftState(void);
  };
//+------------------------------------------------------------------+
//| 返回 Shift 键的状态                                                |
//+------------------------------------------------------------------+
bool CKeys::KeyShiftState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0);
  }


选择文本的组合键

我们来研究所有选择文本的组合键, 这些组合将在文本框中实现。我们将从两个键的组合开始。

  • 'Shift + Left' 和 'Shift + Right' 组合可将文本光标分别向左或向右移动一个字符。文本以不同的背景和字符颜色高亮显示 (也可由用户自行定义):

 图例. 1 选择文本, 左右移动一个字符。

图例. 1 选择文本, 左右移动一个字符。

  • 'Shift + Home' 和 'Shift + End' 组合可将文本光标移至行首或行尾, 并从光标的起始位置选择所有字符。

 图例. 2. 自光标起始位置移动到行的开头和结尾, 并选择文本。

图例. 2. 自光标起始位置移动到行的开头和结尾, 并选择文本。

  • 'Shift + Up' 和 'Shift + Down' 组合可将文本光标分别向上或向下移一行。选择文本则自光标处到起始行的开头, 或自最后一行的行尾到光标处。如果在初始和最终选择的行之间有更多行, 则其内文本将被完全选择。

 图例. 3. 上下移动一行选择文本。

图例. 3. 上下移动一行选择文本。

有时会用三个键的组合来选择文本。例如, 若需要在一行中快速选择多个单词时, 逐个字符的选择将变得太乏味。如果还需要选择由多行组成的文本, 即使是逐行选择也不方便。 

在三个键组合当中, 除了 Shift 之外还使用了 Ctrl。我们来研究所有这些将在本文中实现的组合:

  • 'Ctrl + Shift + Left' 和 'Ctrl + Shift + Right' 组合分别用于在文本光标当前位置的左侧和右侧选择整个单词:

 图例. 4. 左右移动选择一个单词的文字。

图例. 4. 左右移动选择一个单词的文字。

  • 'Ctrl + Shift + Home' 和 'Ctrl + Shift + End' 组合可以选择自文本光标的当前位置开始至首行开头或最后一行的末尾间的所有文本:

 图例. 5. 移动光标到文档的开头和结尾并选择文本。

图例. 5. 移动光标到文档的开头和结尾并选择文本。

下一节将研究文本选择中使用的方法。


选择文本的方法

省缺情况下, 所选文本在蓝色背景上以白色字符显示。若有必要, CTextBox:: SelectedBackColor() 和 CTextBox:: SelectedTextColor() 方法可用来改变颜色。 

class CTextBox : public CElement
  {
private:
   //--- 所选文本的背景和字符颜色
   color             m_selected_back_color;
   color             m_selected_text_color;
   //---
private:
   //--- 所选文本的背景和字符颜色
   void              SelectedBackColor(const color clr)        { m_selected_back_color=clr;       }
   void              SelectedTextColor(const color clr)        { m_selected_text_color=clr;       }
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_selected_text_color(clrWhite),
                           m_selected_back_color(C'51,153,255')
  {
//...
  }

为了选择文本, 必需有字段和方法来保存指定选择文本的起始行和结束行的索引, 字符的初始位置。此外, 当取消选择时, 需要一种重新设置这些值的方法。

每次按下选择文字的组合键时, 将在移动文本光标前调用 CTextBox::SetStartSelectedTextIndexes() 方法。它设置文本光标所在的初始行和字符索引值。这些值仅在最后一次重置之后, 首次调用此方法 时才会设置。调用此方法之后, 光标移动。之后调用 CTextBox::SetEndSelectedTextIndexes() 方法, 它设置行和字符索引的最终值 (即文本光标的光标位置)。如果在文本选择模式中处理光标移动期间, 证实光标位于 起始处的右侧, 则调用 CTextBox::ResetSelectedText() 方法重置该值。任何文本光标的移动, 删除所选文本或文本框失活时, 这些值也会重置。

class CTextBox : public CElement
  {
private:
   //--- 行和字符 (所选文本的) 的起始行和结束行索引
   int               m_selected_line_from;
   int               m_selected_line_to;
   int               m_selected_symbol_from;
   int               m_selected_symbol_to;
   //---
private:
   //--- 设置所选文本的 (1) 起始行 (2) 结束行索引
   void              SetStartSelectedTextIndexes(void);
   void              SetEndSelectedTextIndexes(void);
   //--- 重置所选文本
   void              ResetSelectedText(void);
  };
//+------------------------------------------------------------------+
//| 设置所选文本的起始行索引                                             |
//+------------------------------------------------------------------+
void CTextBox::SetStartSelectedTextIndexes(void)
  {
//--- 如果尚未设置选择文本的起始行索引
   if(m_selected_line_from==WRONG_VALUE)
     {
      m_selected_line_from   =(int)m_text_cursor_y_pos;
      m_selected_symbol_from =(int)m_text_cursor_x_pos;
     }
  }
//+------------------------------------------------------------------+
//| 设置所选文本的结束行索引                                              |
//+------------------------------------------------------------------+
void CTextBox::SetEndSelectedTextIndexes(void)
  {
//--- 设置所选文本的结束行索引
   m_selected_line_to   =(int)m_text_cursor_y_pos;
   m_selected_symbol_to =(int)m_text_cursor_x_pos;
//--- 如果所有索引相同, 则清除选择
   if(m_selected_line_from==m_selected_line_to && m_selected_symbol_from==m_selected_symbol_to)
      ResetSelectedText();
  }
//+------------------------------------------------------------------+
//| 重置所选文本                                                       |
//+------------------------------------------------------------------+
void CTextBox::ResetSelectedText(void)
  {
   m_selected_line_from   =WRONG_VALUE;
   m_selected_line_to     =WRONG_VALUE;
   m_selected_symbol_from =WRONG_VALUE;
   m_selected_symbol_to   =WRONG_VALUE;
  }

之前在移动光标的方法中所用的代码块现在作为单独的方法实现, 因为它们将在文本选择方法中重复使用。当文本光标超出可见区域时, 用于调整滚动条的代码同样适用。 

class CTextBox : public CElement
  {
private:
   //--- 将文本光标向左移动一个字符
   void              MoveTextCursorToLeft(void);
   //--- 将文本光标向右移动一个字符
   void              MoveTextCursorToRight(void);
   //--- 将文本光标向上移动一个字符
   void              MoveTextCursorToUp(void);
   //--- 将文本光标向下移动一个字符
   void              MoveTextCursorToDown(void);

   //--- 调整水平滚动条
   void              CorrectingHorizontalScrollThumb(void);
   //--- 调整垂直滚动条
   void              CorrectingVerticalScrollThumb(void);
  };

处理按键组合的所有方法 (其一是 Shift) 包含几乎相同的代码, 除了调用移动文本光标的方法。所以, 创建一个可以简单地传递方向来移动文本光标的附加方法是合理的。含有若干标识符的 ENUM_MOVE_TEXT_CURSOR 枚举 (参见以下列表) 已被添加到 Enums.mqh 文件中。它们可用于指明文本光标需要移动的位置:

  • TO_NEXT_LEFT_SYMBOL — 向左一个字符。
  • TO_NEXT_RIGHT_SYMBOL — 向右一个字符。
  • TO_NEXT_LEFT_WORD — 向左一个单词。
  • TO_NEXT_RIGHT_WORD — 向右一个单词。
  • TO_NEXT_UP_LINE — 向上一行。
  • TO_NEXT_DOWN_LINE — 向下一行。
  • TO_BEGIN_LINE — 至当前行的开头。
  • TO_END_LINE — 至当前行的末尾。
  • TO_BEGIN_FIRST_LINE — 至当首行的开头。
  • TO_END_LAST_LINE — 至最后一行的末尾。
//+------------------------------------------------------------------+
//| 移动文本光标的方向枚举                                               |
//+------------------------------------------------------------------+
enum ENUM_MOVE_TEXT_CURSOR
  {
   TO_NEXT_LEFT_SYMBOL  =0,
   TO_NEXT_RIGHT_SYMBOL =1,
   TO_NEXT_LEFT_WORD    =2,
   TO_NEXT_RIGHT_WORD   =3,
   TO_NEXT_UP_LINE      =4,
   TO_NEXT_DOWN_LINE    =5,
   TO_BEGIN_LINE        =6,
   TO_END_LINE          =7,
   TO_BEGIN_FIRST_LINE  =8,
   TO_END_LAST_LINE     =9
  };

现在, 我们可以创建一个移动文本光标的通用方法 — CTextBox::MoveTextCursor(), 从上表中传递一个标识符至此就足够了。几乎所有处理 CTextBox 控件中按键事件的方法都将使用相同的方法。

class CTextBox : public CElement
  {
private:
   //--- 按指定的方向移动文本光标
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction);
  };
//+------------------------------------------------------------------+
//| 按指定的方向移动文本光标                                             |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction)
  {
   switch(direction)
     {
      //--- 光标向左移动一个字符
      case TO_NEXT_LEFT_SYMBOL  : MoveTextCursorToLeft();        break;
      //--- 光标向右移动一个字符
      case TO_NEXT_RIGHT_SYMBOL : MoveTextCursorToRight();       break;
      //--- 光标向左移动一个单词
      case TO_NEXT_LEFT_WORD    : MoveTextCursorToLeft(true);    break;
      //--- 光标向右移动一个单词
      case TO_NEXT_RIGHT_WORD   : MoveTextCursorToRight(true);   break;
      //--- 光标向上移动一行
      case TO_NEXT_UP_LINE      : MoveTextCursorToUp();          break;
      //--- 光标向下移动一行
      case TO_NEXT_DOWN_LINE    : MoveTextCursorToDown();        break;
      //--- 光标移动到当前行的开头
      case TO_BEGIN_LINE : SetTextCursor(0,m_text_cursor_y_pos); break;
      //--- 光标移动到当前行的末尾
      case TO_END_LINE :
        {
         //--- 获取当前行中的字符数
         uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
         //--- 移动光标
         SetTextCursor(symbols_total,m_text_cursor_y_pos);
         break;
        }
      //--- 光标移动到首行的开头
      case TO_BEGIN_FIRST_LINE : SetTextCursor(0,0); break;
      //--- 光标移动到最后一行的末尾
      case TO_END_LAST_LINE :
        {
         //--- 获取最后一行的行号和字符数
         uint lines_total   =::ArraySize(m_lines);
         uint symbols_total =::ArraySize(m_lines[lines_total-1].m_symbol);
         //--- 移动光标
         SetTextCursor(symbols_total,lines_total-1);
         break;
        }
     }
  }

此文件中的代码可以显著减少, 因为文本光标移动和文本选择事件的处理程序器方法有很多重复的代码块。 

位于移动文本光标方法当中 的重复代码块示例:

//+------------------------------------------------------------------+
//| 处理 Left 键按压                                                   |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- 如果 (1) 并非 Left 键, 或是 (2) 按下 Ctrl 键, 或是 (3) 按下 Shift 键, 离开
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || m_keys.KeyShiftState())
      return(false);
//--- 重置选择
   ResetSelectedText();
//--- 文本光标向左移动一个字符
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- 调整滚动条
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- 更新文本框中的文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

位于文本选择方法当中 的重复代码块示例:

//+------------------------------------------------------------------+
//| 处理按下 Shift + Left 键                                           |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyShiftAndLeft(const long key_code)
  {
//--- 如果 (1) 并非 Left 键, 或是 (2) 按下 Ctrl 键, 或是 (3) 未按下 Shift 键, 离开
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_keys.KeyShiftState())
      return(false);
//--- 设置选择文本的起始索引
   SetStartSelectedTextIndexes();
//--- 文本光标向左移动一个字符
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- 设置所选文本的结束行索引
   SetEndSelectedTextIndexes();
//--- 调整滚动条
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- 更新文本框中的文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }


我们来实现一个附加的 (重载) 方法 CTextBox::MoveTextCursor()。方法需要传递移动方向标识符, 以及指示标志 (1) 如果它是文本光标移动 或 (2) 文本选择.

class CTextBox : public CElement
  {
private:
   //--- 按指定的方向移动文本光标
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text);
  };
//+------------------------------------------------------------------+
//| 按指定方向和条件                                                    |
//| 移动文本光标                                                       |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text)
  {
//--- 如果它仅是文本光标移动
   if(!with_highlighted_text)
     {
      //--- 重置选择
      ResetSelectedText();
      //--- 光标移动到首行的开头
      MoveTextCursor(direction);
     }
//--- 如果文本选择启用
   else
     {
      //--- 设置选择文本的起始索引
      SetStartSelectedTextIndexes();
      //--- 文本光标向左移动一个字符
      MoveTextCursor(direction);
      //--- 设置所选文本的结束行索引
      SetEndSelectedTextIndexes();
     }
//--- 调整滚动条
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- 更新文本框中的文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
  }


选择文本的组合键处理方法如下所示。它们的代码几乎相同 (它们仅在参数上不同)。因此, 您可以研究文章附带文件中的代码:

class CTextBox : public CElement
  {
private:
   //--- 处理按下 Shift + Left 键
   bool              OnPressedKeyShiftAndLeft(const long key_code);
   //--- 处理按下 Shift + Right 键
   bool              OnPressedKeyShiftAndRight(const long key_code);
   //--- 处理按下 Shift + Up 键
   bool              OnPressedKeyShiftAndUp(const long key_code);
   //--- 处理按下 Shift + Down 键
   bool              OnPressedKeyShiftAndDown(const long key_code);
   //--- 处理按下 Shift + Home 键
   bool              OnPressedKeyShiftAndHome(const long key_code);
   //--- 处理按下 Shift + End 键
   bool              OnPressedKeyShiftAndEnd(const long key_code);

   //--- 处理按下 Ctrl + Shift + Left 键
   bool              OnPressedKeyCtrlShiftAndLeft(const long key_code);
   //--- 处理按下 Ctrl + Shift + Right 键
   bool              OnPressedKeyCtrlShiftAndRight(const long key_code);
   //--- 处理按下 Ctrl + Shift + Home 键
   bool              OnPressedKeyCtrlShiftAndHome(const long key_code);
   //--- 处理按下 Ctrl + Shift + End 键
   bool              OnPressedKeyCtrlShiftAndEnd(const long key_code);
  };


到目前为止, 全部文本行已在画布上应用。但由于所选字符和其下的背景颜色变化, 所以文本输出必须字符明确。为此, 我们对 CTextBox::TextOut() 方法进行一点小改动。 

它还需要一个额外的 CTextBox::CheckSelectedText() 方法来检查所选择的字符。我们已经知道, 在文本选择期间, 文本光标的初始行和最后一行以及字符的索引已被保存。所以, 在循环中遍历字符, 很容易就能确定行中的字符是否被选择。逻辑很简单:

  1. 如果行的初始索引低于最后一个索引, 则选择该字符:
    • 如果是最后一行, 且字符在最终选定的右侧
    • 如果是初始行, 且字符在初始选择的左侧
    • 在中间行内的所有字符都选择
  2. 如果行的初始索引高于最后一个索引, 则选择该字符:
    • 如果是最后一行, 且字符在最终选择的左侧
    • 如果是初始行, 且字符在初始选择的右侧
    • 在中间行内的所有字符都选择
  3. 如果仅在一行中选择文本, 则如果在初始和最终字符索引之间的指定范围内, 则选择一个字符。
class CTextBox : public CElement
  {
private:
   //--- 检查所选文本的存在
   bool              CheckSelectedText(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| 检查所选文本的存在                                                  |
//+------------------------------------------------------------------+
bool CTextBox::CheckSelectedText(const uint line_index,const uint symbol_index)
  {
   bool is_selected_text=false;
//--- 如果没有选择文本, 离开
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- 如果初始索引在下面的行
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- 最后一行和字符在最后选择的右侧
      if((int)line_index==m_selected_line_to && (int)symbol_index>=m_selected_symbol_to)
        { is_selected_text=true; }
      //--- 初始行和字符在最初选择的左侧
      else if((int)line_index==m_selected_line_from && (int)symbol_index<m_selected_symbol_from)
        { is_selected_text=true; }
      //--- 中间行 (选择所有字符)
      else if((int)line_index>m_selected_line_to && (int)line_index<m_selected_line_from)
        { is_selected_text=true; }
     }
//--- 如果初始索引在上面
   else if(m_selected_line_from<m_selected_line_to)
     {
      //--- 最后一行和字符在最后选择的左侧
      if((int)line_index==m_selected_line_to && (int)symbol_index<m_selected_symbol_to)
        { is_selected_text=true; }
      //--- 初始行和字符在初始选择的右侧
      else if((int)line_index==m_selected_line_from && (int)symbol_index>=m_selected_symbol_from)
        { is_selected_text=true; }
      //--- 中间行 (选择所有字符)
      else if((int)line_index<m_selected_line_to && (int)line_index>m_selected_line_from)
        { is_selected_text=true; }
     }
//--- 如果初始和最终索引在同一行
   else
     {
      //--- 查找已检查的行
      if((int)line_index>=m_selected_line_to && (int)line_index<=m_selected_line_from)
        {
         //--- 如果光标向右移动, 且字符在所选范围内
         if(m_selected_symbol_from>m_selected_symbol_to)
           {
            if((int)symbol_index>=m_selected_symbol_to && (int)symbol_index<m_selected_symbol_from)
               is_selected_text=true;
           }
         //--- 如果光标向左移动, 且字符在所选范围内
         else
           {
            if((int)symbol_index>=m_selected_symbol_from && (int)symbol_index<m_selected_symbol_to)
               is_selected_text=true;
           }
        }
     }
//--- 返回结果
   return(is_selected_text);
  }

方法 CTextBox::TextOut() 设计用于输出文本, 有必要添加一个内部循环来遍历行内字符, 从而替代整行输出。它判断已检查的字符是否被选中。在字符已被选择的情况下, 确定其颜色, 并在字符下方 绘制填充矩形。全部完毕之后, 输出字符本身。 

class CTextBox : public CElement
  {
private:
   //--- 将文本输出到画布
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| 将文本输出到画布                                                    |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- 清洁画布
   m_canvas.Erase(AreaColorCurrent());
//--- 获取行数组的大小
   uint lines_total=::ArraySize(m_lines);
//--- 在超界的情况下, 调整大小
   m_text_cursor_y_pos=(m_text_cursor_y_pos>=lines_total)? lines_total-1 : m_text_cursor_y_pos;
//--- 获取字符数组的大小
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 如果启用了多行模式, 或是字符数大于零
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- 获取行宽
      int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
      //--- 获取行高, 并循环遍历所有行
      int line_height=(int)LineHeight();
      for(uint i=0; i<lines_total; i++)
        {
         //--- 获取文本坐标
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
         //--- 获取字符串大小
         uint string_length=::ArraySize(m_lines[i].m_symbol);
         //--- 绘制文本
         for(uint s=0; s<string_length; s++)
           {
            uint text_color=TextColorCurrent();
            //--- 如果有已选择的文本, 确定其颜色和当前字符的背景颜色
            if(CheckSelectedText(i,s))
              {
               //--- 所选文字的颜色
               text_color=::ColorToARGB(m_selected_text_color);
               //--- 计算绘制背景的坐标
               int x2=x+m_lines[i].m_width[s];
               int y2=y+line_height-1;
               //--- 绘制字符的背景颜色
               m_canvas.FillRectangle(x,y,x2,y2,::ColorToARGB(m_selected_back_color));
              }
            //--- 绘制字符
            m_canvas.TextOut(x,y,m_lines[i].m_symbol[s],text_color,TA_LEFT);
            //--- 下一个字符的 X 坐标
            x+=m_lines[i].m_width[s];
           }
        }
     }
//--- 如果多行模式被禁用且没有字符, 则显示省缺文本
   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);
     }
  }

选择文本的方法已经实现了, 这就是它在完成后的应用程序中的外观:

 图例. 6. 在 MQL 应用程序文本框中实现文本选择的演示。

图例. 6. 在 MQL 应用程序文本框中实现文本选择的演示。


删除已选文本的方法

现在我们来研究删除所选文本的方法。在此, 重要的是注意, 删除所选文本时将会采用不同的方法, 具体则取决于选择是在一行还是多行上。 

在单行上删除选择文本调用 CTextBox::DeleteTextOnOneLine() 方法。在方法开始时要判断 将被删除的字符数。之后, 如果所选文本的字符初始索引在右侧, 则从初始位置开始, 开始向右移动删除指定数量的字符。然后, 行的字符数组减少相同的数量。 

在所选文本的字符初始索引位于左侧的情况下, 文本光标还需要向右移动指定删除数量的字符

class CTextBox : public CElement
  {
private:
   //--- 删除一行中选择的文本
   void              DeleteTextOnOneLine(void);
  };
//+------------------------------------------------------------------+
//| 删除一行中选择的文本                                                 |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnOneLine(void)
  {
   int symbols_total     =::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
   int symbols_to_delete =::fabs(m_selected_symbol_from-m_selected_symbol_to);
//--- 如果字符的初始索引在右侧
   if(m_selected_symbol_to<m_selected_symbol_from)
     {
      //--- 将字符移动到当前行中的空余区域
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_from,m_selected_symbol_to);
     }
//--- 如果字符的初始索引在左侧
   else
     {
      //--- 将文本光标向左移动要删除数量的字符
      m_text_cursor_x_pos-=symbols_to_delete;
      //--- 将字符移动到当前行中的空余区域
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_to,m_selected_symbol_from);
     }
//--- 当前行的数组大小减少已提取的字符数
   ArraysResize(m_text_cursor_y_pos,symbols_total-symbols_to_delete);
  }


CTextBox::DeleteTextOnMultipleLines() 方法将用于删除多行选择文本。此处的算法比较复杂。首先, 有必要判断:

  • 初始和最后一行的字符总数
  • 所选文本的中间行数 (初始行和最后一行除外)
  • 初始和最后一行要删除的字符数。

下面给出进一步的动作顺序。取决于文本的选择方向 (向上或向下), 初始和最终的索引将被传递到其它方法。

  • 从一行中移动到另一行的字符, 即删除后将要保留的内容, 将复制到临时的动态数组当中。
  • 调整接收数组 (行) 的大小。
  • 数据被添加到接收行结构的数组中。
  • 根据删除行的数量移动行。
  • 调整行数组大小 (减少已删除的行数)。
  • 在初始行高于最后一行 (文本选择向下) 的情况下, 则将 文本光标移动到所选文本的初始索引 (行和字符)
class CTextBox : public CElement
  {
private:
   //--- 删除多行中选择的文本
   void              DeleteTextOnMultipleLines(void);
  };
//+------------------------------------------------------------------+
//| 删除多行中选择的文本                                                 |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnMultipleLines(void)
  {
//--- 初始和最后一行的字符总数
   uint symbols_total_line_from =::ArraySize(m_lines[m_selected_line_from].m_symbol);
   uint symbols_total_line_to   =::ArraySize(m_lines[m_selected_line_to].m_symbol);
//--- 要被删除的中间行数
   uint lines_to_delete =::fabs(m_selected_line_from-m_selected_line_to);
//--- 初始和最后一行要删除的字符数
   uint symbols_to_delete_in_line_from =::fabs(symbols_total_line_from-m_selected_symbol_from);
   uint symbols_to_delete_in_line_to   =::fabs(symbols_total_line_to-m_selected_symbol_to);
//--- 如果初始行低于最后一行
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- 将要移动的字符复制到数组中
      string array[];
      CopyWrapSymbols(m_selected_line_from,m_selected_symbol_from,symbols_to_delete_in_line_from,array);
      //--- 调整接收行数组大小
      uint new_size=m_selected_symbol_to+symbols_to_delete_in_line_from;
      ArraysResize(m_selected_line_to,new_size);
      //--- 将数据添加到接收行结构的数组
      PasteWrapSymbols(m_selected_line_to,m_selected_symbol_to,array);
      //--- 获取行数组的大小
      uint lines_total=::ArraySize(m_lines);
      //--- 根据要删除的行数向上移动行
      MoveLines(m_selected_line_to+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- 调整行数组大小
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
     }
//--- 如果初始行高于最后一行
   else
     {
      //--- 将要移动的字符复制到数组中
      string array[];
      CopyWrapSymbols(m_selected_line_to,m_selected_symbol_to,symbols_to_delete_in_line_to,array);
      //--- 调整接收行数组大小
      uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to;
      ArraysResize(m_selected_line_from,new_size);
      //--- 将数据添加到接收行结构的数组
      PasteWrapSymbols(m_selected_line_from,m_selected_symbol_from,array);
      //--- 获取行数组的大小
      uint lines_total=::ArraySize(m_lines);
      //--- 根据要删除的行数向上移动行
      MoveLines(m_selected_line_from+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- 调整行数组大小
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
      //--- 将光标移动到选择的初始位置
      SetTextCursor(m_selected_symbol_from,m_selected_line_from);
     }
  }

在删除文本的主要方法中判断调用以上哪种方法 — CTextBox::DeleteSelectedText()。一旦所选文本被删除, 初始和最终索引的值将被重置。此后, 有必要重新计算文本框尺寸, 因为行数可能已更改。另外, 用于计算文本框的最大行宽可能已更改。最后, 方法 发送文本光标移动的消息。如果选择并删除文本, 方法返回 true。如果事实证明在调用该方法时尚未选择文本, 则返回 false。 

class CTextBox : public CElement
  {
private:
   //--- 删除所选文本
   void              DeleteSelectedText(void);
  };
//+------------------------------------------------------------------+
//| 删除所选文本                                                       |
//+------------------------------------------------------------------+
bool CTextBox::DeleteSelectedText(void)
  {
//--- 如果未选择文本, 离开
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- 如果从一行中删除字符
   if(m_selected_line_from==m_selected_line_to)
      DeleteTextOnOneLine();
//--- 如果从多行中删除字符
   else
      DeleteTextOnMultipleLines();
//--- 重置所选文本
   ResetSelectedText();
//--- 计算文本框的大小
   CalculateTextBoxSize();
//--- 设置文本框的新尺寸
   ChangeTextBoxSize();
//--- 调整滚动条
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- 更新文本框中的文本
   DrawTextAndCursor(true);
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

CTextBox::DeleteSelectedText() 方法不仅在按下 Backspace 键时调用, 而且: (1) 当输入一个新字符并 (2) 按下 Enter 键时调用。在这些情况下, 首先删除文本, 然后执行与按键对应的动作。

这是它在完成后的应用程序中的外观:

 图例. 7. 删除所选文本的演示。

图例. 7. 删除所选文本的演示。

操纵图像数据的类

作为本文的补充, 我们来研究操控图像数据的新类 (CImage)。它在许多需要绘制图像的函数库控件类中被重复使用。该类包括在 Objects.mqh 文件中。 

类属性:
  • 图像像素数组;
  • 图像宽度;
  • 图像高度;
  • 图像文件路径。
//+------------------------------------------------------------------+
//| 保存图像数据的类                                                    |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // 图像像素数组
   uint              m_image_width;  // 图像宽度
   uint              m_image_height; // 图像高度
   string            m_bmp_path;     // 图像文件路径
public:
   //--- (1) 数据数组大小, (2) 设置/返回数据 (像素颜色)
   uint              DataTotal(void)                              { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                  { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data)  { m_image_data[data_index]=data;     }
   //--- 设置/返回图像宽度
   void              Width(const uint width)                      { m_image_width=width;               }
   uint              Width(void)                                  { return(m_image_width);             }
   //--- 设置/返回图像高度
   void              Height(const uint height)                    { m_image_height=height;             }
   uint              Height(void)                                 { return(m_image_height);            }
   //--- 设置/返回图像路径
   void              BmpPath(const string bmp_file_path)          { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                                { return(m_bmp_path);                }

  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CImage::CImage(void) : m_image_width(0),
                       m_image_height(0),
                       m_bmp_path("")
  {
  }
//+------------------------------------------------------------------+
//| 析构器                                                            |
//+------------------------------------------------------------------+
CImage::~CImage(void)
  {
  }

CImage::ReadImageData() 方法旨在保存图像及其属性。方法读取指定路径上的图像并存储其数据。

class CImage
  {
public:
   //--- 读取并存储所传递的图像数据
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| 将所传递的图像保存到一个数组                                          |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- 清除最后的错误
   ::ResetLastError();
//--- 保存图像路径
   m_bmp_file_path=bmp_file_path;
//--- 读取并保存图像数据
   if(!::ResourceReadImage(m_bmp_file_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > 错误: ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

有时也许需要复制相同类型的图像 (CImage)。为此目的, 实现了 CImage::CopyImageData() 方法。在方法开始时, 接收数组的大小设置为源数组的大小。然后在一个循环内将数据从源数组复制到接收数组。

class CImage
  {
public:
   //--- 复制所传递的图像数据
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| 复制所传递的图像数据                                                 |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- 获取接收数组和源数组的大小
   uint data_total        =DataTotal();
   uint source_data_total =::GetPointer(array_source).DataTotal();
//--- 调整接收数组的大小
   ::ArrayResize(m_image_data,source_data_total);
//--- 复制数据
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=::GetPointer(array_source).Data(i);
  }

在此更新之前, CCanvasTable 类使用一个结构来存储图像数据。现在, 引入 CImage 类, 并已进行了相应的修改。 


结论

本文总结了多行文本框控件的开发。其关键特征是对输入的字符数量没有过多的限制, 可以键入多行, 而在 OBJ_EDIT 类型的标准图形对象中缺乏这样的能力。在下一篇文章中, 我们将继续拓展 "表格单元中的控件" 主题, 并使用文章中讨论的控件为表格单元添加修改数值的功能。此外, 几个控件将切换到新模式: 它们将被渲染, 而不是由多个标准图形对象构建。

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

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

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

以下是用于测试的最新版本函数库和文件, 在文章里已经展示过。

如果您在使用这些文件当中提供的材料时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。