Графика в библиотеке DoEasy (Часть 91): События стандартных графических объектов в программе. История изменения имени объекта

17 декабря 2021, 14:03
Artyom Trishkin
4
1 235

Содержание


Концепция

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

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

Описанную выше концепцию сегодня делать полностью не будем, но протестируем её функционал на примере изменения свойства "Имя объекта" — будем сохранять историю его изменения. А в последующих статьях постепенно создадим возможность записи и воспроизведения всех свойств объекта.

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


Доработка классов библиотеки

Как обычно, начнём с внесения новых сообщений библиотеки в файле \MQL5\Include\DoEasy\Data.mqh.

Впишем индексы новых сообщений:

//--- CGraphElementsCollection
   MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST,           // Не удалось получить список вновь добавленных объектов
   MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST,         // Не удалось изъять графический объект из списка
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST,          // Не удалось поместить графический объект в список удалённых объектов
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_RNM_LIST,          // Не удалось поместить графический объект в список переименованных объектов
   
   MSG_GRAPH_OBJ_CREATE_EVN_CTRL_INDICATOR,           // Создан индикатор контроля и отправки событий
   MSG_GRAPH_OBJ_FAILED_CREATE_EVN_CTRL_INDICATOR,    // Не удалось создать индикатор контроля и отправки событий
   MSG_GRAPH_OBJ_ADDED_EVN_CTRL_INDICATOR,            // Индикатор контроля и отправки событий успешно добавлен на график
   MSG_GRAPH_OBJ_FAILED_ADD_EVN_CTRL_INDICATOR,       // Не удалось добавить индикатор контроля и отправки событий на график
   MSG_GRAPH_OBJ_ALREADY_EXIST_EVN_CTRL_INDICATOR,    // Индикатор контроля и отправки событий уже есть на графике
   MSG_GRAPH_OBJ_CLOSED_CHARTS,                       // Закрыто окон графиков:
   MSG_GRAPH_OBJ_OBJECTS_ON_CLOSED_CHARTS,            // С ними удалено объектов:
   MSG_GRAPH_OBJ_FAILED_CREATE_EVN_OBJ,               // Не удалось создать объект-событие для графического объекта
   MSG_GRAPH_OBJ_FAILED_ADD_EVN_OBJ,                  // Не удалось добавить объект-событие в список
   MSG_GRAPH_OBJ_GRAPH_OBJ_PROP_CHANGE_HISTORY,       // История изменения свойства: 
   
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE,                // Создан новый графический объект
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE,                // Изменено свойство графического объекта
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME,                // Графический объект переименован
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE,                // Графический объект удалён
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART,             // Графический объект удалён вместе с графиком
   
  };
//+------------------------------------------------------------------+

и текстовые сообщения, соответствующие вновь добавленным индексам:

//--- CGraphElementsCollection
   {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"},
   {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"},
   {"Не удалось поместить графический объект в список удалённых объектов","Failed to place graphic object in the list of deleted objects"},
   {"Не удалось поместить графический объект в список переименованных объектов","Failed to place graphic object in the list of renamed objects"},
   
   {"Создан индикатор контроля и отправки событий","An indicator for monitoring and sending events has been created"},
   {"Не удалось создать индикатор контроля и отправки событий","Failed to create indicator for monitoring and sending events"},
   {"Индикатор контроля и отправки событий успешно добавлен на график ","The indicator for monitoring and sending events has been successfully added to the chart "},
   {"Не удалось добавить индикатор контроля и отправки событий на график ","Failed to add the indicator of monitoring and sending events to the chart "},
   {"Индикатор контроля и отправки событий уже есть на графике","The indicator for monitoring and sending events is already on the chart"},
   {"Закрыто окон графиков: ","Closed chart windows: "},
   {"С ними удалено объектов: ","Objects removed with them: "},
   {"Не удалось создать объект-событие для графического объекта","Failed to create event object for graphic object"},
   {"Не удалось добавить объект-событие в список","Failed to add event object to list"},
   {"История изменения свойства: ","Property change history: "},
   
   {"Создан новый графический объект","New graphic object created"},
   {"Изменено свойство графического объекта","Changed graphic object property"},
   {"Графический объект переименован","Graphic object renamed"},
   {"Удалён графический объект","Graphic object deleted"},
   {"Графический объект удалён вместе с графиком","The graphic object has been removed along with the chart"},
   
  };
//+---------------------------------------------------------------------+


В публичной секции класса абстрактного стандартного графического объекта в файле \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh впишем новый метод для возврата указателя на объект свойств и объявим метод для вывода истории переименований графического объекта в журнал:

//--- Возвращает (1) себя, (2) свойства
   CGStdGraphObj    *GetObject(void)                                       { return &this;      }
   CProperties      *Properties(void)                                      { return this.Prop;  }
//--- Возвращает флаг поддержания объектом данного свойства
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) { return true;       }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property)  { return true;       }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING property)  { return true;       }

//--- Возвращает описание (1) целочисленного, (2) вещественного и (3) строкового свойства
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_INTEGER property);
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_DOUBLE property);
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_STRING property);

//--- Возвращает описание положения точки привязки графического объекта
   virtual string    AnchorDescription(void)                                  const { return (string)this.GetProperty(GRAPH_OBJ_PROP_ANCHOR,0); }

//--- Выводит в журнал описание свойств объекта (full_prop=true - все свойства, false - только поддерживаемые - реализуется в наследниках класса)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Выводит в журнал краткое описание объекта
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
//--- Возвращает краткое наименование объекта
   virtual string    Header(const bool symbol=false);
//--- Выводит в журнал историю переименований объекта
   void              PrintRenameHistory(void);

//--- Возвращает описание (1) типа (ENUM_OBJECT) графического объекта
   virtual string    TypeDescription(void)                                    const { return CMessage::Text(MSG_GRAPH_STD_OBJ_ANY);          }


