Графические интерфейсы IV: Информационные элементы интерфейса (Глава 1)

Anatoli Kazharski | 7 апреля, 2016

Содержание

 


Введение

О том, для чего предназначена эта библиотека, более подробно можно прочитать в самой первой статье: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце статей каждой части представлен список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве. 

На текущий момент в разрабатываемой библиотеке для создания графических интерфейсов есть форма и несколько элементов управления, которые можно к ней присоединять. Ранее уже упоминалось о том, что в одной из следующих статей  будем рассматривать многооконный режим. Сейчас всё готово, чтобы заняться этим вопросом, и мы приступим к нему во второй главе этой статьи. А перед этим, в первой главе, напишем классы, с помощью которых можно будет создавать информационные элементы интерфейса, такие как «статусная строка» и «всплывающая подсказка».


Элемент «Статусная строка»

«Статусная строка» (или «строка состояния») относится к информационным элементам графического интерфейса. Этот элемент предназначен для оперативного вывода каких-либо важных данных, сведений, подсказок и т.д. Например, в терминалах MetaTrader тоже есть строка состояния. Она состоит из нескольких разделов (пунктов). В первом отображается информация о том, в каком разделе терминала сейчас находится курсор мыши или названия команд программы. Также есть пункты, где отображаются дата и цены, когда курсор перемещается в области графика цен. Некоторые пункты содержат контекстное меню, которое вызывается нажатием левой кнопкой мыши. В редакторе кода MetaEditor тоже есть строка состояния. В её пунктах также отображаются подсказки команд программы, местоположение курсора (строка/столбец) и режим ввода текста (INS/OVR). Подробнее о статусных строках терминала и редактора можно прочитать в их справках (F1).

В этой статье мы создадим простой вариант строки состояния, без возможности присоединения к её пунктам контекстных меню. Так же, как и другие элементы интерфейса, статусная строка будет собираться из нескольких объектов-примитивов:

Рис. 1. Составные части элемента «Статусная строка».

Рис. 1. Составные части элемента «Статусная строка».


Создайте файл StatusBar.mqh и подключите его к файлу WndContainer.mqh, чтобы он был доступен в дальнейшем для использования во всей библиотеке. В листинге кода ниже показан класс CStatusBar со стандартными виртуальными методами, которые используются во всех классах элементов управления. 

//+------------------------------------------------------------------+
//|                                                    StatusBar.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Класс для создания статусной строки                              |
//+------------------------------------------------------------------+
class CStatusBar : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //---
public:
                     CStatusBar(void);
                    ~CStatusBar(void);
   //--- Сохраняет указатель формы
   void              WindowPointer(CWindow &object)                   { m_wnd=::GetPointer(object);   }
   //---
public:
   //--- Обработчик событий графика
   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);
   //--- (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);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CStatusBar::CStatusBar(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CStatusBar::~CStatusBar(void)
  {
  }
//+------------------------------------------------------------------+

Методы для обработки событий OnEvent() и OnEventTimer() в этой версии не будут использоваться и оставлены для единообразия с другими классами элементов управления. 

Далее рассмотрим свойства статусной строки. Поскольку предполагаются массивы объектов, то здесь будут общие и уникальные свойства для них. Из уникальных указывается только ширина пунктов. Список общих свойств:

Инициализируем поля свойств в конструкторе значениями по умолчанию. При создании элемента пользователь может их переопределить с помощью публичных методов класса. 

