Прочие классы в библиотеке DoEasy (Часть 70): Расширение функционала и автообновление коллекции объектов-чартов
Содержание
- Концепция
- Доработка классов библиотеки
- Автообновление класса-коллекции объектов чартов и окон
- Тестирование
- Что дальше
Концепция
В прошлой статье мы создали коллекцию объектов-чартов. Теперь у нас каждый открытый в терминале график символа представлен объектом-чартом. Каждый объект-чарт в своём составе имеет набор объектов-окон, в которых расположены объекты-индикаторы окна. Любой объект-чарт имеет как минимум один объект-окно — это главное окно графика. Все остальные окна индикаторов могут добавляться в список окон чарта и удаляться из него. Весь этот набор объектов мы расположили в коллекции объектов-чартов.
Подробное тестирование коллекции объектов-чартов из прошлой статьи выявило некоторые проблемы при добавлении новых окон на основное окно графика. Их мы сегодня исправим. Помимо этого мы добавим новый функционал объектам-чартам — навигацию в окне графика символа, создание скриншотов окон и сохранение и загрузку шаблонов на график.
В дополнение к запланированным доработкам сегодня сделаем автоматическое отслеживание некоторых событий, происходящих с графиками в клиентском терминале и с окнами объектов-чартов — добавление нового/удаление существующего графика символа (объекта-чарта), добавление нового/удаление существующего окна индикатора к объекту-чарту и добавление нового/удаление существующего индикатора в окно чарта.
Доработка классов библиотеки
Приступим. В первую очередь (как стало обычным) в файл \MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений:
MSG_CHART_OBJ_CHART_WINDOW, // Главное окно графика MSG_CHART_OBJ_CHART_SUBWINDOW, // Подокно графика MSG_CHART_OBJ_CHART_SUBWINDOWS_NUM, // Подокон MSG_CHART_OBJ_INDICATORS_MW_NAME_LIST, // Индикаторы в главном окне графика MSG_CHART_OBJ_INDICATORS_SW_NAME_LIST, // Индикаторы в окне графика MSG_CHART_OBJ_INDICATOR, // Индикатор MSG_CHART_OBJ_INDICATORS_TOTAL, // Индикаторов MSG_CHART_OBJ_WINDOW_N, // Окно MSG_CHART_OBJ_INDICATORS_NONE, // Отсутствуют MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ, // Не удалось получить объект-окно графика MSG_CHART_OBJ_SCREENSHOT_CREATED, // Скриншот создан MSG_CHART_OBJ_TEMPLATE_SAVED, // Шаблон графика сохранён MSG_CHART_OBJ_TEMPLATE_APPLIED, // Шаблон применён к графику //--- CChartObjCollection MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION, // Коллекция чартов MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ, // Не удалось создать новый объект-чарт MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART, // Не удалось добавить объект-чарт в коллекцию MSG_CHART_COLLECTION_ERR_CHARTS_MAX, // Нельзя открыть новый график, так как количество открытых графиков уже максимальное }; //+------------------------------------------------------------------+
и тексты сообщений, соответствующие вновь добавленным индексам:
{"Главное окно графика","Main chart window"}, {"Подокно графика","Chart subwindow"}, {"Подокон","Subwindows"}, {"Индикаторы в главном окне графика","Indicators in the main chart window"}, {"Индикаторы в окне графика","Indicators in the chart window"}, {"Индикатор","Indicator"}, {"Индикаторов","Indicators total"}, {"Окно","Window"}, {"Отсутствуют","No indicators"}, {"Не удалось получить объект-окно графика","Failed to get the chart window object"}, {"Скриншот создан","Screenshot created"}, {"Шаблон графика сохранён","Chart template saved"}, {"Шаблон применён к графику","Template applied to the chart"}, //--- CChartObjCollection {"Коллекция чартов","Chart collection"}, {"Не удалось создать новый объект-чарт","Failed to create new chart object"}, {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"}, {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"}, }; //+---------------------------------------------------------------------+
Так как сегодня будем делать дополнительный функционал объектов-чартов, в который будет входить и создание скриншотов и работа с шаблонами, то нам нужно указать папки хранения для скриншотов и шаблонов, а также расширение имени файла по умолчанию (а значит — и формат файла сохраняемого изображения) для скриншотов. Тип файлов, возможных для сохранения скриншотов, может быть *.gif, *.png и *.bmp.
В файле \MQL5\Include\DoEasy\Defines.mqh добавим эти новые макроподстановки:
//--- Параметры данных для файловых операций #define DIRECTORY ("DoEasy\\") // Каталог библиотеки для расположения папок объектов классов #define RESOURCE_DIR ("DoEasy\\Resource\\") // Каталог библиотеки для расположения папок ресурсов #define SCREENSHOT_DIR ("DoEasy\\ScreenShots\\") // Каталог библиотеки для расположения папок скриншотов #define TEMPLATE_DIR ("DoEasy\\") // Каталог библиотеки для расположения папок шаблонов #define FILE_EXT_GIF (".gif") // Расширение имени файла изображения GIF #define FILE_EXT_PNG (".png") // Расширение имени файла изображения PNG #define FILE_EXT_BMP (".bmp") // Расширение имени файла изображения BMP #define SCREENSHOT_FILE_EXT (FILE_EXT_PNG) // Формат файла скриншотов графиков (расширение: можно использовать .gif, .png и .bmp) //--- Параметры символов
Следует отметить, что папки хранения для скриншотов и шаблонов в терминале отличаются.
Скриншоты сохраняются в папку (Каталог данных терминала)\MQL5\Files\
А шаблоны сохраняются в папку (Каталог данных терминала)\ MQL5\Profiles\Templates\
Таким образом, добавление к имени файла указанных макроподстановок сделает хранение файлов библиотеки более адресным.
Скриншоты будут сохраняться в папку \MQL5\Files\DoEasy\ScreenShots\, а шаблоны — в папку MQL5\Profiles\Templates\DoEasy\.
Для удобства сохранения файлов скриншотов сделаем функцию в файле сервисных функций \MQL5\Include\DoEasy\Services\DELib.mqh. Функция будет возвращать имя файла, состоящее из наименования программы, из которой она запущена, префикса, передаваемого в параметрах функции, и локального времени компьютера:
//+------------------------------------------------------------------+ //| Возвращает имя для файла (имя программы+локальное время) | //+------------------------------------------------------------------+ string FileNameWithTimeLocal(const string time_prefix=NULL) { string name= ( MQLInfoString(MQL_PROGRAM_NAME)+"_"+time_prefix+(time_prefix==NULL ? "" : "_")+ TimeToString(TimeLocal(),TIME_DATE|TIME_MINUTES|TIME_SECONDS) ); ResetLastError(); if(StringReplace(name," ","_")==WRONG_VALUE) CMessage::ToLog(DFUN,GetLastError(),true); if(StringReplace(name,":",".")==WRONG_VALUE) CMessage::ToLog(DFUN,GetLastError(),true); return name; } //+------------------------------------------------------------------+
В функции создаётся строка из наименования программы + переданное значение в параметрах функции + локальное время компьютера в формате Дата/Часы-Минуты/Секунды. Затем все пробелы заменяются знаками подчёркивания (_), а все двоеточия — точками (.) и возвращается полученная строка. Если замены не сработали, то будут выведены об этом сообщения в журнал.
Хочется отметить, что функция будет возвращать одинаковое имя файла в течении секунды. Т. е. если за секунду вызвать функцию несколько раз, то она всегда вернёт одну и ту же строку в течении этой секунды. Поэтому здесь введён входной параметр функции, в котором можно передать дополнительную информацию о файле для уникальности его идентификации и, как дополнение — большей информативности.
Для каждого окна графика мы можем узнать координаты в пикселях, соответствующие координатам время/цена при помощи ChartXYToTimePrice(), а также сделать и обратное преобразование при помощи ChartTimePriceToXY(). Добавим такой функционал и нашим объектам. При этом первая функция будет работать в объекте-чарте, тогда как вторая — в объекте окна чарта. Дело в том, что функция ChartXYToTimePrice() в своих параметрах имеет номер подокна, в котором находится курсор, и который возвращается по ссылке из функции. Работает это в любом окне графика — просто в переменную, которую мы подставляем в параметры функции при её вызове, записывается номер окна графика, в котором расположен курсор. А вот во вторую функцию — для преобразования времени и цены в окне графика в соответствующие им координаты экрана в пикселях, мы сами должны передать номер того окна, координаты которого в виде время/цена нам необходимо получить из экранных координат окна. И эту функцию, т. е. метод, который будет с ней работать, нам целесообразнее разместить в объекте окна чарта, и уже из него получать соответствующие координаты.
В файле \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh в приватной секции класса добавим переменные для хранения координат курсора в окне:
//+------------------------------------------------------------------+ //| Класс объекта-окна графика | //+------------------------------------------------------------------+ class CChartWnd : public CBaseObj { private: CArrayObj m_list_ind; // Список индикаторов int m_window_num; // Номер подокна int m_wnd_coord_x; // Координата X для времени на графике в окне int m_wnd_coord_y; // Координата Y для цены на графике в окне //--- Возвращает флаг наличия индикатора из списка в окне bool IsPresentInWindow(const CWndInd *ind); //--- Удаляет из списка уже отсутствующие в окне индикаторы void IndicatorsDelete(void); //--- Добавляет в список новые индикаторы void IndicatorsAdd(void); //--- Устанавливает номер подокна void SetWindowNum(const int num) { this.m_window_num=num; } public:
В публичной секции класса объявим метод, преобразующий координаты графика из представления время/цена в координаты по оси X и Y и напишем два метода, возвращающих уже полученные координаты в переменные, а также метод, возвращающий относительную координату Y в окне:
//--- Обновляет данные по прикреплённым индикаторам void Refresh(void); //--- Преобразует координаты графика из представления время/цена в координаты по оси X и Y bool TimePriceToXY(const datetime time,const double price); //--- Возвращает координаты X и Y расположения курсора в окне int XFromTimePrice(void) const { return this.m_wnd_coord_x; } int YFromTimePrice(void) const { return this.m_wnd_coord_y; } //--- Возвращает относительную координату Y расположения курсора в окне int YFromTimePriceRelative(void) const { return this.m_wnd_coord_y-this.YDistance();} }; //+------------------------------------------------------------------+
Так как значения координаты Y во всех окнах указаны от начала координат (верхний левый угол главного окна графика), то для того, чтобы получить координату относительно верхней границы окна, нам нужно вычесть из координаты Y дистанцию до верхнего края данного окна, что мы и делаем в последнем методе.
В параметрическом конструкторе класса в его списке инициализации инициализируем новые переменные значениями по умолчанию:
//+------------------------------------------------------------------+ //| Параметрический конструктор | //+------------------------------------------------------------------+ CChartWnd::CChartWnd(const long chart_id,const int wnd_num) : m_window_num(wnd_num), m_wnd_coord_x(0), m_wnd_coord_y(0) { CBaseObj::SetChartID(chart_id); this.IndicatorsListCreate(); } //+------------------------------------------------------------------+
За пределами тела класса напишем реализацию метода, преобразующего координаты графика из представления время/цена в координаты по оси X и Y:
//+------------------------------------------------------------------+ //| Преобразует координаты графика из представления | //| время/цена в координаты по оси X и Y | //+------------------------------------------------------------------+ bool CChartWnd::TimePriceToXY(const datetime time,const double price) { ::ResetLastError(); if(!::ChartTimePriceToXY(this.m_chart_id,this.m_window_num,time,price,this.m_wnd_coord_x,this.m_wnd_coord_y)) { //CMessage::ToLog(DFUN,::GetLastError(),true); return false; } return true; } //+------------------------------------------------------------------+
Здесь мы просто возвращаем результат работы функции ChartTimePriceToXY(), передавая в неё все необходимые значения. Сообщение в лог об ошибке я закомментировал по причине, что получаем слишком много этих сообщений в журнал в случае, если курсор нахоится вне поля графика, то в пределах рамки окна этого графика.
Метод записывает полученный результат в новые переменные для их хранения, добавленные нами выше, а методы XFromTimePrice() и YFromTimePrice() возвращают значения этих переменных. Таким образом, мы сначала должны вызвать метод TimePriceToXY(), а после того, как он вернул true, мы можем получить значение нужной нам координаты.
Доработаем метод для обновления данных по прикреплённым индикаторам к окну. Чтобы постоянно не пересоздавать список индикаторов, мы сначала сравним текущее количество индикаторов в окне с количеством в списке, и только в случае наличия изменений пересоздадим список индикаторов:
//+------------------------------------------------------------------+ //| Обновляет данные по прикреплённым индикаторам | //+------------------------------------------------------------------+ void CChartWnd::Refresh(void) { int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total(); if(change!=0) { this.IndicatorsDelete(); this.IndicatorsAdd(); } } //+------------------------------------------------------------------+
Доработаем класс объекта-чарта, расположенный в файле \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh. В прошлой статье мы сделали работу метода WindowsTotal() таким образом, что за один вызов сразу получали значение из окружения и записывали его же в свойства объекта. Однако это оказалось не очень практично с точки зрения наглядности построения логики кода и количества обращений к окружению, и я решил отказаться от этой затеи. Теперь метод просто возвращает значение свойства объекта:
int WindowsTotal(void) const { return (int)this.GetProperty(CHART_PROP_WINDOWS_TOTAL); }
Запись же этого значения из окружения в свойство объекта сделаем отдельно там, где это действительно необходимо.
В объект-чарт будем добавлять остальной дополнительный функционал, запланированный на сегодня: навигация по графику, создание скриншотов графика, работа с шаблонами графика и преобразование координат X и Y графика в значения время и цена.
Подключим к файлу класса CChartObj файл класса CSelect:
//+------------------------------------------------------------------+ //| ChartObj.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/ru/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/ru/users/artmedia70" #property version "1.00" #property strict // Нужно для mql4 //+------------------------------------------------------------------+ //| Включаемые файлы | //+------------------------------------------------------------------+ #include "..\..\Objects\BaseObj.mqh" #include "..\..\Services\Select.mqh" #include "ChartWnd.mqh" //+------------------------------------------------------------------+
Он нам потребуется для фильтрации списка объектов-чартов по их свойствам.
В приватной секции класса впишем две новые переменные-члены класса для хранения времени для координаты X и цены для координаты Y на графике:
//+------------------------------------------------------------------+ //| Класс объекта-чарта | //+------------------------------------------------------------------+ class CChartObj : public CBaseObj { private: CArrayObj m_list_wnd; // Список объектов окон графика long m_long_prop[CHART_PROP_INTEGER_TOTAL]; // Целочисленные свойства double m_double_prop[CHART_PROP_DOUBLE_TOTAL]; // Вещественные свойства string m_string_prop[CHART_PROP_STRING_TOTAL]; // Строковые свойства int m_digits; // Digits() символа datetime m_wnd_time_x; // Время для координаты X на графике в окне double m_wnd_price_y; // Цена для координаты Y на графике в окне
Здесь же — в приватной секции класса, объявим метод, добавляющий расширение файлу скриншота при его отсутствии:
//--- Создаёт список окон графика void CreateWindowsList(void); //--- Добавляет расширение файлу скриншота при его отсутствии string FileNameWithExtention(const string filename); public:
В конце листинга тела класса объявим новые методы, которые сегодня запланировали сделать:
//--- Возвращает флаг, что объект-чарт принадлежит графику программы bool IsMainChart(void) const { return(this.m_chart_id==CBaseObj::GetMainChartID()); } //--- Возвращает указанное по индексу окно графика CChartWnd *GetWindowByIndex(const int index) const { return this.m_list_wnd.At(index); } //--- Возвращает объект-окно по его номеру подокна CChartWnd *GetWindowByNum(const int win_num) const; //--- Возвращает объект-окно по имени индикатора в нём CChartWnd *GetWindowByIndicator(const string ind_name) const; //--- Выводит в журнал данные всех индикаторов всех окон графика void PrintWndIndicators(void); //--- Выводит в журнал свойства всех окон графика void PrintWndParameters(void); //--- Сдвигает график на указанное количество баров относительно указанной позиции графика bool Navigate(const int shift,const ENUM_CHART_POSITION position); //--- Сдвигает график (1) влево, (2) вправо на указанное количество баров bool NavigateLeft(const int shift); bool NavigateRight(const int shift); //--- Смещает график (1) к началу, (2) к концу исторических данных bool NavigateBegin(void); bool NavigateEnd(void); //--- Создаёт скриншот графика bool ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align); //--- Создаёт скриншот в разрешении (1) окна графика, (2) 800х600, (3) 750х562 пикселей bool ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER); bool ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER); bool ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER); //--- Сохраняет шаблон графика с текущими настройками bool SaveTemplate(const string filename=NULL); //--- Применяет к графику указанный шаблон bool ApplyTemplate(const string filename=NULL); //--- Преобразует координаты X и Y графика окна в значения время и цена int XYToTimePrice(const long x,const double y); //--- Возвращает (1) время, (2) цену из значений координат XY datetime TimeFromXY(void) const { return this.m_wnd_time_x; } double PriceFromXY(void) const { return this.m_wnd_price_y; } }; //+------------------------------------------------------------------+
В параметричестком конструкторе, в его списке инициализации инициализируем новые переменные-члены класса значениями по умолчанию:
//+------------------------------------------------------------------+ //| Параметрический конструктор | //+------------------------------------------------------------------+ CChartObj::CChartObj(const long chart_id) : m_wnd_time_x(0),m_wnd_price_y(0) { }
Рассмотрим реализацию новых методов.
Метод, возвращающий объект-окно по имени индикатора в нём:
//+------------------------------------------------------------------+ //| Возвращает объект-окно по имени индикатора в нём | //+------------------------------------------------------------------+ CChartWnd *CChartObj::GetWindowByIndicator(const string ind_name) const { int index=(this.m_program==PROGRAM_INDICATOR && ind_name==NULL ? ::ChartWindowFind() : ::ChartWindowFind(this.m_chart_id,ind_name)); return this.GetWindowByIndex(index); } //+------------------------------------------------------------------+
Если запущенная программа, работающая на основе данной библиотеки — индикатор, то для того, чтобы этот индикатор знал, в каком окне он запущен, требуется вызвать функцию ChartWindowFind() без параметров. Если же нам нужно найти окно другого индикатора, то в ChartWindowFind() нам нужно передать идентификатор графика, номер окна которого мы хотим найти по имени индикатора.
Поэтому здесь мы сначала проверяем тип программы, и если это индикатор и в качестве имени в метод передан NULL, то вызываем функцию ChartWindowFind() без параметров — это запрос от индикатора для поиска своего собственного окна.
В ином случае — вызываем ChartWindowFind(), в который передаём идентификатор графика, принадлежащий этому объекту-чарту, и короткое имя индикатора, переданное в метод, номер окна которого нам необходимо найти.
В итоге для возврата объекта-окна, в котором расположен указанный индикатор, мы используем метод, возвращающий объект-окно, принадлежащее данному объекту-чарту, по найденному номеру окна с индикатором.
Метод, сдвигающий график на указанное количество баров относительно указанной позиции графика:
//+------------------------------------------------------------------+ //| Сдвигает график на указанное количество баров | //| относительно указанной позиции графика | //+------------------------------------------------------------------+ bool CChartObj::Navigate(const int shift,const ENUM_CHART_POSITION position) { ::ResetLastError(); bool res=::ChartNavigate(m_chart_id,position,shift); if(!res) CMessage::ToLog(DFUN,::GetLastError(),true); return res; } //+------------------------------------------------------------------+
Метод просто вызывает функцию ChartNavigate() с переданными в метод параметрами для смещения — количество баров (shift) и позиция графика, относительно которой будет произведено смещение (position). При неудачном исполнении функции метод выводит в журнал сообщение об ошибке. Возвращается результат вызова функции ChartNavigate(). Перед вызовом метода, для правильной его работы, необходимо отключить объекту-чарту автопрокрутку к правому краю графика.
Метод, сдвигающий график влево на указанное количество баров:
//+------------------------------------------------------------------+ //| Сдвигает график влево на указанное количество баров | //+------------------------------------------------------------------+ bool CChartObj::NavigateLeft(const int shift) { this.SetAutoscrollOFF(); return this.Navigate(shift,CHART_CURRENT_POS); } //+------------------------------------------------------------------+
Здесь: сначала отключаем объекту-чарту автопрокрутку к правому краю графика и
возвращаем результат работы метода Navigate(), в который передаём значение смещения графика в барах, переданное в метод.
Смещаем график от текущей позиции.
Метод, сдвигающий график вправо на указанное количество баров:
//+------------------------------------------------------------------+ //| Сдвигает график вправо на указанное количество баров | //+------------------------------------------------------------------+ bool CChartObj::NavigateRight(const int shift) { this.SetAutoscrollOFF(); return this.Navigate(-shift,CHART_CURRENT_POS); } //+------------------------------------------------------------------+
Здесь: сначала отключаем объекту-чарту автопрокрутку к правому краю графика и
возвращаем результат работы метода Navigate(), в который передаём отрицательное значение смещения графика в барах, переданное в метод.
Смещаем график от текущей позиции.
Метод, смещающий график к началу исторических данных:
//+------------------------------------------------------------------+ //| Смещает график к началу исторических данных | //+------------------------------------------------------------------+ bool CChartObj::NavigateBegin(void) { this.SetAutoscrollOFF(); return this.Navigate(0,CHART_BEGIN); } //+------------------------------------------------------------------+
Здесь: сначала отключаем объекту-чарту автопрокрутку к правому краю графика и
возвращаем результат работы метода Navigate(), в который передаём нулевое значение смещения графика в барах.
Смещаем график к началу истории.
Метод, смещающий график к концу исторических данных (к текущему времени):
//+------------------------------------------------------------------+ //| Смещает график к концу исторических данных | //+------------------------------------------------------------------+ bool CChartObj::NavigateEnd(void) { this.SetAutoscrollOFF(); return this.Navigate(0,CHART_END); } //+------------------------------------------------------------------+
Здесь: сначала отключаем объекту-чарту автопрокрутку к правому краю графика и
возвращаем результат работы метода Navigate(), в который передаём нулевое значение смещения графика в барах.
Смещаем график к концу истории — к текущему времени.
Метод, создающий скриншот графика:
//+------------------------------------------------------------------+ //| Создаёт скриншот графика | //+------------------------------------------------------------------+ bool CChartObj::ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align) { ::ResetLastError(); if(!::ChartScreenShot(m_chart_id,filename,width,height,align)) { CMessage::ToLog(DFUN,::GetLastError(),true); return false; } return true; } //+------------------------------------------------------------------+
В метод передаются имя файла скриншота, ширина и высота получаемого изображения и выравнивание (ENUM_ALIGN_MODE). Выравнивание необходимо при создании вертикальных снимков — когда высота изображения больше его ширины. В этом случае выравнивание указывает нам к какому краю графика будет "прижато" изображение.
Здесь мы просто делаем скриншот с параметрами, переданными в метод, при помощи функции ChartScreenShot().
При успешном создании скриншота экрана возвращаем true.
Если же скриншот создать не удалось, то выводим об этом сообщение в журнал и возвращаем false.
Дополнительными методами для создания скриншотов у нас будут три метода, создающие скриншоты заданных размеров:
- скриншот по размеру окна графика,
- скриншот размером 800х600,
- скриншот размером 750х562.
Второй и третий размеры были выбраны неспроста — такие размеры часто требуются для публикации изображений на форуме ресурса MQL5.com, в статьях и в описании продуктов маркета. А скриншот по размеру окна графика позволит прямо в терминале настроить внешний вид и размеры окна и сделать скриншот именно такого размера.
Метод, создающий скриншот графика в разрешении окна графика (включая шкалы цены и времени при их наличии):
//+------------------------------------------------------------------+ //| Создаёт скриншот графика в разрешении окна графика | //+------------------------------------------------------------------+ bool CChartObj::ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER) { //--- Создаём имя файла или используем переданное в метод string name= (filename==NULL || filename=="" ? SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT : this.FileNameWithExtention(filename) ); //--- Получаем окно графика, имеющего наибольший из всех окон номер CChartWnd *wnd=this.GetWindowByNum(this.m_list_wnd.Total()-1); if(wnd==NULL) { ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ),string(this.m_list_wnd.Total()-1)); return false; } //--- Рассчитываем ширину и высоту скриншота с учётом размеров шкал цены и времени int width=this.WidthInPixels()+(IsShowPriceScale() ? 56 : 0); int height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale() ? 15 : 0); //--- Создаём скриншот и возвращаем результат работы метода ScreenShot() bool res=this.ScreenShot(name,width,height,align); if(res) ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")"); return res; } //+------------------------------------------------------------------+
В листинге метода подробно прокомментирована его логика. Вкратце: при создании имени, если в метод передан NULL, то создаём имя файла, состоящее из пути к файлам скриншотов библиотеки, наименования программы и расширения, заданного по умолчанию в файле Defines.mqh. Если же имя файла передано в метод не пустым, то при помощи метода FileNameWithExtention(), который рассмотрим далее, проверяем наличие расширения в имени файла (для скриншотов может быть только три расширения: .gif, .png и .bmp) и добавляем расширение к имени файла при его отсутствии.
Чтобы рассчитать размеры скриншота с учётом всех окон, принадлежащих графику, нам нужно найти окно, которое имеет наибольший номер из всех (0 — главное окно графика, 1, 2, 3, N — все открытые на нём окна сверху-вниз). Т. е. самое нижнее окно будет иметь наибольший номер. Зная дистанцию от верхнего края главного окна графика до верхнего края самого нижнего окна, открытого на графике, мы получим точку отсчёта, к которой нужно прибавить высоту этого окна. Таким образом мы получим полную высоту всего графика. И нам останется лишь проверить наличие на графике шкалы времени. Если шкала есть, то прибавляем к уже рассчитанной высоте 15 пикселей (размер подбирался опытным путём), если шкалы нет, то ничего не прибавляем. Таким образом мы находим высоту будущего скриншота.
С шириной скриншота немного проще — получаем ширину объекта-чарта и прибавляем к ней 56 пикселей в случае, если у графика есть шкала цены. При отсутствии шкалы ничего не прибавляем. Размер шкалы цены тоже подбирался опытным путём.
Вполне вероятно, что на разных мониторах с разным разрешением экрана размеры шкал цены и времени могут отличаться — у меня не было возможности поэкспериментировать с различными мониторами и их разрешением. Впрочем, размеры шкал, добавленные к размеру скриншота, не вносят сильной погрешности во внешний вид изображения — только сам размер файла по высоте и ширине и немного отличающиеся на вид расположения графика цены в окне терминала и на скриншоте.
Метод, создающий скриншот графика в разрешении 800x600 пикселей:
//+------------------------------------------------------------------+ //| Создаёт скриншот графика в разрешении 800x600 пикселей | //+------------------------------------------------------------------+ bool CChartObj::ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER) { string name= (filename==NULL || filename=="" ? SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT : this.FileNameWithExtention(filename) ); int width=800; int height=600; bool res=this.ScreenShot(name,width,height,align); if(res) ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")"); return res; } //+------------------------------------------------------------------+
Здесь всё проще, чем в вышерассмотренном методе. Имя файла создаётся точно так же, как и в прошлом методе, а размеры файла здесь заданы жёстко — передаём их в метод создания скриншота ScreenShot() и возвращаем результат его работы.
Метод, создающий скриншот графика в разрешении 750x562 пикселя:
//+------------------------------------------------------------------+ //| Создаёт скриншот графика в разрешении 750x562 пикселей | //+------------------------------------------------------------------+ bool CChartObj::ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER) { string name= (filename==NULL || filename=="" ? SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT : this.FileNameWithExtention(filename) ); int width=750; int height=562; bool res=this.ScreenShot(name,width,height,align); if(res) ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")"); return res; } //+------------------------------------------------------------------+
Метод аналогичен методу создания скриншота 800х600 пикселей за исключением размеров изображения.
Любой график со всеми его настройками, прикреплёнными к нему индикаторами и советником можно сохранить в качестве шаблона для последующего его применения к другим графикам. У нас будут два метода — для сохранения шаблона графика и для применения указанного шаблона к графику, описываемому объектом-чартом.
Метод, сохраняющий шаблон графика с текущими настройками:
//+------------------------------------------------------------------+ //| Сохраняет шаблон графика с текущими настройками | //+------------------------------------------------------------------+ bool CChartObj::SaveTemplate(const string filename=NULL) { ::ResetLastError(); string name= (filename==NULL || filename=="" ? TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) : filename ); if(!::ChartSaveTemplate(this.m_chart_id,name)) { CMessage::ToLog(DFUN,::GetLastError(),true); return false; } ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_SAVED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe())); return true; } //+------------------------------------------------------------------+
В метод передаётся имя файла, под которым необходимо сохранить шаблон. Если имя пустое (по умолчанию), то будет использовано имя, состоящее из пути к файлу, определённого ранее в файле Defines.mqh, и имени программы.
При успешном сохранении файла шаблона, об этом в журнал выводится запись с указанием того, шаблон какого графика был сохранён (символ и таймфрейм) и возвращается true. Если же шаблон сохранить не удалось, то об этом тоже выводится сообщение в журнал и метод возвращает false.
Метод, применяющий указанный шаблон к графику:
//+------------------------------------------------------------------+ //| Применяет к графику указанный шаблон | //+------------------------------------------------------------------+ bool CChartObj::ApplyTemplate(const string filename=NULL) { ::ResetLastError(); string name= (filename==NULL || filename=="" ? TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) : filename ); if(!::ChartApplyTemplate(this.m_chart_id,name)) { CMessage::ToLog(DFUN,::GetLastError(),true); return false; } ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_APPLIED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe())); return true; } //+------------------------------------------------------------------+
Метод устроен аналогично методу, сохраняющему шаблон графика. Так же используется либо переданное в метод, либо автоматически создаваемое имя файла шаблона. Далее шаблон применяется к графику и выводится запись в журнал о результате этой операции.
Следует помнить, что если к текущему графику с работающим советником, вызвавшим этот метод, применить шаблон, в котором прописан другой советник, то текущий советник выгрузится из памяти и больше работать не будет — он будет заменён новым советником из шаблона. Данный метод не проверяет возможность такой коллизии, поэтому всегда нужно самостоятельно следить за своими шаблонами и проверять вероятность подмены текущего советника на тот, который возможно будет запущен из применённого к графику шаблона.
Метод, преобразующий координаты X и Y графика окна в значения время и цена:
//+------------------------------------------------------------------+ //|Преобразует координаты X и Y графика окна в значения время и цена| //+------------------------------------------------------------------+ int CChartObj::XYToTimePrice(const long x,const double y) { int sub_window=WRONG_VALUE; ::ResetLastError(); if(!::ChartXYToTimePrice(this.m_chart_id,(int)x,(int)y,sub_window,this.m_wnd_time_x,this.m_wnd_price_y)) { //CMessage::ToLog(DFUN,::GetLastError(),true); return WRONG_VALUE; } return sub_window; } //+------------------------------------------------------------------+
Функция ChartXYToTimePrice() преобразует координаты X и Y графика в значения время и цена. При этом она записывает в переменную sub_window, передаваемую в неё по ссылке, номер подокна, в котором находятся координаты X и Y графика, для которых необходимо вернуть время и цену.
На основании этого данный метод возвращает номер подокна графика: 0 — если координаты находятся в главном окне графика, 1,2,3 и т. д. — если координаты попадают в соответствующее подокно графика, и -1 — если координаты вычислить не удалось. После вызова данного метода, и если значение, которое он вернул, не равно -1, то можно получить время и цену при помощи методов TimeFromXY() и PriceFromXY(), которые просто возвращают значения переменных, в которые будут записаны полученные время и цена функцией ChartXYToTimePrice().
Метод, добавляющий расширение файлу скриншота при его отсутствии:
//+------------------------------------------------------------------+ //| Добавляет расширение файлу скриншота при его отсутствии | //+------------------------------------------------------------------+ string CChartObj::FileNameWithExtention(const string filename) { if(::StringFind(filename,FILE_EXT_GIF)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_PNG)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_BMP)>WRONG_VALUE) return filename; return filename+SCREENSHOT_FILE_EXT; } //+------------------------------------------------------------------+
В метод передаётся проверяемая строка, в которой нужно найти расширение файла скриншота. Так как форматы файлов для скриншотов строго определены, и ими могут быть только три типа файлов — GIF, PNG и BMP, то, если в переданной в метод строке есть вхождение хотя бы одной подстроки с таким расширением (т. е. — расширение уже задано), то метод возвращает переданную в него строку без изменений. Иначе — к строке добавляется расширение имени файла, заданное по умолчанию в файле Defines.mqh, и это файл формата PNG (расширение .png), и изменённая строка возвращается.
О проблемах, выявленных при добавлении нового окна на график:
при подробном тестировании было выявлено, что в момент, когда мы добавляем на график новый индикатор в окне, появляется его окно (но мы ещё не нажали кнопку "ОК" или "Отмена"), и это окно сразу же видно в терминале как уже существующее. На данный момент библиотека его увидит и добавит в свой список окно объекта-чарта, при этом индикатора в этом окне не будет. Но если мы нажмём отмену в окне добавления нового оконного индикатора, то этого окна уже не будет в списке окон графика клиентского терминала. Библиотека удалит это окно из списка на следующей проверке.
Во избежание таких ненужных действий и случайных обращений к пустому окну индикатора, не существующему в клиентском терминале, нам нужно при появлении нового окна прежде, чем добавить его в список, убедиться в том, что в этом окне есть индикатор. Если индикатор есть — добавить окно, если его нет, то такое окно всего лишь открыто, но не добавлено на график — его необходимо пропустить.
Таким образом, метод для создания списка окон графика будет доработан:
//+------------------------------------------------------------------+ //| Создаёт список окон графика | //+------------------------------------------------------------------+ void CChartObj::CreateWindowsList(void) { //--- Очищаем список окон графика this.m_list_wnd.Clear(); //--- Получаем общее количество окон графика из окружения int total=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL); //--- В цикле по общему количеству окон for(int i=0;i<total;i++) { //--- Создаём новый объект-окно чарта CChartWnd *wnd=new CChartWnd(this.m_chart_id,i); if(wnd==NULL) continue; //--- Если номер окна больше 0 (не главное окно графика) и в нём ещё нет индикатора, //--- то удаляем вновь созданный объект-окно чарта и идём на следующую итерацию цикла if(wnd.WindowNum()!=0 && wnd.IndicatorsTotal()==0) { delete wnd; continue; } //--- Если объект в список не был добавлен - удаляем этот объект this.m_list_wnd.Sort(); if(!this.m_list_wnd.Add(wnd)) delete wnd; } //--- Если количество объектов в списке соответствует количеству окон на графике, //--- то записываем это значение в свойство объекта-чарта //--- Если же количество объектов в списке не соответствует количеству окон на графике, //--- то в свойство объекта-чарта записываем значение количества объектов в списке. int value=int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total()); this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value); } //+------------------------------------------------------------------+
Логика метода подробно расписана в его листинге. Вся проверка на ненужность добавления в список ещё не созданного окна на графике находится в отмеченном блоке кода. Т. е. при добавлении только оконного индикатора у нас может быть ещё не созданное окно. Поэтому мы не анализируем окно с индексом 0 — это главное окно графика, и оно уже есть точно, и в него можно только добавлять новые индикаторы. Если же у нас найдено новое окно, но в нём ещё нет индикатора, чего быть не может при уже добавленном на график окне индикатора, то это и есть то окно, где мы ещё не нажали кнопку "ОК" для его добавления на график. Такое окно пропускаем, чтобы не добавлять его в список окон.
По окончании всего цикла мы должны записать в свойство объекта-чарта количество его окон. Здесь мы проверяем, что все окна добавлены в список успешно — если их реальное количество равно их количеству в списке — значит, все окна добавлены — записываем в свойство объекта количество окон на графике. Если же эти величины не равны, то в свойство мы будем записывать то количество, которое есть в списке (и оно не равно реальному), чтобы на следующей проверке мы опять попали на неравенство и добавили в список уже окончательно созданное окно, либо, если при добавлении нового окна мы нажали "Отмена", то количество окон на графике и в списке сравняются, и мы не будем обрабатывать это изменение — его и нет.
Доработка классов библиотеки завершена.
Автообновление класса-коллекции объектов чартов и окон
Теперь нам нужно сделать так, чтобы при любом изменении количества открытых графиков, количества окон на графиках и количества индикаторов в этих окнах, библиотека могла сама автоматически обновить все эти данные — чтобы нам самим не отвлекаться на это и при этом всегда иметь актуальные данные.
В файле \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh класса объекта чарта дополним метод Refresh() так, чтобы мы могли проверять не только изменение количества открытых окон на графике (в объекте-чарте), но и контролировать количество индикаторов в уже открытых окнах (в одно окно можно разместить несколько индикаторов).
Метод, обновляющий объект-чарт и список его окон:
//+------------------------------------------------------------------+ //| Обновляет объект-чарт и список его окон | //+------------------------------------------------------------------+ void CChartObj::Refresh(void) { for(int i=0;i<m_list_wnd.Total();i++) { CChartWnd *wnd=m_list_wnd.At(i); if(wnd==NULL) continue; wnd.Refresh(); } int change=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL)-this.WindowsTotal(); if(change==0) return; this.CreateWindowsList(); } //+------------------------------------------------------------------+
Здесь: прежде, чем проверять изменение количества открытых окон в объекте-чарте, сначала пройдёмся по списку всех окон объекта и вызовем метод обновления для каждого последующего окна в цикле. Метод Refresh() объекта-окна графика проверяет изменение количества индикаторов, размещённых в нём, и при регистрации изменений пересоздаёт их список.
В файле класса-коллекции объектов-чартов \MQL5\Include\DoEasy\Collections\ChartObjCollection.mqh исправим логическую ошибку, мешающую обновлению объектов-чартов в списке-коллекции и, соответственно — и обновлению их окон и индикаторов в них.
Ранее блок обновления объектов-чартов был расположен ниже проверки изменения количества открытых графиков, что не давало дойти до него в случае, если никаких изменений в количестве открытых графиков не было:
//+------------------------------------------------------------------+ //| Обновляет список-коллекцию объектов-чартов | //+------------------------------------------------------------------+ void CChartObjCollection::Refresh(void) { //--- Получаем количество открытых графиков в терминале и int charts_total=this.ChartsTotal(); //--- рассчитываем разницу между количеством открытых графиков в терминале //--- и объектов-чартов в списке-коллекции, эти значения выводим в комментарии на графике int change=charts_total-this.m_list.Total(); Comment(DFUN,", list total=",DataTotal(),", charts total=",charts_total,", change=",change); //--- Если нет изменений - уходим if(change==0) return; //--- Если добавлен график в терминале if(change>0) { //--- Находим недостающий объект-чарт, создаём и добавляем его в список-коллекцию this.FindAndCreateMissingChartObj(); //--- Получаем текущий график и возвращаемся к нему т.к. //--- добавление нового графика переключает фокус на него CChartObj *chart=this.GetChart(GetMainChartID()); if(chart!=NULL) chart.SetBringToTopON(true); } //--- Если удалён график в терминале else if(change<0) { //--- Находим лишний объект-чарт в списке-коллекции и удаляем его из списка this.FindAndDeleteExcessChartObj(); } //--- В цикле по количеству объектов-чартов в списке for(int i=0;i<this.m_list.Total();i++) { //--- получаем очередной объект-чарт и CChartObj *chart=this.m_list.At(i); if(chart==NULL) continue; //--- обновляем его chart.Refresh(); } } //+------------------------------------------------------------------+
Решение банально — достаточно перенести блок кода обновления объектов-чартов выше — перед проверкой изменения количества открытых графиков в клиентском терминале:
//+------------------------------------------------------------------+ //| Обновляет список-коллекцию объектов-чартов | //+------------------------------------------------------------------+ void CChartObjCollection::Refresh(void) { //--- В цикле по количеству объектов-чартов в списке for(int i=0;i<this.m_list.Total();i++) { //--- получаем очередной объект-чарт и CChartObj *chart=this.m_list.At(i); if(chart==NULL) continue; //--- обновляем его chart.Refresh(); } //--- Получаем количество открытых графиков в терминале и int charts_total=this.ChartsTotal(); //--- рассчитываем разницу между количеством открытых графиков в терминале //--- и объектов-чартов в списке-коллекции, эти значения выводим в комментарии на графике int change=charts_total-this.m_list.Total(); //--- Если нет изменений - уходим if(change==0) return; //--- Если добавлен график в терминале if(change>0) { //--- Находим недостающий объект-чарт, создаём и добавляем его в список-коллекцию this.FindAndCreateMissingChartObj(); //--- Получаем текущий график и возвращаемся к нему т.к. //--- добавление нового графика переключает фокус на него CChartObj *chart=this.GetChart(GetMainChartID()); if(chart!=NULL) chart.SetBringToTopON(true); } //--- Если удалён график в терминале else if(change<0) { //--- Находим лишний объект-чарт в списке-коллекции и удаляем его из списка this.FindAndDeleteExcessChartObj(); } } //+------------------------------------------------------------------+
Теперь, прежде чем проверять изменение количества открытых графиков (и выходить из метода в случае, если количество не изменилось), мы сначала проходимся в цикле по всем объектам-чартам в списке коллекции и проверяем изменение их объектов-окон в их методе Refresh(), где в свою очередь вызывается свой метод Refresh() для проверки количества индикаторов в окне. Таким образом мы сначала делаем полную проверку всех возможных изменений количества индикаторов в окнах, окон индикаторов в чартах, а уже потом проверяем изменение количества открытых графиков.
Добавим в класс-коллекцию объектов-чартов методы для открытия нового графика и закрытия уже существующего.
В публичной секции класса объявим два новых метода:
//--- Обновляет (1) список-коллекцию объектов-чартов, (2) указанный объект-чарт void Refresh(void); void Refresh(const long chart_id); //--- (1) Открывает новый график с указанным символом и периодом, (2) закрывает указанный график bool Open(const string symbol,const ENUM_TIMEFRAMES timeframe); bool Close(const long chart_id); }; //+------------------------------------------------------------------+
За пределами тела класса напишем их реализацию.
Метод, открывающий новый график с указанным символом и периодом:
//+------------------------------------------------------------------+ //| Открывает новый график с указанным символом и периодом | //+------------------------------------------------------------------+ bool CChartObjCollection::Open(const string symbol,const ENUM_TIMEFRAMES timeframe) { if(this.m_list.Total()==CHARTS_MAX) { ::Print(CMessage::Text(MSG_CHART_COLLECTION_ERR_CHARTS_MAX)," (",(string)CHARTS_MAX,")"); return false; } ::ResetLastError(); long chart_id=::ChartOpen(symbol,timeframe); if(chart_id==0) CMessage::ToLog(::GetLastError(),true); return(chart_id>0); } //+------------------------------------------------------------------+
Здесь: если количество объектов-чартов в коллекции достигло предельного (CHARTS_MAX), то попытка открытия нового графика будет бесполезной — сообщаем об этом и возвращаем false. Далее, если мы всё ещё можем открыть новый график, вызываем функцию ChartOpen() с указанными параметрами открываемого графика, и в случае ошибки сообщаем об этом в журнал. Возвращаем флаг того, что функции открытия нового графика вернула не ноль.
Метод, закрывающий указанный график:
//+------------------------------------------------------------------+ //| Закрывает указанный график | //+------------------------------------------------------------------+ bool CChartObjCollection::Close(const long chart_id) { ::ResetLastError(); bool res=::ChartClose(chart_id); if(!res) CMessage::ToLog(DFUN,::GetLastError(),true); return res; } //+------------------------------------------------------------------+
Здесь: если попытка закрытия указанного по идентификатору графика не была успешной — выводим об этом запись в журнал.
Метод возвращает результат работы функции ChartClose().
В файле \MQL5\Include\DoEasy\Engine.mqh главного объекта библиотеки CEngine добавим методы для управления коллекцией чартов.
Два метода, возвращающие списки объектов-чартов по символу и таймфрейму
//--- Возвращает список объектов-чартов по (1) символу, (2) таймфрейму CArrayObj *ChartGetChartsList(const string symbol) { return this.m_charts.GetChartsList(symbol); } CArrayObj *ChartGetChartsList(const ENUM_TIMEFRAMES timeframe) { return this.m_charts.GetChartsList(timeframe); }
переименуем для соответствия аналогичным методам других классов и переместим чуть выше в листинге:
//--- Создаёт коллекцию чартов bool ChartCreateCollection(void) { return this.m_charts.CreateCollection(); } //--- Возвращает (1) коллекцию чартов, (2) список чартов из коллекции чартов CChartObjCollection *GetChartObjCollection(void) { return &this.m_charts; } CArrayObj *GetListCharts(void) { return this.m_charts.GetList(); } //--- Возвращает список объектов-чартов по (1) символу, (2) таймфрейму CArrayObj *GetListCharts(const string symbol) { return this.m_charts.GetChartsList(symbol); } CArrayObj *GetListCharts(const ENUM_TIMEFRAMES timeframe) { return this.m_charts.GetChartsList(timeframe); }
Добавим метод, возвращающий объект-чарт последнего открытого графика , метод, возвращающий количество объектов-чартов в списке коллекции,
и два метода для открытия и закрытия указанного графика:
//--- Создаёт коллекцию чартов bool ChartCreateCollection(void) { return this.m_charts.CreateCollection(); } //--- Возвращает (1) коллекцию чартов, (2) список чартов из коллекции чартов CChartObjCollection *GetChartObjCollection(void) { return &this.m_charts; } CArrayObj *GetListCharts(void) { return this.m_charts.GetList(); } //--- Возвращает список объектов-чартов по (1) символу, (2) таймфрейму CArrayObj *GetListCharts(const string symbol) { return this.m_charts.GetChartsList(symbol); } CArrayObj *GetListCharts(const ENUM_TIMEFRAMES timeframe) { return this.m_charts.GetChartsList(timeframe); } //--- Возвращает (1) указанный объект-чарт, (2) объект-чарт с программой, (3) объект-чарт последнего открытого графика CChartObj *ChartGetChartObj(const long chart_id) { return this.m_charts.GetChart(chart_id); } CChartObj *ChartGetMainChart(void) { return this.m_charts.GetChart(this.m_charts.GetMainChartID());} CChartObj *ChartGetLastOpenedChart(void) { return this.m_charts.GetChart(this.GetListCharts().Total()-1);} //--- Возвращает количество чартов в списке-коллекции int ChartsTotal(void) { return this.m_charts.DataTotal(); } //--- Обновляет (1) указанный по идентификатору чарт, (2) всех чарты void ChartRefresh(const long chart_id) { this.m_charts.Refresh(chart_id); } void ChartsRefreshAll(void) { this.m_charts.Refresh(); } //--- (1) Открывает, (2) закрывает указанный график bool ChartOpen(const string symbol,const ENUM_TIMEFRAMES timeframe) { return this.m_charts.Open(symbol,timeframe); } bool ChartClose(const long chart_id) { return this.m_charts.Close(chart_id); } //--- Возвращает (1) коллекцию буферов, (2) список буферов из коллекции
Метод ChartGetLastOpenedChart() просто возвращает указатель на самый последний объект, находящийся в списке коллекции объектов-чартов,
а метод ChartsTotal() возвращает размер списка коллекции объектов-чартов.
Методы ChartOpen() и ChartClose() возвращают результат работы методов Open() и Close() класса-коллекции чартов соответственно.
Это все изменения и доработки, запланированные нами на сегодня.
Тестирование
Для тестирования возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part70\ под новым именем TestDoEasyPart70.mq5.
Что мы сделаем. К панельке советника добавим новые кнопки с такими значками:
- "<" и ">" — кнопка для смещения графика на один бар влево и вправо соответственно;
- "<<" и ">>" — кнопка для смещения графика на десять баров влево и вправо соответственно;
- "|<" и ">|" — кнопка для установки графика в начало истории и в конец истории соответственно;
- "N" и "X" — кнопка для открытия нового и для закрытия последнего открытого графика символа соответственно;
- "[O]" — кнопка для создания скриншота текущего графика с советником.
Логика тестирования нового функционала будет такой:
- При нажатии кнопок смещения графика советник будет смещать график влево-вправо на один бар и на 10 баров соответственно.
- При нажатии кнопок установки графика в начало и в конец истории график будет становиться соответственно.
- При нажатии кнопки открытия графика советник будет поочерёдно открывать графики символов, находящихся в списке коллекции объектов-чартов по порядку их нахождения в списке, а не в окне обзора рынка (в окне обзора рынка сортировка может быть иной).
При этом, для проверки сохранения и применения шаблона графика советник будет сохранять шаблон последнего открытого графика и при открытии нового — применять к нему сохранённый шаблон. Т.е., если мы прежде, чем нажать на кнопку открытия нового графика, откроем новый график вручную, например, GBPUSD и настроим его внешний вид, отличный от текущего, то все последующие графики будут открываться с таким внешним видом. Вернее — к ним после открытия будет применяться шаблон с внешним видом настроенного заранее графика.
Т.е., для этого теста нам необходимо будет сначала открыть новый график символа GBPUSD и настроить его внешний вид. Ко всем остальным, открываемым уже советником графикам, будет применяться сохранённый шаблон от настроенного вручную графика символа GBPUSD. - При нажатии кнопки закрытия графика советник будет закрывать самый последний из открытых графиков.
- При последовательном нажатии кнопки создания скриншота советник будет создавать скриншоты графика:
последовательно по кругу в таком разрешении: 800х600 --> 750x562 --> Текущий размер графика.
Итак, в обработчике OnInit() советника впишем включение разрешения на отслеживание событий мыши для текущего графика:
//--- Проверка воспроизведения стандартного звука по макроподстановке и пользовательского звука по описанию engine.PlaySoundByDescription(SND_OK); //--- Ждём 600 миллисекунд engine.Pause(600); engine.PlaySoundByDescription(TextByLanguage("Звук упавшей монетки 2","The sound of a falling coin 2")); //--- Проверка расчёта координат курсора в окнах графика. //--- Установим текущему графику разрешение на отслеживание событий перемещения мыши engine.ChartGetMainChart().SetEventMouseMoveON(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Это необходимо чтобы программа могла получать сообщения о событиях перемещения и нажатия кнопок мышки (CHARTEVENT_MOUSE_MOVE).
В обработчике OnChartEvent() нам нужно при получении события перемещения мыши получить координаты курсора на графике в пикселях, преобразовать их при помощи созданных методов библиотеки в значения времени, цены и номера окна, в котором курсор находится, а затем только что полученные время и цену преобразовать обратно в координаты курсора на графике и вывести все эти значения в комментарии на графике.
Напишем такую обработку события перемещения курсора:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Если работа в тестере - выход if(MQLInfoInteger(MQL_TESTER)) return; //--- Обработка событий мыши if(id==CHARTEVENT_OBJECT_CLICK) { //--- Обработка нажатий кнопок в панели if(StringFind(sparam,"BUTT_")>0) PressButtonEvents(sparam); } //--- Обработка событий библиотеки DoEasy if(id>CHARTEVENT_CUSTOM-1) { OnDoEasyEvent(id,lparam,dparam,sparam); } //--- Проверка ChartXYToTimePrice() if(id==CHARTEVENT_MOUSE_MOVE) { //--- Получим объект-чарт текущего (главного) графика программы CChartObj *chart=engine.ChartGetMainChart(); if(chart==NULL) return; //--- Получим номер подокна, в котором находится курсор int wnd_num=chart.XYToTimePrice(lparam,dparam); if(wnd_num==WRONG_VALUE) return; //--- Получим рассчитанные время и цену расположения курсора datetime time=chart.TimeFromXY(); double price=chart.PriceFromXY(); //--- Получим объект-окно чарта, в котором расположен курсор, по номеру подокна CChartWnd *wnd=chart.GetWindowByNum(wnd_num); if(wnd==NULL) return; //--- Если координаты X и Y рассчитаны по времени и цене (делаем обратное предыдущему преобразование), if(wnd.TimePriceToXY(time,price)) { //--- то выведем в комментарии рассчитанные по X и Y курсора время, цену и номер окна, //--- а также преобразованные обратно из времени и цены координаты X и Y курсора Comment ( DFUN,"time: ",TimeToString(time),", price: ",DoubleToString(price,Digits()), ", win num: ",(string)wnd_num,": x: ",(string)wnd.XFromTimePrice(), ", y: ",(string)wnd.YFromTimePrice()," (",(string)wnd.YFromTimePriceRelative(),")") ; } } } //+------------------------------------------------------------------+
Логика обработки события перемещения мыши подробно расписана в коде OnChartEvent() советника.
В функции советника CreateButtons() добавим код для создания новых кнопок панели:
//+------------------------------------------------------------------+ //| Создаёт панель кнопок | //+------------------------------------------------------------------+ bool CreateButtons(const int shift_x=20,const int shift_y=0) { int h=18,w=82,offset=2,wpt=14; int cx=offset+shift_x+wpt*2+2,cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1; int x=cx,y=cy; int shift=0; for(int i=0;i<TOTAL_BUTT;i++) { x=x+(i==7 ? w+2 : 0); if(i==TOTAL_BUTT-6) x=cx; y=(cy-(i-(i>6 ? 7 : 0))*(h+1)); if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-6 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text); return false; } } h=18; offset=2; cx=offset+shift_x; cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1; x=cx; y=cy; shift=0; for(int i=0;i<18;i++) { y=(cy-(i-(i>6 ? 7 : 0))*(h+1)); if(!ButtonCreate(butt_data[i].name+"_PRICE",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5 : x),y,wpt,h,"P",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"P\""); return false; } if(!ButtonCreate(butt_data[i].name+"_TIME",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5+wpt+1 : x+wpt+1),y,wpt,h,"T",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"T\""); return false; } } //--- Кнопки "Влево", "Вправо" int xbn=x+wpt*2+w*2+5; int ybn=y+h*3+3; if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT1",xbn,ybn,wpt,h,"<",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT1"); return false; } if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT1",xbn+wpt+1,ybn,wpt,h,">",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT1"); return false; } //--- Кнопки "Влево 10", "Вправо 10" ybn=y+h*2+2; if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT10",xbn,ybn,wpt,h,"<<",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT10"); return false; } if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT10",xbn+wpt+1,ybn,wpt,h,">>",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT10"); return false; } //--- Кнопки "Home", "End" ybn=y+h+1; if(!ButtonCreate(prefix+"BUTT_NAVIGATE_HOME",xbn,ybn,wpt,h,"|<",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_HOME"); return false; } if(!ButtonCreate(prefix+"BUTT_NAVIGATE_END",xbn+wpt+1,ybn,wpt,h,">|",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_END"); return false; } //--- Кнопки "Open", "Close" ybn=y; if(!ButtonCreate(prefix+"BUTT_CHART_OPEN",xbn,ybn,wpt,h,"N",clrBlue)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_OPEN"); return false; } if(!ButtonCreate(prefix+"BUTT_CHART_CLOSE",xbn+wpt+1,ybn,wpt,h,"X",clrRed)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_CLOSE"); return false; } //--- Кнопка ScreenShot ybn=y-h-1; if(!ButtonCreate(prefix+"BUTT_CHART_SCREENSHOT",xbn,ybn,wpt*2+offset,h,"[O]",clrBlue)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_SCREENSHOT"); return false; } ChartRedraw(0); return true; } //+------------------------------------------------------------------+
Здесь просто: для каждой новой кнопки рассчитываются её координаты и кнопка создаётся при помощи функции ButtonCreate(), в которую передаются имя создаваемого графического объекта, его координаты, размеры по ширине и высоте, надпись и цвет. Если кнопку создать не удалось, то об этом сообщается алертом и возвращается false. В обработчике OnInit(), если функция создания панели вернула false, то осуществляется выход с кодом возврата INIT_FAILED.
В функции обработки нажатий кнопок PressButtonEvents() впишем обработку нажатий на новые кнопки:
//+------------------------------------------------------------------+ //| Обработка нажатий кнопок | //+------------------------------------------------------------------+ void PressButtonEvents(const string button_name) { bool comp_magic=true; // Временная переменная для выбора использования составного магика со случайными идентификаторами групп string comment=""; //--- Преобразуем имя кнопки в её строковый идентификатор string button=StringSubstr(button_name,StringLen(prefix)); //--- Случайные номера групп 1 и 2 в диапазоне 0 - 15 group1=(uchar)Rand(); group2=(uchar)Rand(); uint magic=(comp_magic ? engine.SetCompositeMagicNumber(magic_number,group1,group2) : magic_number); //--- Если кнопка в нажатом состоянии if(ButtonState(button_name)) { //--- Если нажата кнопка смещения графика влево на 1 бар if(button=="BUTT_NAVIGATE_LEFT1") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateLeft(1); } //--- Если нажата кнопка смещения графика вправо на 1 бар if(button=="BUTT_NAVIGATE_RIGHT1") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateRight(1); } //--- Если нажата кнопка смещения графика влево на 10 баров if(button=="BUTT_NAVIGATE_LEFT10") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateLeft(10); } //--- Если нажата кнопка смещения графика вправо на 10 баров if(button=="BUTT_NAVIGATE_RIGHT10") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateRight(10); } //--- Если нажата кнопка смещения графика в начало истории if(button=="BUTT_NAVIGATE_HOME") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateBegin(); } //--- Если нажата кнопка смещения графика в конец истории if(button=="BUTT_NAVIGATE_END") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateEnd(); } //--- Если нажата кнопка открытия нового графика if(button=="BUTT_CHART_OPEN") { int total_charts=engine.ChartsTotal(); static int first_index=total_charts; string name=SymbolName(total_charts-first_index,true); if(engine.ChartOpen(name,PERIOD_CURRENT)) { engine.ChartsRefreshAll(); CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.SetBringToTopON(true); } //--- Этот блок кода нужен только для данного теста, и только при условии наличия открытого чарта GBPUSD //--- При этом, чарт GBPUSD должен отличаться настройками от чартов, открываемых по умолчанию CArrayObj *list_gbpusd=engine.GetListCharts("GBPUSD"); if(list_gbpusd!=NULL && list_gbpusd.Total()>0) { CChartObj *chart=list_gbpusd.At(0); if(chart.SaveTemplate()) { chart=engine.ChartGetLastOpenedChart(); if(chart!=NULL) chart.ApplyTemplate(); } } //--- Конец тестового блока кода } //--- Если нажата кнопка закрытия последнего графика if(button=="BUTT_CHART_CLOSE") { CArrayObj *list_charts=engine.GetListCharts(); if(list_charts!=NULL) { list_charts.Sort(SORT_BY_CHART_ID); CChartObj *chart=list_charts.At(list_charts.Total()-1); if(chart!=NULL && !chart.IsMainChart()) engine.ChartClose(chart.ID()); } } //--- Если нажата кнопка ScreenShot if(button=="BUTT_CHART_SCREENSHOT") { static int num=0; if(++num>3) num=1; CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) { switch(num) { case 1 : chart.ScreenShot800x600(); break; case 2 : chart.ScreenShot750x562(); break; default: chart.ScreenShotWndSize(); break; } } } //--- Если нажата кнопка BUTT_BUY: Открыть позицию Buy if(button==EnumToString(BUTT_BUY)) { ... ... ... ... ... ... ...
Код функции приведён не полностью, а только добавленные изменения.
Здесь мы просто обрабатываем нажатие каждой новой кнопки. Логика простая, и описывать её не буду — оставлю это для самостоятельного разбора, тем более что этот обработчик как раз служит примером того, как можно работать с методами библиотеки в своих программах. Все возникшие вопросы по коду всегда можно обсудить в комментариях к статье.
Это все доработки, которые нам необходимо было сделать в новом тестовом советнике.
Скомпилируем советник, и запустим его на графике любого символа с настройками "Использовать текущий символ" и "Использовать текущий таймфрейм":
Перед запуском советника обязательно откройте новый график символа GBPUSD и настройте его внешний вид иначе, чем у графиков, открываемых по умолчанию при использовании шаблона default.tpl, например, так (график GBPUSD был открыт заранее):
Теперь можно протестировать новый функционал библиотеки нажатием кнопок панели:
Каждый раз при открытии нового графика, советник сохранял шаблон ранее настроенного нами графика символа GBPUSD и тут же применял его к каждому вновь открытому графику, о чём делал записи в журнале:
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: USDCHF H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: GBPUSD H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: EURUSD H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: USDRUB H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: EURJPY H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: EURGBP H1 CChartObjCollection::Close: Wrong chart ID (4101)
При закрытии открытых графиков появилась одна ошибка. Дело в том, что библиотека обновляет раз в полсекунды состояние открытых графиков.
Это задано в файле Defines.mqh:
//--- Параметры таймера коллекции чартов #define COLLECTION_CHARTS_PAUSE (500) // Пауза таймера коллекции чартов в миллисекундах #define COLLECTION_CHARTS_COUNTER_STEP (16) // Шаг приращения счётчика таймера чартов #define COLLECTION_CHARTS_COUNTER_ID (9) // Идентификатор счётчика таймера чартов
Просто я быстрее двух раз в секунду нажал кнопку закрытия последнего открытого графика, и произошла попытка закрыть уже закрытый предыдущий график, о котором запись в списке коллекции чартов ещё оставалась. Частоту обновления текущего состояния открытых графиков, их окон и индикаторов в них можно подстроить изменением указанной макроподстановки. Чтобы быстрее обновлялось окружение, нужно уменьшить значение этой константы. При этом возрастёт нагрузка на процессор из-за увеличенной частоты обновлений. Здесь нужно подобрать "золотую середину", так как этот функционал всё-таки предназначен для ручного управления графиками, поэтому частота обновлений индивидуальна, а иногда возникающие ошибки обращения к отсутствующим графикам не критичны — просто второй раз нажать на кнопку — когда произойдёт очередное обновление окружения и синхронизация списка объектов-чартов в библиотеке с их состоянием в клиентском терминале.
Осталось протестировать создание скриншотов текущего графика. Каждое нажатие кнопки будет создавать скриншот графика в определённом размере. Первое нажатие — скриншот 800x600, второе нажатие — скриншот 750x562, третье нажатие — скриншот в текущем размере графика:
После создания трёх скриншотов в разных разрешениях, о чём в журнале библиотека сделала записи,
CChartObj::ScreenShot800x600: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.25.png (800 x 600) CChartObj::ScreenShot750x562: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.28.png (750 x 562) CChartObj::ScreenShotWndSize: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.29.png (726 x 321)
мы вдобавок поглядели содержимое папки, в которую эти скриншоты сохраняются.
Перед созданием скриншотов я поводил курсором в разных местах двух окон текущего графика, и в комментарии на графике можно увидеть время, цену, номер подокна и координаты X и Y курсора в пикселях. У координаты Y курсора есть два значения. Первое значение отображает координату Y относительно начальных координат главного окна символа, второе же значение, отображаемое (в скобках), показывает значение Y-координаты относительно верхней границы окна, в котором находится курсор.
Как видим, весь запланированный на сегодня функционал работает исправно.
Что дальше
В следующей статье сделаем автоматическое отслеживание событий изменения свойств объектов-чартов и их окон.
Ниже прикреплены все файлы текущей версии библиотеки и файл тестового советника для MQL5. Их можно скачать и протестировать всё самостоятельно.
При возникновении вопросов, замечаний и пожеланий, вы можете озвучить их в комментариях к статье.
*Статьи этой серии:
Прочие классы в библиотеке DoEasy (Часть 67): Класс объекта-чарта
Прочие классы в библиотеке DoEasy (Часть 68): Класс объекта-окна графика и классы объектов-индикаторов в окне графика
Прочие классы в библиотеке DoEasy (Часть 69): Класс-коллекция объектов-чартов
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Limit Order, кнопкой DeletePending, дает много одинаковых событии OnChartEvent а должны быть разные же
Или через Bulk Operations Delete Limit Orders дает много одинаковых событии
Если закрывать в ручную то события, идут последовательно
Вообще не понял о чём Вы...
При закрытии отложенных 10 ордеров идут 10 событии но они одинаковые
Только в ручную если закрывать идут разные события в OnChartEvent
Вот так поправил вроде работает.
При закрытии отложенных 10 ордеров идут 10 событии но они одинаковые
Только в ручную если закрывать идут разные события в OnChartEvent
Вот так поправил вроде работает.
Имелось в виду, что библиотека отправляет неправильные события в обработчик событий?
Да. Я это имел ввиду. Вроде поправил, но не уверен в решении, так как не особо понимаю, где будут минусы.
Да. Я это имел ввиду. Вроде поправил, но не уверен в решении, так как не особо понимаю, где будут минусы.