При переименовании объекта его прошлое имя мы не можем узнать в случае, если оно нигде заранее не записано. Механизм определения переименования объекта у нас уже построен — для каждого физического объекта на графике у нас в коллекции графических объектов хранится объект класса стандартного графического объекта с описанием его параметров. Как только определяется событие переименования объекта, мы в соответствующем ему объекте класса меняем имя. Но перед заменой имени мы можем узнать прошлое имя объекта и сохранить его для истории. И точно так же можно сделать и с остальными свойствами.

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

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

В публичной секции класса напишем метод для установки прошлого имени объекта:

//--- Сравнивает объекты CGStdGraphObj между собой по указанному свойству (для сортировки списка по свойству графического объекта)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Сравнивает объекты CGStdGraphObj между собой по всем свойствам (для поиска равных графических объектов)
   bool              IsEqual(CGStdGraphObj* compared_req) const;
   
//--- Устанавливает прошлое имя объекта
   bool              SetNamePrev(const string name)
                       {
                        if(!this.Prop.SetSizeRange(GRAPH_OBJ_PROP_NAME,this.Prop.CurrSize(GRAPH_OBJ_PROP_NAME)+1))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_NAME,this.Prop.CurrSize(GRAPH_OBJ_PROP_NAME)-1,name);
                        return true;
                       }
  
//--- Конструктор по умолчанию
                     CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; m_group=WRONG_VALUE; }
//--- Деструктор
                    ~CGStdGraphObj()
                       {
                        if(this.Prop!=NULL)
                           delete this.Prop;
                       }
protected:

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

За пределами тела класса напишем реализацию метода, выводящего в журнал историю переименований объекта:

//+------------------------------------------------------------------+
//| Выводит в журнал историю переименований объекта                  |
//+------------------------------------------------------------------+
void CGStdGraphObj::PrintRenameHistory(void)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_GRAPH_OBJ_PROP_CHANGE_HISTORY),this.GetPropertyDescription(GRAPH_OBJ_PROP_NAME),":");
   int size=this.Properties().CurrSize(GRAPH_OBJ_PROP_NAME);
   ::Print(DFUN,"0: ",this.GetProperty(GRAPH_OBJ_PROP_NAME,0));
   for(int i=size-1;i>0;i--)
      ::Print(DFUN,size-i,": ",this.GetProperty(GRAPH_OBJ_PROP_NAME,i));
  }
//+------------------------------------------------------------------+

Здесь: сначала в журнал выводится заголовок с новым именем объекта, затем выводится его текущее наименование с указанием индекса 0,
и далее в обратном цикле до ячейки массива с индексом 1 включительно выводим описание свойства объекта "Имя", вписанного в ячейки массива имени объекта, хранящие историю его переименований по индексу цикла. Так как текущее имя объекта всегда хранится в нулевой ячейке, а предыдущее — в последней, то для правильного вывода номера очерёдности переименований объекта мы рассчитываем это значение, а не используем индекс цикла.

В итоге, для трижды переименованного объекта в журнал будет выведена такая запись:

CGStdGraphObj::PrintRenameHistory: История изменения свойства: Имя: "M30 Vertical Line 24264_3":
CGStdGraphObj::PrintRenameHistory: 0: M30 Vertical Line 24264_3
CGStdGraphObj::PrintRenameHistory: 1: M30 Vertical Line 24264_2
CGStdGraphObj::PrintRenameHistory: 2: M30 Vertical Line 24264_1
CGStdGraphObj::PrintRenameHistory: 3: M30 Vertical Line 24264

Как видим, здесь мы получили наглядную картину последовательности переименований графического объекта. (Впрочем, скорее всего мы поменяем нумерацию этой последовательности так, чтобы номер 0 всегда соответствовал первоначальному имени объекта, а последний номер — текущему имени).


Основную работу по доработке отслеживания событий графических объектов будем делать в классе коллекции графических объектов.

Откроем файл \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh и внесём в него все необходимые изменения.

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

Но мы будем делать чуть иначе — сначала создадим список всех добавленных объектов, а уже потом — из этого списка, будем отправлять события на график управляющей программы. Вновь добавляемые объекты мы проверяем в методах Refresh() объектов класса управления чартами CChartObjectsControl, которые у нас имеются для каждого из открытых графиков. В методах мы создаём объекты классов для каждого добавленного объекта, затем будем добавлять их в список, и далее, в списке по этому циклу будем отправлять сообщения о создании объектов в управляющую программу. Из программы будет вызван обработчик событий библиотеки, который получит нужный объект управления чартом, от которого пришли сообщения, извлечёт из списка объекты классов стандартных графических объектов и переместит их в список-коллекцию.

После доработки метод Refresh() класса объекта управления чартами будет выглядеть так:

//+------------------------------------------------------------------+
//| CChartObjectsControl: Проверяет объекты на чарте                 |
//+------------------------------------------------------------------+
void CChartObjectsControl::Refresh(void)
  {
//--- Очищаем список вновь добавленных объектов
   this.m_list_new_graph_obj.Clear();
//--- Рассчитаем количество новых объектов на чарте
   this.m_total_objects=::ObjectsTotal(this.ChartID());
   this.m_delta_graph_obj=this.m_total_objects-this.m_last_objects;
   //--- Если добавлен объект на график
   if(this.m_delta_graph_obj>0)
     {
      //--- Создаём список добавленных графических объектов
      for(int i=0;i<this.m_delta_graph_obj;i++)
        {
         //--- Получаем имя последнего добавленного объекта (если добавлен один новый объект),
         //--- или имя из списка объектов в терминале по индексу (если объектов добавлено несколько)
         string name=(this.m_delta_graph_obj==1 ? this.LastAddedGraphObjName() : ::ObjectName(this.m_chart_id,i));
         //--- Обрабатываем только объекты, созданные не программно
         if(name==NULL || ::StringFind(name,this.m_name_program)>WRONG_VALUE)
            continue;
         //--- Создаём объект класса графического объекта, соответствующий типу добавленного графического объекта
         ENUM_OBJECT type=(ENUM_OBJECT)::ObjectGetInteger(this.ChartID(),name,OBJPROP_TYPE);
         ENUM_OBJECT_DE_TYPE obj_type=ENUM_OBJECT_DE_TYPE(type+OBJECT_DE_TYPE_GSTD_OBJ+1);
         CGStdGraphObj *obj=this.CreateNewGraphObj(type,name);
         //--- Если объект создать не удалось - сообщаем об этом и идём на новую итерацию
         if(obj==NULL)
           {
            CMessage::ToLog(DFUN,MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ);
            continue;
           }
         //--- Устанавливаем объекту его принадлежность и добавляем созданный объект в список новых объектов
         obj.SetBelong(GRAPH_OBJ_BELONG_NO_PROGRAM); 
         //--- Если объект добавить в список не удалось - сообщаем об этом, удаляем объект и идём на следующую итерацию
         if(!this.m_list_new_graph_obj.Add(obj))
           {
            CMessage::ToLog(DFUN_ERR_LINE,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
            delete obj;
            continue;
           }
        }
      //--- Из созданного списка отправляем события на график управляющей программы
      for(int i=0;i<this.m_list_new_graph_obj.Total();i++)
        {
         CGStdGraphObj *obj=this.m_list_new_graph_obj.At(i);
         if(obj==NULL)
            continue;
         //--- Отправляем событие на график управляющей программы
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_CREATE,this.ChartID(),obj.TimeCreate(),obj.Name());
        }
     }
//--- сохранение индекса последнего добавленного графического объекта и разницы по сравнению с прошлой проверкой
   this.m_last_objects=this.m_total_objects;
   this.m_is_graph_obj_event=(bool)this.m_delta_graph_obj;
  }
//+------------------------------------------------------------------+

Логика метода полностью расписана в комментариях к коду. Хочу обратить внимание, что в параметре dparam функции EventChartCustom() мы передаём время создания графического объекта. Это нам необходимо для идентификации объектов, которые находятся на восстановленном графике, который ранее был удалён. Т.е., если разместить на открытом чарте графические объекты, затем удалить этот график, а после его восстановить, то будут восстановлены вместе с графиком и графические объекты, находившиеся на нём в момент его удаления. И такие объекты будут иметь нулевое время создания. Такое значение времени мы в последствии можем использовать для идентификации графических объектов, восстановленных вместе с графиком символа.

В методе, добавляющем на чарт индикатор контроля событий класса управления чартами, перед тем, как добавить индикатор на график, проверим что такого индикатора ещё нет на этом графике:

//+------------------------------------------------------------------+
//|CChartObjectsControl: Добавляет на чарт индикатор контроля событий|
//+------------------------------------------------------------------+
bool CChartObjectsControl::AddEventControlInd(void)
  {
   if(this.m_handle_ind==INVALID_HANDLE)
      return false;
   ::ResetLastError();
   string shortname="EventSend_From#"+(string)this.ChartID()+"_To#"+(string)this.m_chart_id_main;
   int total=::ChartIndicatorsTotal(this.ChartID(),0);
   for(int i=0;i<total;i++)
      if(::ChartIndicatorName(this.ChartID(),0,i)==shortname)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_ALREADY_EXIST_EVN_CTRL_INDICATOR);
         return true;
        }
   return ::ChartIndicatorAdd(this.ChartID(),0,this.m_handle_ind);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Коллекция графических объектов                                   |
//+------------------------------------------------------------------+
#resource "\\"+PATH_TO_EVENT_CTRL_IND;          // Индикатор контроля событий графических объектов, упакованный в ресурсы программы
class CGraphElementsCollection : public CBaseObj
  {
private:
   CArrayObj         m_list_charts_control;     // Список объектов управления чартами
   CListObj          m_list_all_canv_elm_obj;   // Список всех графических элементов на канвасе
   CListObj          m_list_all_graph_obj;      // Список всех графических объектов
   CArrayObj         m_list_deleted_obj;        // Список удалённых графических объектов
   bool              m_is_graph_obj_event;      // Флаг события в списке графических объектов
   int               m_total_objects;           // Количество графических объектов
   int               m_delta_graph_obj;         // Разница в количестве графических объектов по сравнению с прошлой проверкой
   
//--- Возвращает флаг наличия объекта класса графического элемента в списке-коллекции графических элементов
   bool              IsPresentGraphElmInList(const int id,const ENUM_GRAPH_ELEMENT_TYPE type_obj);
//--- Возвращает флаг наличия объекта класса графического объекта в списке-коллекции графических объектов
   bool              IsPresentGraphObjInList(const long chart_id,const string name);
//--- Возвращает флаг наличия графического объекта на графике по имени
   bool              IsPresentGraphObjOnChart(const long chart_id,const string name);
//--- Возвращает указатель на объект управления объектами указанного чарта
   CChartObjectsControl *GetChartObjectCtrlObj(const long chart_id);
//--- Создаёт новый объект управления графическими объектами указанного чарта и добавляет его в список
   CChartObjectsControl *CreateChartObjectCtrlObj(const long chart_id);
//--- Обновляет список графических объектов по идентификатору чарта
   CChartObjectsControl *RefreshByChartID(const long chart_id);
//--- Проверяет наличие окна графика
   bool              IsPresentChartWindow(const long chart_id);
//--- Обрабатывает удаление окна графика
   void              RefreshForExtraObjects(void);
//--- Возвращает первый свободный идентификатор графического (1) объекта, (2) элемента на канвасе
   long              GetFreeGraphObjID(bool program_object);
   long              GetFreeCanvElmID(void);
//--- Добавляет графический объект в коллекцию
   bool              AddGraphObjToCollection(const string source,CChartObjectsControl *obj_control);
//--- Находит объект, имеющийся в коллекции, но отсутствующий на графике
   CGStdGraphObj    *FindMissingObj(const long chart_id);
   CGStdGraphObj    *FindMissingObj(const long chart_id,int &index);
//--- Находит графический объект, имеющийся на графике, но отсутствующий в коллекции
   string            FindExtraObj(const long chart_id);
//--- Удаляет (1) указанный, (2) по идентификатору графика объект класса графического объекта из списка-коллекции графических объектов
   bool              DeleteGraphObjFromList(CGStdGraphObj *obj);
   void              DeleteGraphObjectsFromList(const long chart_id);
//--- Перемещает (1) указанный, (2) по индексу объект класса графического объекта в список удалённых графических объектов
   bool              MoveGraphObjToDeletedObjList(CGStdGraphObj *obj);
   bool              MoveGraphObjToDeletedObjList(const int index);
//--- Перемещает в список удалённых графических объектов все объекты по идентификатору графика
   void              MoveGraphObjectsToDeletedObjList(const long chart_id);
//--- Удаляет объект управления графиками из списка
   bool              DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj);