class CStatusBar : public CElement
  {
private:
   //--- Свойства:
   //    Массивы для уникальных свойств
   int               m_width[];
   //--- (1) Цвет фона и (2) рамки фона
   color             m_area_color;
   color             m_area_border_color;
   //--- Цвет текста
   color             m_label_color;
   //--- Приоритет на нажатие левой кнопки мыши
   int               m_zorder;
   //--- Цвета для разделительных линий
   color             m_sepline_dark_color;
   color             m_sepline_light_color;
   //---
public:
   //--- Цвет (1) фона, (2) рамки фона и (3) текста
   void              AreaColor(const color clr)                       { m_area_color=clr;             }
   void              AreaBorderColor(const color clr)                 { m_area_border_color=clr;      }
   void              LabelColor(const color clr)                      { m_label_color=clr;            }
   //--- Цвета разделительных линий
   void              SeparateLineDarkColor(const color clr)           { m_sepline_dark_color=clr;     }
   void              SeparateLineLightColor(const color clr)          { m_sepline_light_color=clr;    }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CStatusBar::CStatusBar(void) : m_area_color(C'240,240,240'),
                               m_area_border_color(clrSilver),
                               m_label_color(clrBlack),
                               m_sepline_dark_color(C'160,160,160'),
                               m_sepline_light_color(clrWhite)
  {
//--- Сохраним имя класса элемента в базовом классе  
   CElement::ClassName(CLASS_NAME);
//--- Установим приоритеты на нажатие левой кнопки мыши
   m_zorder=2;
  }

Рассмотрим методы для создания статусной строки. Для создания фона будем использовать объект-примитив типа CRectLabel. Для пунктов нужно объявить динамический массив объектов-примитивов типа CEdit. Для создания разделительных линий ранее был создан класс CSeparateLine, который может использоваться и как независимый элемент интерфейса. К примеру, сейчас мы будем использовать его в качестве составной части строки состояния, и нам понадобится массив таких элементов.

Перед созданием строки состояния нужно обязательно добавить пункты с помощью метода CStatusBar::AddItem(), иначе создание графического интерфейса прервётся. Получить количество пунктов можно, вызвав метод CStatusBar::ItemsTotal(). 

//+------------------------------------------------------------------+
//|                                                    StatusBar.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "SeparateLine.mqh"
//+------------------------------------------------------------------+
//| Класс для создания статусной строки                              |
//+------------------------------------------------------------------+
class CStatusBar : public CElement
  {
private:
   //--- Объекты для создания кнопки
   CRectLabel        m_area;
   CEdit             m_items[];
   CSeparateLine     m_sep_line[];
   //---
public:
   //--- Методы для создания статусной строки
   bool              CreateStatusBar(const long chart_id,const int subwin,const int x,const int y);
   //---
private:
   bool              CreateArea(void);
   bool              CreateItems(void);
   bool              CreateSeparateLine(const int line_number,const int x,const int y);
   //---
public:
   //--- Количество пунктов
   int               ItemsTotal(void)                           const { return(::ArraySize(m_items)); }

   //--- Добавляет пункт с указанными свойствами до создания статусной строки
   void              AddItem(const int width);
  };

Из всех этих методов рассмотрим подробнее только метод CStatusBar::CreateItems() для создания пунктов статусной строки. Остальные методы не содержат в себе ничего нового, чего мы не рассмотрели бы ранее.

Пункты устанавливаются внутри области фона таким образом, чтобы не перекрывать его рамку. Поэтому в самом начале координатам добавляются отступы в один пиксель. Затем идёт проверка на количество пунктов. Если окажется, что до сих пор не установлено ни одного пункта, то в журнал выводится сообщение об этом, и создание графического интерфейса прерывается. 

Сделаем так, что если ширина первого пункта не задана, то она будет рассчитана автоматически следующим образом. Предполагается, что ширина статусной строки равна ширине формы (минус 2 пикселя, чтобы быть внутри области), к которой её присоединяют. Значит, чтобы получить ширину для первого пункта, нужно в цикле суммировать размеры (ширину) остальных пунктов и это значение отнять от ширины формы. 

Далее идёт цикл, в котором создаются пункты строки состояния, а после него — цикл для создания разделительных линий. Координаты для разделительных линий рассчитываются относительно координат каждого пункта. Для первого пункта разделительная линия не создаётся. Таким образом количество разделительных линий всегда на один объект меньше количества пунктов.  

//+------------------------------------------------------------------+
//| Создаёт список пунктов статусной строки                          |
//+------------------------------------------------------------------+
bool CStatusBar::CreateItems(void)
  {
   int l_w=0;
   int l_x=m_x+1;
   int l_y=m_y+1;
//--- Получим количество пунктов
   int items_total=ItemsTotal();
//--- Если нет ни одного пункта в группе, сообщить об этом и выйти
   if(items_total<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
              "когда в группе есть хотя бы один пункт! Воспользуйтесь методом CStatusBar::AddItem()");
      return(false);
     }
//--- Если ширина первого пункта не задана, то...
   if(m_width[0]<1)
     {
      //--- ...рассчитаем её относительно общей ширины других пунктов
      for(int i=1; i<items_total; i++)
         l_w+=m_width[i];
      //---
      m_width[0]=m_wnd.XSize()-l_w-(items_total+2);
     }
//--- Создадим указанное количество пунктов
   for(int i=0; i<items_total; i++)
     {
      //--- Формирование имени объекта
      string name=CElement::ProgramName()+"_statusbar_edit_"+string(i)+"__"+(string)CElement::Id();
      //--- Координата X
      l_x=(i>0)? l_x+m_width[i-1] : l_x;
      //--- Создание объекта
      if(!m_items[i].Create(m_chart_id,name,m_subwin,l_x,l_y,m_width[i],m_y_size-2))
         return(false);
      //--- Установка свойств
      m_items[i].Description("");
      m_items[i].TextAlign(ALIGN_LEFT);
      m_items[i].Font(FONT);
      m_items[i].FontSize(FONT_SIZE);
      m_items[i].Color(m_label_color);
      m_items[i].BorderColor(m_area_color);
      m_items[i].BackColor(m_area_color);
      m_items[i].Corner(m_corner);
      m_items[i].Anchor(m_anchor);
      m_items[i].Selectable(false);
      m_items[i].Z_Order(m_zorder);
      m_items[i].ReadOnly(true);
      m_items[i].Tooltip("\n");
      //--- Отступы от крайней точки панели
      m_items[i].XGap(l_x-m_wnd.X());
      m_items[i].YGap(l_y-m_wnd.Y());
      //--- Координаты
      m_items[i].X(l_x);
      m_items[i].Y(l_y);
      //--- Размеры
      m_items[i].XSize(m_width[i]);
      m_items[i].YSize(m_y_size-2);
      //--- Сохраним указатель объекта
      CElement::AddToArray(m_items[i]);
     }
//--- Создание разделительных линий
   for(int i=1; i<items_total; i++)
     {
      //--- Координата X
      l_x=m_items[i].X();
      //--- Создание линии
      CreateSeparateLine(i,l_x,l_y+2);
     }
//---
   return(true);
  }

Конечно же, понадобится публичный метод для изменения текста в каждом пункте. Создадим такой метод и назовём его CStatusBar::ValueToItem(). В качестве параметров он будет принимать номер индекса пункта и строку, которую нужно в нём отобразить. 

class CStatusBar : public CElement
  {
public:
   //--- Установка значения по указанному индексу
   void              ValueToItem(const int index,const string value);
  };
//+------------------------------------------------------------------+
//| Устанавливает значение по указанному индексу                     |
//+------------------------------------------------------------------+
void CStatusBar::ValueToItem(const int index,const string value)
  {
//--- Проверка на выход из диапазона
   int array_size=::ArraySize(m_items);
   if(array_size<1 || index<0 || index>=array_size)
      return;
//--- Установка переданного текста
   m_items[index].Description(value);
  }

 


Тест строки состояния

Всё готово для того, чтобы протестировать элемент «Строка состояния». Для теста возьмём первого эксперта из предыдущей части (3) серии. Оставим в нём главное меню и пять кнопок типа CIconButton, а все остальные элементы управления удалим. Файл StatusBar.mqh уже подключен к библиотеке, и теперь в пользовательском классе CProgram можно создать его экземпляр и метод для создания строки состояния.

Создадим строку состояния из двух пунктов. Ширину для первого пункта не будем указывать и тогда она рассчитается автоматически. После создания статусной строки, в качестве примера, в первом пункте установим текст «For Help press F1».

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Статусная строка
   CStatusBar        m_status_bar;
   //---
private:
   //--- Статусная строка
#define STATUSBAR1_GAP_X         (1)
#define STATUSBAR1_GAP_Y         (175)
   bool              CreateStatusBar(void);
  };
//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы 1 для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки
   if(!CreateStatusBar())
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }
//+------------------------------------------------------------------+
//| Создаёт статусную строку                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateStatusBar(void)
  {
#define STATUS_LABELS_TOTAL 2
//--- Передать объект панели
   m_status_bar.WindowPointer(m_window1);
//--- Координаты
   int x=m_window1.X()+STATUSBAR1_GAP_X;
   int y=m_window1.Y()+STATUSBAR1_GAP_Y;
//--- Ширина
   int width[]={0,110};
//--- Установим свойства перед созданием
   m_status_bar.YSize(24);
//--- Укажем сколько должно быть частей и установим им свойства
   for(int i=0; i<STATUS_LABELS_TOTAL; i++)
      m_status_bar.AddItem(width[i]);
//--- Создадим элемент управления
   if(!m_status_bar.CreateStatusBar(m_chart_id,m_subwin,x,y))
      return(false);
//--- Установка текста в первый пункт статусной строки
   m_status_bar.ValueToItem(0,"For Help, press F1");
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_status_bar);
   return(true);
  }

Во втором пункте будем отображать локальное время компьютера. Для этого в таймер приложения добавьте код, как это показано в листинге ниже. То есть, обновление пункта будет происходить каждые 500 миллисекунд.

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();

//--- Обновление второго пункта статусной строки будет каждые 500 миллисекунд
   static int count=0;
   if(count<500)
     {
      count+=TIMER_STEP_MSC;
      return;
     }
//---
   count=0;
   m_status_bar.ValueToItem(1,TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS));
   m_chart.Redraw();
  }

Если всё сделали правильно, то результат должен быть таким, как на скриншоте ниже:

Рис. 2. Тест элемента «Статусная строка».

Рис. 2. Тест элемента «Статусная строка».

 

Разработка класса для создания элемента «Статусная строка» завершена. Полную версию можно посмотреть в приложенных к статье файлах.  

 


Элемент «Всплывающая подсказка»