//--- Создаёт новый стандартный графический объект, возвращает имя объекта
   bool              CreateNewStdGraphObject(const long chart_id,
                                             const string name,
                                             const ENUM_OBJECT type,
                                             const int subwindow,
                                             const datetime time1,
                                             const double price1,
                                             const datetime time2=0,
                                             const double price2=0,
                                             const datetime time3=0,
                                             const double price3=0,
                                             const datetime time4=0,
                                             const double price4=0,
                                             const datetime time5=0,
                                             const double price5=0);
public:


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

public:
//--- Возвращает себя
   CGraphElementsCollection *GetObject(void)                                                             { return &this;                        }
//--- Возвращает полный список-коллекцию стандартных графических объектов "как есть"
   CArrayObj        *GetListGraphObj(void)                                                               { return &this.m_list_all_graph_obj;   }
//--- Возвращает полный список-коллекцию графических элеменов на канвасе "как есть"
   CArrayObj        *GetListCanvElm(void)                                                                { return &this.m_list_all_canv_elm_obj;}
//--- Возвращает список графических элементов по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)             { return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),property,value,mode);           }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL)            { return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),property,value,mode);           }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL)            { return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),property,value,mode);           }
//--- Возвращает список существующих графических объектов по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   CArrayObj        *GetList(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value,ENUM_COMPARER_TYPE mode=EQUAL)      { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,index,value,mode);    }
   CArrayObj        *GetList(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value,ENUM_COMPARER_TYPE mode=EQUAL)     { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,index,value,mode);    }
   CArrayObj        *GetList(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value,ENUM_COMPARER_TYPE mode=EQUAL)     { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,index,value,mode);    }
//--- Возвращает список удалённых графических объектов по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   CArrayObj        *GetListDel(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value,ENUM_COMPARER_TYPE mode=EQUAL)   { return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),property,index,value,mode);  }
   CArrayObj        *GetListDel(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),property,index,value,mode);  }
   CArrayObj        *GetListDel(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),property,index,value,mode);  }
//--- Возвращает количество новых графических объектов, (3) флаг произошедшего изменения в списке графических объектов
   int               NewObjects(void)   const                                                            { return this.m_delta_graph_obj;       }
   bool              IsEvent(void) const                                                                 { return this.m_is_graph_obj_event;    }
//--- Возвращает (1) существующий, (2) удалённый графический объект по имени и идентификатору графика
   CGStdGraphObj    *GetStdGraphObject(const string name,const long chart_id);
   CGStdGraphObj    *GetStdDelGraphObject(const string name,const long chart_id);
//--- Возвращает список (1) объектов управления графиками, (2) удалённых графических объектов
   CArrayObj        *GetListChartsControl(void)                                                          { return &this.m_list_charts_control;  }
   CArrayObj        *GetListDeletedObj(void)                                                             { return &this.m_list_deleted_obj;     }
//--- Возвращает (1) последний удалённый графический объект, (2) размер массива свойств графического объекта
   CGStdGraphObj    *GetLastDeletedGraphObj(void)                 const { return this.m_list_deleted_obj.At(this.m_list_deleted_obj.Total()-1); }
   int               GetSizeProperty(const string name,const long chart_id,const int prop)
                       {
                        CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id);
                        return(obj!=NULL ? obj.Properties().CurrSize(prop) : 0);
                       }
   
//--- Конструктор
                     CGraphElementsCollection();
//--- Выводит в журнал описание свойств объекта (full_prop=true - все свойства, false - только поддерживаемые - реализуется в наследниках класса)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Выводит в журнал краткое описание объекта
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
//--- Выводит в журнал историю переименований объекта
   void              PrintRenameHistory(const string name,const long chart_id);

//--- Создаёт список объектов управления чартами, возвращает количество чартов
   int               CreateChartControlList(void);
//--- Обновляет список (1) всех графических объектов, (2) на указанном чарте, заполняет данные о количестве новых и устанавливает флаг события
   void              Refresh(void);
   void              Refresh(const long chart_id);
//--- Обработчик событий
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

private:


В конструкторе класса очистим список удалённых графических объектов и установим ему флаг сортированного списка:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CGraphElementsCollection::CGraphElementsCollection()
  {
   this.m_type=COLLECTION_GRAPH_OBJ_ID;
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
   this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID);
   this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
   this.m_list_all_graph_obj.Clear();
   this.m_list_charts_control.Sort();
   this.m_list_charts_control.Clear();
   this.m_total_objects=0;
   this.m_is_graph_obj_event=false;
   this.m_list_deleted_obj.Clear();
   this.m_list_deleted_obj.Sort();
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Обновляет список всех графических объектов                       |
//+------------------------------------------------------------------+
void CGraphElementsCollection::Refresh(void)
  {
   this.RefreshForExtraObjects();
//--- Объявим переменные для поиска графиков
   long chart_id=0;
   int i=0;
//--- В цикле по всем открытым графикам терминала (не более 100)
   while(i<CHARTS_MAX)
     {
      //--- Получим идентификатор графика
      chart_id=::ChartNext(chart_id);
      if(chart_id<0)
         break;
      //--- Получаем указатель на объект управления графическими объектами
      //--- и обновляем список графических объектов по идентификатору чарта
      CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id);
      //--- Если указатель получить не удалось - идём к следующему графику
      if(obj_ctrl==NULL)
         continue;
      //--- Если есть событие изменения количества объектов на графике
      if(obj_ctrl.IsEvent())
        {
         //--- Если добавлен графический объект на график
         if(obj_ctrl.Delta()>0)
           {
            //--- Получаем список добавленных графических объектов и перемещаем их в список-коллекцию
            //--- (если объект поместить в коллекцию не удалось - идём к следующему объекту)
            if(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
               continue;
           }
         //--- Если удалён графический объект
         else if(obj_ctrl.Delta()<0)
           {
            int index=WRONG_VALUE;
            //--- В цикле по количеству удалённых графических объектов
            for(int j=0;j<-obj_ctrl.Delta();j++)
              {
               // Найдём лишний объект в списке
               CGStdGraphObj *obj=this.FindMissingObj(chart_id,index);
               if(obj!=NULL)
                 {
                  //--- Получим параметры удалённого объекта
                  long   lparam=obj.ChartID();
                  string sparam=obj.Name();
                  double dparam=(double)obj.TimeCreate();
                  //--- Переместим объект класса графического объекта в список удалённых объектов
                  //--- и отправим событие на график управляющей программы
                  if(this.MoveGraphObjToDeletedObjList(index))
                     ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DELETE,lparam,dparam,sparam);
                 }
              }
           }
        }
      //--- Увеличиваем индекс цикла
      i++;
     }
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Обрабатывает удаление окна графика                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::RefreshForExtraObjects(void)
  {
   for(int i=this.m_list_charts_control.Total()-1;i>WRONG_VALUE;i--)
     {
      CChartObjectsControl *obj_ctrl=this.m_list_charts_control.At(i);
      if(obj_ctrl==NULL)
         continue;
      if(!this.IsPresentChartWindow(obj_ctrl.ChartID()))
        {
         long chart_id=obj_ctrl.ChartID();
         string chart_symb=obj_ctrl.Symbol();
         int total_ctrl=this.m_list_charts_control.Total();
         this.DeleteGraphObjCtrlObjFromList(obj_ctrl);
         int total_obj=this.m_list_all_graph_obj.Total();
         this.MoveGraphObjectsToDeletedObjList(chart_id);
         int del_ctrl=total_ctrl-this.m_list_charts_control.Total();
         int del_obj=total_obj-this.m_list_all_graph_obj.Total();
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DEL_CHART,chart_id,del_obj,chart_symb);
        }
     }
  }
//+------------------------------------------------------------------+


Метод, находящий объект, имеющийся в коллекции, но отсутствующий на графике, и возвращающий указатель на объект и индекс объекта в списке:

//+------------------------------------------------------------------+
//|Находит объект, имеющийся в коллекции, но отсутствующий на графике|
//| Возвращает указатель на объект и индекс объекта в списке.        |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::FindMissingObj(const long chart_id,int &index)
  {
   CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL);
   if(list==NULL)
      return NULL;
   index=WRONG_VALUE;
   for(int i=0;i<list.Total();i++)
     {
      CGStdGraphObj *obj=list.At(i);
      if(obj==NULL)
         continue;
      if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name()))
        {
         index=i;
         return obj;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

Этот метод является перегрузкой одноимённого метода, написанного нами ранее. Но в отличие от своего "напарника", метод возвращает помимо указателя на найденный объект, ещё и индекс найденного объекта, что бывает нужно, когда требуется доступ к найденному объекту по его индексу в списке.


Метод, перемещающий в список удалённых графических объектов все объекты по идентификатору графика:

//+------------------------------------------------------------------+
//| Перемещает в список удалённых графических объектов               |
//| все объекты по идентификатору графика                            |
//+------------------------------------------------------------------+
void CGraphElementsCollection::MoveGraphObjectsToDeletedObjList(const long chart_id)
  {
   CArrayObj *list=GetList(GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL);
   if(list==NULL)
      return;
   for(int i=list.Total()-1;i>WRONG_VALUE;i--)
     {
      CGStdGraphObj *obj=list.At(i);
      if(obj==NULL)
         continue;
      this.MoveGraphObjToDeletedObjList(obj);
     }
  }
//+------------------------------------------------------------------+

Метод перемещает все объекты с указанным идентификатором графика в список удалённых объектов.
Здесь: получаем список объектов с указанным идентификатором графика, в цикле по полученному списку получаем очередной объект и перемещаем его в список удалённых графических объектов методом, который рассмотрим чуть ниже.


Метод, перемещающий объект класса графического объекта по индексу в список удалённых графических объектов:

//+------------------------------------------------------------------+
//| Перемещает объект класса графического объекта по индексу         |
//| в список удалённых графических объектов                          |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(const int index)
  {
   CGStdGraphObj *obj=this.m_list_all_graph_obj.Detach(index);
   if(obj==NULL)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST);
      return false;
     }
   if(!this.m_list_deleted_obj.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST);
      delete obj;
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Метод перемещает объект, индекс которого передан в метод, из списка-коллекции в список удалённых объектов.
Здесь: извлекаем объект из списка по указанному индексу. Если объект извлечь не удалось, сообщаем об этом и возвращаем false.
Далее, если объект не удалось поместить в список удалённых объектов, сообщаем об этом, удаляем объект и возвращаем false.
В итоге возвращаем true.

Метод, перемещающий указанный объект класса графического объекта в список удалённых графических объектов:

//+------------------------------------------------------------------+
//| Перемещает указанный объект класса графического объекта          |
//| в список удалённых графических объектов                          |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(CGStdGraphObj *obj)
  {
   this.m_list_all_graph_obj.Sort();
   int index=this.m_list_all_graph_obj.Search(obj);
   return this.MoveGraphObjToDeletedObjList(index);
  }
//+------------------------------------------------------------------+