Далее займёмся разработкой класса для создания всплывающих подсказок. Ранее, в четвёртой главе первой части серии, при рассмотрении функций для кнопок формы, довольно подробно излагалось, почему для этого элемента нужно сделать отдельный класс. Если изложить это очень кратко, то нужны подсказки без ограничений на количество символов и с возможностью выделять некоторые слова. Реализуем это, используя класс для рисования элементов. Ранее уже был показан пример, как рисовать элементы интерфейса. Во второй главе второй части этой серии статей был создан класс CSeparateLine для создания элемента «Разделительная линия». Теперь по такому же принципу напишем класс для создания всплывающих подсказок.

Рис. 3. Пример всплывающей подсказки в программе Word.

Рис. 3. Пример всплывающей подсказки в программе Word.

 

Создайте файл Tooltip.mqh и подключите его к файлу WndContainer.mqh, к которому подключаются все элементы интерфейса. Далее создайте в этом новом файле класс с уже знакомыми стандартными для всех элементов интерфейса методами. Кроме указателя на форму, в этом классе понадобится также указатель на элемент, к которому будет привязываться всплывающая подсказка, а также метод, с помощью которого можно этот указатель сохранить. 

//+------------------------------------------------------------------+
//|                                                      Tooltip.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Класс для создания всплывающей подсказки                         |
//+------------------------------------------------------------------+
class CTooltip : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //--- Указатель на элемент, к которому присоединена всплывающая подсказка
   CElement         *m_element;
   //---
public:
   //--- (1) Сохраняет указатель формы, (2) сохраняет указатель элемента
   void              WindowPointer(CWindow &object)   { m_wnd=::GetPointer(object);     }
   void              ElementPointer(CElement &object) { m_element=::GetPointer(object); }
   //---
public:
   //--- Обработчик событий графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTooltip::CTooltip(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTooltip::~CTooltip(void)
  {
  }

Свойства, которые может задать пользователь перед созданием всплывающей подсказки:

Остальные свойства сделаем со значениями по умолчанию. К ним относятся:

Массив строк можно добавить построчно перед созданием всплывающей подсказки с помощью метода CTooltip::AddString()

class CTooltip : public CElement
  {
private:
   //--- Свойства:
   //    Заголовок
   string            m_header;
   //--- Массив строк текста подсказки
   string            m_tooltip_lines[];
   //--- Значение альфа-канала (прозрачность подсказки)
   uchar             m_alpha;
   //--- Цвета (1) текста, (2) заголовка и (3) рамки фона
   color             m_text_color;
   color             m_header_color;
   color             m_border_color;
   //--- Массив градиента фона
   color             m_array_color[];
   //---
public:
   //--- Заголовок всплывающей подсказки
   void              Header(const string text)        { m_header=text;                  }
   //--- Добавляет строку для подсказки
   void              AddString(const string text);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTooltip::CTooltip(void) : m_header(""),
                           m_alpha(0),
                           m_text_color(clrDimGray),
                           m_header_color(C'50,50,50'),
                           m_border_color(C'118,118,118'),
                           m_gradient_top_color(clrWhite),
                           m_gradient_bottom_color(C'208,208,235')
  {
//--- Сохраним имя класса элемента в базовом классе  
   CElement::ClassName(CLASS_NAME);
  }
//+------------------------------------------------------------------+
//| Добавляет строку                                                 |
//+------------------------------------------------------------------+
void CTooltip::AddString(const string text)
  {
//--- Увеличим размер массивов на один элемент
   int array_size=::ArraySize(m_tooltip_lines);
   ::ArrayResize(m_tooltip_lines,array_size+1);
//--- Сохраним значения переданных параметров
   m_tooltip_lines[array_size]=text;
  }

Так же, как и в классе элемента «Разделительная линия», для создания всплывающей подсказки будет использоваться класс CRectCanvas, а также два метода (публичный и приватный). 

class CTooltip : public CElement
  {
private:
   //--- Объекты для создания всплывающей подсказки
   CRectCanvas       m_canvas;
   //---
public:
   //--- Методы для создания всплывающей подсказки
   bool              CreateTooltip (const long chart_id,const int subwin);
   //---
private:
   //--- Создаёт холст для рисования подсказки
   bool              CreateCanvas(void);
  };

В публичном методе CTooltip::CreateTooltip(), кроме проверки на наличие указателя на форму, к которой будет присоединён элемент, есть ещё аналогичная проверка на наличие указателя на элемент, для которого эта подсказка предназначена. При наличии указателя на элемент, координаты для всплывающей подсказки рассчитываются относительно координат этого элемента. В данном случае — на один пиксель ниже нижней границы элемента.

//+------------------------------------------------------------------+
//| Создаёт объект Tooltip                                           |
//+------------------------------------------------------------------+
bool CTooltip::CreateTooltip(const long chart_id,const int subwin)
  {
//--- Выйти, если нет указателя на форму
   if(::CheckPointer(m_wnd)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Перед созданием всплывающей подсказки классу нужно передать "
              "указатель на форму: CTooltip::WindowPointer(CWindow &object).");
      return(false);
     }
//--- Выйти, если нет указателя на элемент
   if(::CheckPointer(m_element)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Перед созданием всплывающей подсказки классу нужно передать "
              "указатель на элемент: CTooltip::ElementPointer(CElement &object).");
      return(false);
     }
//--- Инициализация переменных
   m_id       =m_wnd.LastId()+1;
   m_chart_id =chart_id;
   m_subwin   =subwin;
   m_x        =m_element.X();
   m_y        =m_element.Y2()+1;
//--- Отступы от крайней точки
   CElement::XGap(m_x-m_wnd.X());
   CElement::YGap(m_y-m_wnd.Y());
//--- Создаёт всплывающую подсказку
   if(!CreateTooltip())
      return(false);
//---
   return(true);
  }

В приватном методе создания холста для рисования CTooltip::CreateCanvas(), в отличие от аналогичного метода в классе CSeparateLine, вместо рисования после создания объекта и установки свойств, далее производятся следующие действия:

В самом конце метода объект добавляется в общий массив объектов элемента. В листинге ниже можно подробнее изучить код этого метода. 

//+------------------------------------------------------------------+
//| Создаёт холст для рисования                                      |
//+------------------------------------------------------------------+
bool CTooltip::CreateCanvas(void)
  {
//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_help_tooltip_"+(string)CElement::Id();
//--- Создадим холст
   if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,m_x,m_y,m_x_size,m_y_size,COLOR_FORMAT_ARGB_NORMALIZE))
      return(false);
//--- Прикрепить к графику
   if(!m_canvas.Attach(m_chart_id,name,m_subwin,1))
      return(false);
//--- Установим свойства
   m_canvas.Background(false);
//--- Отступы от крайней точки
   m_canvas.XGap(m_x-m_wnd.X());
   m_canvas.YGap(m_y-m_wnd.Y());
//--- Установка размера массива градиента для фона подсказки
   CElement::GradientColorsTotal(m_y_size);
   ::ArrayResize(m_array_color,m_y_size);
//--- Инициализация массива градиента
   CElement::InitColorArray(m_gradient_top_color,m_gradient_bottom_color,m_array_color);
//--- Очистка холста для рисования
   m_canvas.Erase(::ColorToARGB(clrNONE,0));
   m_canvas.Update();
   m_alpha=0;
//--- Сохраним указатель объекта
   CElement::AddToArray(m_canvas);
   return(true);
  }

Далее рассмотрим методы для рисования вертикального градиента и рамки. Для градиента уже есть массив, инициализированный при создании холста нужными значениями. Поэтому в цикле, количество итераций которого равно высоте холста, просто рисуем линию за линией с указанным из массива цветом. Для рисования рамки в самом начале метода объявляются и тут же инициализируются массивы с координатами для каждой стороны холста. После того, как рамка нарисована, производится округление углов на один пиксель и затем дорисовываются ещё четыре пикселя по четырём углам внутрь холста. 

В обоих методах есть только один параметр – альфа-канал. То есть, вызывая эти методы можно указать, с какой степенью прозрачности нужно нарисовать градиент и рамку. 

//+------------------------------------------------------------------+
//| Вертикальный градиент                                            |
//+------------------------------------------------------------------+
void CTooltip::VerticalGradient(const uchar alpha)
  {
//--- Координаты X
   int x1=0;
   int x2=m_x_size;
//--- Рисуем градиент
   for(int y=0; y<m_y_size; y++)
      m_canvas.Line(x1,y,x2,y,::ColorToARGB(m_array_color[y],alpha));
  }
//+------------------------------------------------------------------+
//| Рамка                                                            |
//+------------------------------------------------------------------+
void CTooltip::Border(const uchar alpha)
  {
//--- Цвет рамки
   color clr=m_border_color;
//--- Границы
   int x_size =m_canvas.X_Size()-1;
   int y_size =m_canvas.Y_Size()-1;
//--- Координаты: Сверху/Справа/Снизу/Слева
   int x1[4]; x1[0]=0;      x1[1]=x_size; x1[2]=0;      x1[3]=0;
   int y1[4]; y1[0]=0;      y1[1]=0;      y1[2]=y_size; y1[3]=0;
   int x2[4]; x2[0]=x_size; x2[1]=x_size; x2[2]=x_size; x2[3]=0;
   int y2[4]; y2[0]=0;      y2[1]=y_size; y2[2]=y_size; y2[3]=y_size;
//--- Рисуем рамку по указанным координатам
   for(int i=0; i<4; i++)
      m_canvas.Line(x1[i],y1[i],x2[i],y2[i],::ColorToARGB(clr,alpha));
//--- Округление по углам на один пиксель
   clr=clrBlack;
   m_canvas.PixelSet(0,0,::ColorToARGB(clr,0));
   m_canvas.PixelSet(0,m_y_size-1,::ColorToARGB(clr,0));
   m_canvas.PixelSet(m_x_size-1,0,::ColorToARGB(clr,0));
   m_canvas.PixelSet(m_x_size-1,m_y_size-1,::ColorToARGB(clr,0));
//--- Дорисовка пикселей по указанным координатам
   clr=C'180,180,180';
   m_canvas.PixelSet(1,1,::ColorToARGB(clr,alpha));
   m_canvas.PixelSet(1,m_y_size-2,::ColorToARGB(clr,alpha));
   m_canvas.PixelSet(m_x_size-2,1,::ColorToARGB(clr,alpha));
   m_canvas.PixelSet(m_x_size-2,m_y_size-2,::ColorToARGB(clr,alpha));
  }