Метод перемещает указанный по указателю объект в список удалённых графических объектов.
Здесь: устанавливаем списку-коллекции флаг сортированного списка и получаем индекс объекта в списке при помощи метода Search().
При помощи метода, перемещающего объект по его индексу в списке, рассмотренному выше, перемещаем объект в список удалённых графических объектов.

Метод, возвращающий удалённый графический объект по имени и идентификатору графика:

//+------------------------------------------------------------------+
//| Возвращает удалённый графический объект                          |
//| по имени и идентификатору графика                                |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdDelGraphObject(const string name,const long chart_id)
  {
   CArrayObj *list=this.GetListDel(GRAPH_OBJ_PROP_CHART_ID,0,chart_id);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,0,name,EQUAL);
   return(list!=NULL && list.Total()>0 ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

В метод передаётся имя объекта и идентификатор графика. Далее, получаем список объектов по идентификатору графика, из полученного списка получаем список объектов по имени (должен быть лишь один такой объект). Если список получен и его размер больше нуля — возвращаем указатель на единственный объект в списке, расположенный по индексу 0. В противном случае возвращаем NULL — объект получить не удалось.

Из обработчика событий удалим блок кода, отвечающий за обработку событий графических объектов — его перенесём в обработчик событий советника:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj=NULL;
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Рассчитаем идентификатор графика
      //--- Если id события соответствует событию с текущего графика, то идентификатор графика получаем от ChartID
      //--- Если id события соответствует пользовательскому событию, то идентификатор графика получаем из lparam
      //--- Иначе - идентификатору графика присваиваем -1
      long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param);
      //--- Получим объект из списка-коллекции по его имени, записанном в sparam,
      //--- свойства которого изменены, или который был перемещён
      obj=this.GetStdGraphObject(sparam,chart_id);
      //--- Если объект не удалось получить по имени - он отсутствует в списке,
      //--- а значит его имя было изменено
      if(obj==NULL)
        {
         //--- Найдём в списке объект, которого нет на графике
         obj=this.FindMissingObj(chart_id);
         //--- Если и тут не удалось найти объект - уходим
         if(obj==NULL)
            return;
         //--- Получим имя переименованного  графического объекта на графике, которого нет в списке-коллекции
         string name_new=this.FindExtraObj(chart_id);
         //--- Отправим событие со старым именем объекта на график управляющей программы и
         //--- установим новое имя объекту в списке-коллекции, которому не соответствует ни один графический объект на графике
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),0,obj.Name());
         obj.SetName(name_new);
        }
      //--- Обновим свойства полученного объекта
      //--- и проверим их изменение
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
     }
//--- Обработка событий стандартных графических объектов
   if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE)
     {
      //--- В зависимости от типа события выводим сообщение о нём в журнал
      switch(idx)
        {
         case GRAPH_OBJ_EVENT_CREATE   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE));
           obj=this.GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
              obj.PrintShort();
           break;
         case GRAPH_OBJ_EVENT_CHANGE   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE));
           obj=this.GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
              obj.PrintShort();
           break;
         case GRAPH_OBJ_EVENT_RENAME   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME));
           obj=this.GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
              obj.PrintShort();
           break;
         case GRAPH_OBJ_EVENT_DELETE   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE));
           break;
         case GRAPH_OBJ_EVENT_DEL_CHART:
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": ChartID: ",lparam,", ChartSymbol: ",sparam);
           break;
         default:
           break;
        }
     }
  }
//+------------------------------------------------------------------+

В самом обработчике подправим логику отправки сообщения о переименовании графического объекта — если объекту установлено новое имя, то в этом случае отправляем событие на график управляющей программы:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj=NULL;
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Рассчитаем идентификатор графика
      //--- Если id события соответствует событию с текущего графика, то идентификатор графика получаем от ChartID
      //--- Если id события соответствует пользовательскому событию, то идентификатор графика получаем из lparam
      //--- Иначе - идентификатору графика присваиваем -1
      long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param);
      //--- Получим объект из списка-коллекции по его имени, записанном в sparam,
      //--- свойства которого изменены, или который был перемещён
      obj=this.GetStdGraphObject(sparam,chart_id);
      //--- Если объект не удалось получить по имени - он отсутствует в списке,
      //--- а значит его имя было изменено
      if(obj==NULL)
        {
         //--- Найдём в списке объект, которого нет на графике
         obj=this.FindMissingObj(chart_id);
         //--- Если и тут не удалось найти объект - уходим
         if(obj==NULL)
            return;
         //--- Получим имя переименованного  графического объекта на графике, которого нет в списке-коллекции
         string name_new=this.FindExtraObj(chart_id);
         //--- установим новое имя объекту в списке-коллекции, которому не соответствует ни один графический объект на графике,
         //--- и отправим событие с новым именем объекта на график управляющей программы
         if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name());
        }
      //--- Обновим свойства полученного объекта
      //--- и проверим их изменение
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
     }
  }
//+------------------------------------------------------------------+


Метод, выводящий в журнал историю переименований объекта:

//+------------------------------------------------------------------+
//| Выводит в журнал историю переименований объекта                  |
//+------------------------------------------------------------------+
void CGraphElementsCollection::PrintRenameHistory(const string name,const long chart_id)
  {
   CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id);
   if(obj==NULL)
      return;
   obj.PrintRenameHistory();
  }
//+------------------------------------------------------------------+

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

Для удобной работы с программой, нам нужно добавить в главный класс библиотеки CEngine некоторые методы.

Откроем файл \MQL5\Include\DoEasy\Engine.mqh и добавим метод, возвращающий список удалённых графических объектов, метод, возвращающий количество удалённых графических объектов, и метод, возвращающий размер массива указанного свойства:

//--- Запускает новый отсчёт паузы
   void                 Pause(const ulong pause_msc,const datetime time_start=0)
                          {
                           this.PauseSetWaitingMSC(pause_msc);
                           this.PauseSetTimeBegin(time_start*1000);
                           while(!this.PauseIsCompleted() && !::IsStopped()){}
                          }