Сделаем так, чтобы подсказка сразу появлялась при наведении курсора мыши на элемент и плавно исчезала, когда курсор уходит из области элемента. 

Для отображения (появления) подсказки создадим метод CTooltip::ShowTooltip(). В самом начале этого метода производится проверка на значение поля m_alpha (альфа-канал). Если установлено, что значение альфа-канала уже равно или выше 255, то это означает, что программа уже сюда заходила и подсказка видна на 100%, поэтому не имеет смысла идти дальше. В противном случае далее устанавливаем координаты и отступ для заголовка подсказки, рисуем градиент и рамку. Заголовок не будет нарисован, если пользователь не указал текст для него. Если текст для заголовка есть, то устанавливаются параметры для шрифта и рисуется заголовок. 

После этого определяются координаты для основного текста подсказки с учётом наличия заголовка. Устанавливаются параметры уже для основного текста. В отличие от заголовка, текст которого выделен жирным шрифтом (FW_BLACK), у основного текста шрифт будет менее броский (FW_THIN). Затем в цикле из инициализированного пользователем массива на холст выводится текст подсказки. Координата по оси Y для каждой строки корректируется на указанное значение в каждой итерации. В самом конце холст обновляется для отображения изменений и устанавливается признак полностью видимой подсказки. В листинге ниже вы можете подробнее ознакомиться с кодом этого метода. 

//+------------------------------------------------------------------+
//| Отображает всплывающую подсказку                                 |
//+------------------------------------------------------------------+
void CTooltip::ShowTooltip(void)
  {
//--- Выйти, если подсказка видна на 100%
   if(m_alpha>=255)
      return;
//--- Координаты и отступ для заголовка
   int  x        =5;
   int  y        =5;
   int  y_offset =15;
//--- Рисуем градиент
   VerticalGradient(255);
//--- Рисуем рамку
   Border(255);
//--- Рисуем заголовок (если установлен)
   if(m_header!="")
     {
      //--- Установим параметры шрифта
      m_canvas.FontSet(FONT,-80,FW_BLACK);
      //--- Рисуем текст заголовка
      m_canvas.TextOut(x,y,m_header,::ColorToARGB(m_header_color),TA_LEFT|TA_TOP);
     }
//--- Координаты для основного текста подсказки (с учётом наличия заголовка)
   x=(m_header!="")? 15 : 5;
   y=(m_header!="")? 25 : 5;
//--- Установим параметры шрифта
   m_canvas.FontSet(FONT,-80,FW_THIN);
//--- Рисуем основной текст подсказки
   int lines_total=::ArraySize(m_tooltip_lines);
   for(int i=0; i<lines_total; i++)
     {
      m_canvas.TextOut(x,y,m_tooltip_lines[i],::ColorToARGB(m_text_color),TA_LEFT|TA_TOP);
      y=y+y_offset;
     }
//--- Обновить холст
   m_canvas.Update();
//--- Признак полностью видимой подсказки
   m_alpha=255;
  }

Для плавного исчезновения подсказки напишем метод CTooltip::FadeOutTooltip(). В самом начале этого метода производится обратная проверка на значение альфа-канала. То есть, если мы уже достигли полной прозрачности, то дальше идти не нужно. В противном случае в цикле с указанным шагом на убывание холст перерисовывается, пока не будет достигнута полная прозрачность. В листинге ниже представлена полная версия этого метода.

//+------------------------------------------------------------------+
//| Плавное исчезновение всплывающей подсказки                       |
//+------------------------------------------------------------------+
void CTooltip::FadeOutTooltip(void)
  {
//--- Выйти, если подсказка скрыта на 100%
   if(m_alpha<1)
      return;
//--- Отступ для заголовка
   int y_offset=15;
//--- Шаг прозрачности
   uchar fadeout_step=7;
//--- Плавное исчезновение подсказки
   for(uchar a=m_alpha; a>=0; a-=fadeout_step)
     {
      //--- Если следующий шаг в минус, остановим цикл
      if(a-fadeout_step<0)
        {
         a=0;
         m_canvas.Erase(::ColorToARGB(clrNONE,0));
         m_canvas.Update();
         m_alpha=0;
         break;
        }
      //--- Координаты для заголовка
      int x =5;
      int y =5;
      //--- Рисуем градиент и рамку
      VerticalGradient(a);
      Border(a);
      //--- Рисуем заголовок (если установлен)
      if(m_header!="")
        {
         //--- Установим параметры шрифта
         m_canvas.FontSet(FONT,-80,FW_BLACK);
         //--- Рисуем текст заголовка
         m_canvas.TextOut(x,y,m_header,::ColorToARGB(m_header_color,a),TA_LEFT|TA_TOP);
        }
      //--- Координаты для основного текста подсказки (с учётом наличия заголовка)
      x=(m_header!="")? 15 : 5;
      y=(m_header!="")? 25 : 5;
      //--- Установим параметры шрифта
      m_canvas.FontSet(FONT,-80,FW_THIN);
      //--- Рисуем основной текст подсказки
      int lines_total=::ArraySize(m_tooltip_lines);
      for(int i=0; i<lines_total; i++)
        {
         m_canvas.TextOut(x,y,m_tooltip_lines[i],::ColorToARGB(m_text_color,a),TA_LEFT|TA_TOP);
         y=y+y_offset;
        }
      //--- Обновить холст
      m_canvas.Update();
     }
  }

Все методы для создания и рисования подсказки готовы. Вызывать их нужно в обработчике событий элемента. Чтобы узнать, нажата ли сейчас на форме кнопка для отображения подсказок, в классе CWindow нужно создать метод CWindow::TooltipBmpState():

class CWindow : public CElement
  {
public:
   //--- Проверка режима показа всплывающих подсказок
   bool              TooltipBmpState(void)                             const { return(m_button_tooltip.State());   }
  };

Теперь, если режим для отображения подсказок включен, в случае, когда фокус на элементе, пользователю будет показываться подсказка. Если же курсор мыши находится вне области элемента или форма заблокирована, то подсказка будет скрываться.

//+------------------------------------------------------------------+
//| Обработчик событий графика                                       |
//+------------------------------------------------------------------+
void CTooltip::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::IsVisible())
         return;
      //--- Выйти, если кнопка всплывающих подсказок на форме отключена
      if(!m_wnd.TooltipBmpState())
         return;
      //--- Если форма заблокирована
      if(m_wnd.IsLocked())
        {
         //--- Скрыть подсказку
         FadeOutTooltip();
         return;
        }
      //--- Если есть фокус на элементе
      if(m_element.MouseFocus())
         //--- Показать подсказку
         ShowTooltip();
      //--- Если нет фокуса
      else
      //--- Скрыть подсказку
         FadeOutTooltip();
      //---
      return;
     }
  }

 


Тест всплывающих подсказок

Теперь можно протестировать, как всё это работает. На форме в тестовом эксперте сейчас пять кнопок. Сделаем для каждой кнопки всплывающую подсказку. Объявляем пять экземпляров классов типа CTooltip и пять методов. Для примера приведём имплементацию только одного из них.

class CProgram : public CWndEvents
  {
private:
   CTooltip          m_tooltip1;
   CTooltip          m_tooltip2;
   CTooltip          m_tooltip3;
   CTooltip          m_tooltip4;
   CTooltip          m_tooltip5;
   //---
private:
   bool              CreateTooltip1(void);
   bool              CreateTooltip2(void);
   bool              CreateTooltip3(void);
   bool              CreateTooltip4(void);
   bool              CreateTooltip5(void);
  };
//+------------------------------------------------------------------+
//| Создаёт всплывающую подсказку 5                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateTooltip5(void)
  {
#define TOOLTIP5_LINES_TOTAL 3
//--- Сохраним указатель на окно
   m_tooltip5.WindowPointer(m_window1);
//--- Сохраним указатель на элемент
   m_tooltip5.ElementPointer(m_icon_button5);
//--- Массив с текстом подсказки
   string text[]=
     {
      "Элемент управления \"Кнопка с картинкой\" (5).",
      "Это вторая строка подсказки.",
      "Это третья строка подсказки."
     };
//--- Установим свойства перед созданием
   m_tooltip5.Header("Icon Button 5");
   m_tooltip5.XSize(250);
   m_tooltip5.YSize(80);
//--- Добавим текст построчно
   for(int i=0; i<TOOLTIP5_LINES_TOTAL; i++)
      m_tooltip5.AddString(text[i]);
//--- Создадим элемент управления
   if(!m_tooltip5.CreateTooltip(m_chart_id,m_subwin))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_tooltip5);
   return(true);
  }

В главном методе создания графического интерфейса создание всплывающих подсказок нужно осуществлять в последнюю очередь, чтобы они оказались в самом конце в массиве элементов в базе. Таким образом, они всегда будут располагаться выше других объектов на графике. 

//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы 1 для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки
//--- Кнопки с картинкой