//--- Возвращает (1) коллекцию графических объектов, (2) список удалённых объектов
   CGraphElementsCollection *GetGraphicObjCollection(void)              { return &this.m_graph_objects;                       }
   CArrayObj           *GetListDeletedObj(void)                         { return this.m_graph_objects.GetListDeletedObj();    }
//--- Возвращает (1) количество удалённых графических объектов, (2) размер массива свойства
   int                  TotalDeletedGraphObjects(void)                  { return this.GetListDeletedObj().Total();            }
   int                  GraphGetSizeProperty(const string name,const long chart_id,const int prop)
                          {
                           return this.m_graph_objects.GetSizeProperty(name,chart_id,prop);
                          }
   
//--- Заполняет массив идентификаторами графиков, открытых в терминале

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


Тестирование

Для тестирования возьмём советник из прошлой статьи и
сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part91\ под новым именем TestDoEasyPart91.mq5.

Всё, что нам нужно сделать, это добавить блок кода обработки событий графических объектов, удалённый нами из обработчика событий класса-коллекции графических объектов, в обработчик 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_MOUSE_MOVE)
     {
      CForm *form=NULL;
      datetime time=0;
      double price=0;
      int wnd=0;
      
      //--- Если клавиша Ctrl не нажата
      if(!IsCtrlKeyPressed())
        {
         //--- очищаем список созданных объектов-форм и разрешаем прокручивать график мышкой и показывать контекстное меню
         list_forms.Clear();
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true);
         ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,true);
         return;
        }
      
      //--- Если координаты  X и Y графика успешно преобразованы в значения время и цена
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- получим индекс бара, над которым находится курсор
         int index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         if(index==WRONG_VALUE)
            return;
         
         //--- Получим объект-бар по индексу
         CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index);
         if(bar==NULL)
            return;
         
         //--- Преобразуем координаты графика из представления время/цена объекта-бара в координаты по оси X и Y
         int x=(int)lparam,y=(int)dparam;
         if(!ChartTimePriceToXY(ChartID(),0,bar.Time(),(bar.Open()+bar.Close())/2.0,x,y))
            return;
         
         //--- Запретим перемещать график мышкой и показывать контекстное меню
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,false);
         ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,false);
         
         //--- Создадим имя объекта-формы и скроем все объекты кроме одного с таким именем
         string name="FormBar_"+(string)index;
         HideFormAllExceptOne(name);
         
         //--- Если ещё нет объекта-формы с таким именем
         if(!IsPresentForm(name))
           {
            //--- создадим новый объект-форму
            form=bar.CreateForm(index,name,x,y,114,16);   
            if(form==NULL)
               return;
            
            //--- Установим форме флаги активности и неперемещаемости
            form.SetActive(true);
            form.SetMovable(false);
            //--- Установим непрозрачность 200
            form.SetOpacity(200);
            //--- Цвет фона формы зададим как первый цвет из массива цветов
            form.SetColorBackground(array_clr[0]);
            //--- Цвет очерчивающей рамки формы
            form.SetColorFrame(C'47,70,59');
            //--- Установим флаг рисования тени
            form.SetShadow(true);
            //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
            color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
            //--- Если в настройках задано использовать цвет фона графика, то затемним монохромный цвет на 20 единиц
            //--- Иначе - будем использовать для рисования тени заданный в настройках цвет
            color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
            //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
            //--- Непрозрачность тени при этом установим равной 200, а радиус размытия равный 4
            form.DrawShadow(2,2,clr,200,3);
            //--- Зальём фон формы вертикальным градиентом
            form.Erase(array_clr,form.Opacity());
            //--- Нарисуем очерчивающий прямоугольник по краям формы
            form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
            //--- Если объект-форму не удалось добавить в список - удаляем форму и уходим из обработчика
            if(!list_forms.Add(form))
              {
               delete form;
               return;
              }
            //--- Зафиксируем внешний вид формы
            form.Done();
           }
         //--- Если объект-форма существует
         if(form!=NULL)
           {
            //--- нарисуем на нём текст с описанием типа бара, соответствующего положению курсора мышки и покажем форму
            form.TextOnBG(0,bar.BodyTypeDescription(),form.Width()/2,form.Height()/2-1,FRAME_ANCHOR_CENTER,C'7,28,21');
            form.Show();
           }
         //--- Перерисуем чарт
         ChartRedraw();
        }
     }
   */
   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         long array[];
         engine.GraphGetArrayChartsID(array);
         for(int i=0;i<ArraySize(array);i++)
            engine.CreateLineVertical(array[i],"LineVertical",0,time);
        }
     }
   engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

//--- Обработка событий стандартных графических объектов
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   CGStdGraphObj *obj=NULL;
   if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE)
     {
      CChartObjectsControl *chart_ctrl=NULL;
      int end=0;
      string evn="";
      //--- В зависимости от типа события выводим сообщение о нём в журнал
      switch(idx)
        {
         //--- Событие создания графического объекта
         case GRAPH_OBJ_EVENT_CREATE   :
           //--- Выводим сообщение о создании нового графического объекта
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE),":");
           //--- Получаем указатель на объект по имени и идентификатору графика, переданных, соответственно, в sparam и lparam
           //--- и выводим в журнал краткое описание вновь созданного объекта
           obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              obj.PrintShort();
             }
           break;
         //--- Событие изменения свойства графического объекта
         case GRAPH_OBJ_EVENT_CHANGE   :
           //--- Выводим сообщение об изменении свойства графического объекта
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE),":");
           //--- Получаем указатель на объект по имени и идентификатору графика, переданных, соответственно, в sparam и lparam
           obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              //--- Выводим в журнал краткое описание изменённого объекта
              obj.PrintShort();
              //--- рассчитываем код изменённого свойства, которое было передано в dparam и получаем описание этого свойства
              if(dparam<GRAPH_OBJ_PROP_INTEGER_TOTAL)
                 evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_INTEGER)dparam);
              else if(dparam<GRAPH_OBJ_PROP_DOUBLE_TOTAL)
                 evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_DOUBLE)dparam);
              else
                 evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_STRING)dparam);
              //--- Выводим в журнал описание изменённого свойства графического объекта
              Print(DFUN,evn);
             }
           break;
         //--- Событие переименования графического объекта
         case GRAPH_OBJ_EVENT_RENAME   :
           //--- Выводим сообщение о переименовании графического объекта
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME));
           //--- Получаем указатель на объект по имени и идентификатору графика, переданных, соответственно, в sparam и lparam
           obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              //--- Выводим в журнал прошлое и новое имя объекта и всю историю его переименований
              Print(DFUN,obj.GetProperty(GRAPH_OBJ_PROP_NAME,obj.Properties().CurrSize(GRAPH_OBJ_PROP_NAME)-1)," >>> ",obj.GetProperty(GRAPH_OBJ_PROP_NAME,0));
              obj.PrintRenameHistory();
             }
           break;
         //--- Событие удаления графического объекта
         case GRAPH_OBJ_EVENT_DELETE   :
           //--- Выводим сообщение об удалении графического объекта
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE),":");
           //--- Получаем указатель на удалённый объект по имени и идентификатору графика, переданных, соответственно, в sparam и lparam
           //--- и выводим в журнал краткое описание удалённого объекта
           obj=engine.GetGraphicObjCollection().GetStdDelGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              obj.PrintShort();
             }
           break;
         //--- Событие удаления графического объекта вместе с окном графика
         case GRAPH_OBJ_EVENT_DEL_CHART:
           //--- Выводим сообщение об удалении графических объектов вместе с окном графика, идентификатор и символ которого переданы в lparam и sparam
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": #",lparam,", ",sparam,":");
           //--- Рассчитываем конечное значение для цикла по списку удалённых графических объектов
           end=engine.TotalDeletedGraphObjects()-(int)dparam;
           if(end<0)
              end=0;
           //--- В цикле от конца списка удалённых графических объектов до рассчитанного значения в переменной end
           for(int i=engine.TotalDeletedGraphObjects()-1;i>=end;i--)
             {
              //--- получаем из списка очередной удалённый графический объект
              obj=engine.GetListDeletedObj().At(i);
              if(obj==NULL)
                 continue;
              //--- и выводим в журнал его краткое описание
              obj.PrintShort();
             }
           break;
         //---
         default:
           break;
        }
     }
  }
//+------------------------------------------------------------------+

Логика в добавленном блоке кода очень подробно расписана, и останавливаться на её повторении, думаю, нет смысла.
В любом случае, любые вопросы можно задать в обсуждении к статье.

Скомпилируем советник и запустим его на графике:


Как видим, переименование объекта сохраняется в его "памяти".

Пакетное удаление графических объектов также обрабатывается корректно:


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

Что дальше

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

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

К содержанию

*Статьи этой серии:

Графика в библиотеке DoEasy (Часть 86): Коллекция графических объектов - контролируем модификацию свойств
Графика в библиотеке DoEasy (Часть 87): Коллекция графических объектов - контроль модификации свойств объектов на всех открытых графиках
Графика в библиотеке DoEasy (Часть 88): Коллекция графических объектов - двумерный динамический массив для хранения динамически изменяемых свойств объектов
Графика в библиотеке DoEasy (Часть 89): Программное создание стандартных графических объектов. Базовый функционал
Графика в библиотеке DoEasy (Часть 90): События стандартных графических объектов. Базовый функционал

Прикрепленные файлы |
MQL5.zip (4239.74 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Roman Shiredchenko

ИМХО, вообще кто-нибудь этой развернутой штукой пользуется?

Или так, чисто решили к НГ 200 халявных бакинских рублей поднять?

Плз, шапками не кидать и не банить, только только - помывшись и на сухо вытершись.

Artyom Trishkin
Roman Shiredchenko #:

ИМХО, вообще кто-нибудь этой развернутой штукой пользуется?

Или так, чисто решили к НГ 200 халявных бакинских рублей поднять?

Плз, шапками не кидать и не банить, только только - помывшись и на сухо вытершись.

Да, решили поднять 200 халявных. Ведь для этого ничего не нужно сделать ...

TheXpert
Roman Shiredchenko #:

ИМХО, вообще кто-нибудь этой развернутой штукой пользуется?

Вот вы бы пользовались недописанным монстром без документации но с описанием в 90 статей?
Визуальная оценка результатов оптимизации Визуальная оценка результатов оптимизации
Разговор в этой статье пойдёт о том, как построить графики всех проходов оптимизации и подобрать оптимальный пользовательский критерий. А также о том, как, имея минимальные знания в MQL5 и большое желание, используя статьи сайта и комментарии на форуме, написать то, что хочется.
Работаем со временем (Часть 1): Основные принципы Работаем со временем (Часть 1): Основные принципы
Рассмотренные в статье функции и код помогут лучше понять принципы обработки времени, смещение времени брокера и перехода на летнее или зимнее время. Точная работа со временем — очень важный аспект трейдинга. Лондонская или нью-йоркская биржа уже открылась или еще нет? Когда начинается и заканчивается торговая сессия на форексе?
Графика в библиотеке DoEasy (Часть 92): Класс памяти стандартных графических объектов. История изменения свойств объекта Графика в библиотеке DoEasy (Часть 92): Класс памяти стандартных графических объектов. История изменения свойств объекта
В статье создадим класс памяти стандартного графического объекта, позволяющий объекту сохранять свои состояния при модификации его свойств, что в свою очередь позволит в любое время вернуться к прошлым состояниям графического объекта.
Улучшение распознавания свечных паттернов на примере Доджи Улучшение распознавания свечных паттернов на примере Доджи
Как находить свечные паттерны чаще, чем обычно. За простотой свечных паттернов скрывается и серьезный недостаток, который как раз можно устранить, используя существенно выросшие возможности современных средств автоматизации трейдинга.