//--- Всплывающие подсказки
   if(!CreateTooltip1())
      return(false);
   if(!CreateTooltip2())
      return(false);
   if(!CreateTooltip3())
      return(false);
   if(!CreateTooltip4())
      return(false);
   if(!CreateTooltip5())
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

Чтобы отображать кнопку подсказок на форме, при её создании нужно использовать метод CWindow::UseTooltipsButton():

//+------------------------------------------------------------------+
//| Создаёт форму 1 для элементов управления                         |
//+------------------------------------------------------------------+
bool CProgram::CreateWindow1(const string caption_text)
  {
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_window1);
//--- Координаты
   int x=1;
   int y=20;
//--- Свойства
   m_window1.Movable(true);
   m_window1.XSize(251);
   m_window1.YSize(200);
   m_window1.UseTooltipsButton();
   m_window1.CaptionBgColor(clrCornflowerBlue);
   m_window1.CaptionBgColorHover(C'150,190,240');
//--- Создание формы
   if(!m_window1.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   return(true);
  }

На скриншоте ниже показан итоговый результат:

Рис. 4. Тест всплывающей подсказки.

Рис. 4. Тест всплывающей подсказки.

 

Выглядит и работает неплохо! Но теперь для этого элемента нужно сделать персональный массив в базе указателей. В дальнейшем при разработке многооконного режима будет показано, в каких случаях это может пригодиться.  

 


Персональный массив для всплывающих подсказок

Итак,  переходим в класс CWndContainer и дополняем его новыми членами. В структуру WindowElements нужно добавить персональный массив для всплывающих подсказок. Также создадим (1) метод CWndContainer::TooltipsTotal() для получения количества подсказок и (2) метод CWndContainer::AddTooltipElements() для добавления указателя подсказки в персональный массив.

class CWndContainer
  {
protected:
   //--- Структура массивов элементов
   struct WindowElements
     {
      //--- Всплывающие подсказки
      CTooltip         *m_tooltips[];
     };
   //--- Массив массивов элементов для каждого окна
   WindowElements    m_wnd[];
   //---
public:
   //--- Количество всплывающих подсказок
   int               TooltipsTotal(const int window_index);
   //---
private:
   //--- Сохраняет указатели на элементы всплывающих подсказок в базу
   bool              AddTooltipElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Возвращает кол-во подсказок по указанному индексу окна           |
//+------------------------------------------------------------------+
int CWndContainer::TooltipsTotal(const int window_index)
  {
   if(window_index>=::ArraySize(m_wnd))
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return(::ArraySize(m_wnd[window_index].m_tooltips));
  }
//+------------------------------------------------------------------+
//| Сохраняет указатель на подсказку в персональный массив           |
//+------------------------------------------------------------------+
bool CWndContainer::AddTooltipElements(const int window_index,CElement &object)
  {
//--- Выйдем, если это не всплывающая подсказка
   if(object.ClassName()!="CTooltip")
      return(false);
//--- Получим указатель на всплывающую подсказку
   CTooltip *t=::GetPointer(object);
//--- Добавим указатель в персональный массив
   AddToRefArray(t,m_wnd[window_index].m_tooltips);
   return(true);
  }

Вызов метода CWndContainer::AddTooltipElements() нужно осуществлять в публичном методе CWndContainer::AddToElementsArray(), который используется в пользовательском классе при добавлении элемента в базу. В листинге кода ниже показана его сокращённая версия.

//+------------------------------------------------------------------+
//| Добавляет указатель в массив элементов                           |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- Если в базе нет форм для элементов управления
//--- Если запрос на несуществующую форму
//--- Добавим в общий массив элементов
//--- Добавим объекты элемента в общий массив объектов
//--- Запомним во всех формах id последнего элемента
//--- Увеличим счётчик идентификаторов элементов

//--- Сохраняет указатели на объекты контекстного меню в базу
//--- Сохраняет указатели на объекты главного меню в базу
//--- Сохраняет указатели на объекты сдвоенной кнопки в базу

//--- Сохраняет указатели на объекты всплывающей подсказки в базу
   if(AddTooltipElements(window_index,object))
      return;
  }

Разработка класса для создания всплывающих подсказок завершена. Полную версию Вы можете загрузить к себе на компьютер в конце статьи. 

 


Заключение

В этой статье (первой главе четвёртой части) мы рассмотрели разработку информационных элементов интерфейса, таких как «Строка состояния» и «Всплывающая подсказка». Во второй главе четвёртой части серии реализуем возможность создавать многооконные графические интерфейсы, а также будет затронута такая тема, как система управления приоритетами на нажатие левой кнопкой мыши.

Ниже вы можете загрузить к себе на компьютер весь материал первой части серии, чтобы у Вас сразу была возможность протестировать, как всё это работает. Если у Вас возникают вопросы по использованию материала предоставленного в этих файлах, то Вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей в представленном списке ниже или задать вопрос в комментариях к статье.

Список статей (глав) четвёртой части: