Таблицы в парадигме MVC на MQL5: настраиваемые и сортируемые столбцы таблицы
Содержание
- Введение
- Дорабатываем классы библиотеки
- Класс для упрощенного создания таблиц
- Тестируем результат
- Заключение
Введение
В предыдущей статье, посвящённой созданию таблиц на MQL5 в парадигме MVC, мы связали воедино табличные данные (Model) с их графическим представлением (View) в едином элементе управления (TableView), и на основе полученного объекта создали простую статичную таблицу. Таблицы — это удобный инструмент для классификации и отображения различных данных в удобном для пользователя представлении. Соответственно, таблица должна давать больше возможностей для пользователя по управлению отображением данных.
Сегодня добавим к таблицам возможность настраивать ширину столбцов, указывать типы отображаемых данных и сортировать данные таблицы по её столбцам. Для этого нам потребуется просто доработать уже созданные ранее классы элементов управления. Но в конце всё же добавим новый класс, упрощающий создание таблиц. Класс будет позволять в несколько строк создавать таблицы из ранее подготовленных данных.
В концепции MVC (Model — View — Controller) взаимодействие между тремя компонентами организовано так, что при изменении внешней составляющей (View) при помощи контроллера (Controller), изменяется модель (Model), и далее изменённая модель заново отображается визуальной компонентой (View). Здесь мы точно так же организуем взаимодействие между тремя составляющими — щелчок мышкой по заголовку столбца таблицы (работа компонента Controller) повлечёт за собой изменение в расположении данных в модели таблицы (реорганизация компонента Model), что повлечёт за собой изменение внешнего вида таблицы — отображение результата компонентом View.
Дорабатываем классы библиотеки
Все файлы разрабатываемой библиотеки расположены по адресу \MQL5\Indicators\Tables\. Файл классов модели таблиц (Tables.mqh), вместе с файлом тестового индикатора (iTestTable.mq5), располагается в папке \MQL5\Indicators\Tables\.
Файлы графической библиотеки (Base.mqh и Controls.mqh) расположены в подпапке \MQL5\Indicators\Tables\Controls\. Все необходимые для работы файлы можно загрузить одним архивом из прeдыдущей статьи.
Доработаем классы модели таблиц в файле \MQL5\Indicators\Tables\Tables.mqh.
Строки в модели таблицы по умолчанию имеют тип сортировки по идентификатору (индексу) строки. Самая первая строка имеет индекс 0. И ячейки в строке тоже начинаются с нулевого индекса. Для сортировки по индексам ячеек нам нужно обозначить некое число, которое будем прибавлять к индексу ячейки, и по которому будем определять, что задана сортировка по индексу ячейки. И ещё нам нужно обозначить направление сортировки. Значит, нам нужны два числа: первое будет определять, что требуется сортировка по индексу ячейки по возрастанию, второе число будет определять необходимость сортировки по индексу ячейки по убыванию.
Определим макроподстановки для этих чисел:
//+------------------------------------------------------------------+ //| Макросы | //+------------------------------------------------------------------+ #define __TABLES__ // Идентификатор данного файла #define MARKER_START_DATA -1 // Маркер начала данных в файле #define MAX_STRING_LENGTH 128 // Максимальная длина строки в ячейке #define CELL_WIDTH_IN_CHARS 19 // Ширина ячейки таблицы в символах #define ASC_IDX_CORRECTION 10000 // Смещение индекса столбца для сортировки по возрастанию #define DESC_IDX_CORRECTION 20000 // Смещение индекса столбца для сортировки по убыванию
Определив такие макроподстановки, мы указали, что строк в таблице и ячеек в одной строке может быть не более 10 000. Кажется, этого более чем достаточно для работы с таблицами (100 миллионов ячеек таблицы).
В метод сортировки будем передавать в качестве параметра mode номер от нуля до 10 000 — это будет сортировка по индексам строк таблицы. Числа от 10 000 включительно до 19 999 будут указывать на сортировку по индексу столбца таблицы по возрастанию. Числа от 20 000 — по индексу столбца по убыванию:
/*
Sort(0) - по индексу строки
Sort(ASC_IDX_CORRECTION) - по возрастанию по столбцу 0
Sort(1+ASC_IDX_CORRECTION) - по возрастанию по столбцу 1
Sort(2+ASC_IDX_CORRECTION) - по возрастанию по столбцу 2
и т.д.
Sort(DESC_IDX_CORRECTION) - по убыванию по столбцу 0
Sort(1+DESC_IDX_CORRECTION) - по убыванию по столбцу 1
Sort(2+DESC_IDX_CORRECTION) - по убыванию по столбцу 2
и т.д.
*/
Для того, чтобы метод сортировки списка Sort() правильно работал, необходимо переопределить виртуальный метод сравнения двух объектов Compare(), определённый в базовом объекте Стандартной Библиотеки. По умолчанию этот метод возвращает 0, что означает равенство сравниваемых объектов.
У нас уже есть реализованный метод сравнения в классе строки таблицы CTableRow. Доработаем его так, чтобы была возможность сортировки строк по индексам столбцов таблицы с учётом направления сортировки:
//+------------------------------------------------------------------+ //| Сравнение двух объектов | //+------------------------------------------------------------------+ int CTableRow::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; //--- Сортировка по индексу строки if(mode==0) { const CTableRow *obj=node; return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0); } //--- Сортировка по индексу ячейки с возрастанием/убыванием //--- Флаг направления сортировки и индекс ячейки для сортировки bool asc=(mode>=ASC_IDX_CORRECTION && mode<DESC_IDX_CORRECTION); int col= mode%(asc ? ASC_IDX_CORRECTION : DESC_IDX_CORRECTION); //--- Снимаем константность node CTableRow *nonconst_this=(CTableRow*)&this; CTableRow *nonconst_node=(CTableRow*)node; //--- Получаем текущую и сравниваемую ячейки по индексу mode CTableCell *cell_current =nonconst_this.GetCell(col); CTableCell *cell_compared=nonconst_node.GetCell(col); if(cell_current==NULL || cell_compared==NULL) return -1; //--- Сравниваем в зависимости от типа ячейки int cmp=0; switch(cell_current.Datatype()) { case TYPE_DOUBLE : cmp=(cell_current.ValueD()>cell_compared.ValueD() ? 1 : cell_current.ValueD()<cell_compared.ValueD() ? -1 : 0); break; case TYPE_LONG : case TYPE_DATETIME: case TYPE_COLOR : cmp=(cell_current.ValueL()>cell_compared.ValueL() ? 1 : cell_current.ValueL()<cell_compared.ValueL() ? -1 : 0); break; case TYPE_STRING : cmp=::StringCompare(cell_current.ValueS(),cell_compared.ValueS()); break; default : break; } //--- Возвращаем результат сравнения ячеек с учётом направления сортировки return(asc ? cmp : -cmp); }
Блок кода, выделенный цветом, выполняет сравнение двух ячеек таблицы по возрастанию (mode >= 10000 && <20000) и убыванию (mode>=20000). Так как нам для сравнения потребуется получить из объекта строки нужные объекты ячеек, а они не константные (при этом строка для сравнения в метод передана как константный указатель), то сначала необходимо снять константность для *node, объявив неконстантные объекты для сравнения. И уже из них получать объекты ячеек для сравнения.
Это опасные преобразования, так как нарушается константность указателей, и объекты могут быть случайно изменены. Но здесь мы точно знаем, что в данном методе выполняется только сравнение значений, но не их изменение. Поэтому здесь мы можем позволить себе небольшую контролируемую вольность для получения нужного результата.
В класс модели таблицы CTableModel добавим три новых метода для упрощения работы со столбцами таблицы и сортировки по ним:
public: //--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка CTableRow *RowAddNew(void); CTableRow *RowInsertNewTo(const uint index_to); //--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки bool RowDelete(const uint index); bool RowMoveTo(const uint row_index, const uint index_to); void RowClearData(const uint index); //--- (1) Возвращает, (2) выводит в журнал описание строки string RowDescription(const uint index); void RowPrint(const uint index,const bool detail); //--- (1) Добавляет, (2) удаляет (3) перемещает столбец, (4) очищает данные, устанавливает (5) тип, //--- (6) точность данных, флаги отображения (7) времени, (8) имён цветов столбца bool ColumnAddNew(const int index=-1); bool ColumnDelete(const uint index); bool ColumnMoveTo(const uint col_index, const uint index_to); void ColumnClearData(const uint index); void ColumnSetDatatype(const uint index,const ENUM_DATATYPE type); void ColumnSetDigits(const uint index,const int digits); void ColumnSetTimeFlags(const uint index, const uint flags); void ColumnSetColorNamesFlag(const uint index, const bool flag); //--- Сортирует таблицу по указанному столбцу и направлению void SortByColumn(const uint column, const bool descending); //--- (1) Возвращает, (2) выводит в журнал описание таблицы virtual string Description(void); void Print(const bool detail); void PrintTable(const int cell_width=CELL_WIDTH_IN_CHARS);
За пределами тела класса напишем их реализацию.
Метод, устанавливающий флаги отображения времени столбца:
//+------------------------------------------------------------------+ //| Устанавливает флаги отображения времени столбца | //+------------------------------------------------------------------+ void CTableModel::ColumnSetTimeFlags(const uint index,const uint flags) { //--- В цикле по всем строкам таблицы for(uint i=0;i<this.RowsTotal();i++) { //--- получаем из каждой строки ячейку с индексом столбца и устанавливаем флаги отображения времени CTableCell *cell=this.GetCell(i, index); if(cell!=NULL) cell.SetDatetimeFlags(flags); } }
Метод, устанавливающий флаг отображения имён цветов столбца:
//+------------------------------------------------------------------+ //| Устанавливает флагb отображения имён цветов столбца | //+------------------------------------------------------------------+ void CTableModel::ColumnSetColorNamesFlag(const uint index,const bool flag) { //--- В цикле по всем строкам таблицы for(uint i=0;i<this.RowsTotal();i++) { //--- получаем из каждой строки ячейку с индексом столбца и устанавливаем флаг отображения имён цветов CTableCell *cell=this.GetCell(i, index); if(cell!=NULL) cell.SetColorNameFlag(flag); } }
Оба метода в простом цикле по строками таблицы получают нужную ячейку из каждой последующей строки и устанавливают для неё указанный флаг.
Метод, сортирующий таблицу по указанному столбцу и направлению:
//+------------------------------------------------------------------+ //| Сортирует таблицу по указанному столбцу и направлению | //+------------------------------------------------------------------+ void CTableModel::SortByColumn(const uint column,const bool descending) { if(this.m_list_rows.Total()==0) return; int mode=(int)column+(descending ? DESC_IDX_CORRECTION : ASC_IDX_CORRECTION); this.m_list_rows.Sort(mode); this.CellsPositionUpdate(); }
В метод передаётся индекс столбца таблицы, по значениям которого необходимо сортировать таблицу, и флаг направления сортировки. Если список строк пустой — уходим из метода. Далее определяем режим сортировки (mode). Если сортировка по убыванию (descending == true), то к индексу столбца прибавляем 20000, если же сортировка по возрастанию, то к индексу столбца прибавляем 10000. Далее вызываем метод сортировки с указанием режима и обновляем все ячейки таблицы в каждой строке.
Теперь добавим новые методы в класс таблицы CTable. Это одноимённые методы для только что добавленных в класс модели таблицы:
public: //--- (1) Возвращает, (2) выводит в журнал описание ячейки, (3) назначенный в ячейку объект string CellDescription(const uint row, const uint col); void CellPrint(const uint row, const uint col); //---Возвращает (1) назначенный в ячейку объект, (2) тип назначенного в ячейку объекта CObject *CellGetObject(const uint row, const uint col); ENUM_OBJECT_TYPE CellGetObjType(const uint row, const uint col); //--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка CTableRow *RowAddNew(void); CTableRow *RowInsertNewTo(const uint index_to); //--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки bool RowDelete(const uint index); bool RowMoveTo(const uint row_index, const uint index_to); void RowClearData(const uint index); //--- (1) Возвращает, (2) выводит в журнал описание строки string RowDescription(const uint index); void RowPrint(const uint index,const bool detail); //--- (1) Добавляет новый, (2) удаляет, (3) перемещает столбец, (4) очищает данные столбца bool ColumnAddNew(const string caption,const int index=-1); bool ColumnDelete(const uint index); bool ColumnMoveTo(const uint index, const uint index_to); void ColumnClearData(const uint index); //--- Устанавливает (1) значение указанному заголовку, (2) точность данных, //--- флаги отображения (3) времени, (4) имён цвета указанному столбцу void ColumnCaptionSetValue(const uint index,const string value); void ColumnSetDigits(const uint index,const int digits); void ColumnSetTimeFlags(const uint index,const uint flags); void ColumnSetColorNamesFlag(const uint col, const bool flag); //--- (1) Устанавливает, (2) возвращает тип данных для указанного столбца void ColumnSetDatatype(const uint index,const ENUM_DATATYPE type); ENUM_DATATYPE ColumnDatatype(const uint index); //--- (1) Возвращает, (2) выводит в журнал описание объекта virtual string Description(void); void Print(const int column_width=CELL_WIDTH_IN_CHARS); //--- Сортирует таблицу по указанному столбцу и направлению void SortByColumn(const uint column, const bool descending) { if(this.m_table_model!=NULL) this.m_table_model.SortByColumn(column,descending); } //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(OBJECT_TYPE_TABLE); }
...
//+------------------------------------------------------------------+ //| Устанавливает флаги отображения времени указанному столбцу | //+------------------------------------------------------------------+ void CTable::ColumnSetTimeFlags(const uint index,const uint flags) { if(this.m_table_model!=NULL) this.m_table_model.ColumnSetTimeFlags(index,flags); } //+------------------------------------------------------------------+ //| Устанавливает флаги отображения имён цвета указанному столбцу | //+------------------------------------------------------------------+ void CTable::ColumnSetColorNamesFlag(const uint index,const bool flag) { if(this.m_table_model!=NULL) this.m_table_model.ColumnSetColorNamesFlag(index,flag); }
Методы проверяют валидность объекта модели таблицы и вызывают его соответствующие одноимённые методы, которые были рассмотрены выше.
Доработаем классы в файле \MQL5\Indicators\Tables\Controls\Base.mqh.
Допишем форвард-декларацию классов, которые сделаны были в прошлый раз, но не были добавлены в список, и новый класс, который сегодня будем делать:
//+------------------------------------------------------------------+ //| Включаемые библиотеки | //+------------------------------------------------------------------+ #include <Canvas\Canvas.mqh> // Класс СБ CCanvas #include <Arrays\List.mqh> // Класс СБ CList #include "..\Tables.mqh" //--- Форвард-декларация классов элементов управления class CBoundedObj; // Базовый класс, хранящий размеры объекта class CCanvasBase; // Базовый класс холста графических элементов class CCounter; // Класс счётчика задержки class CAutoRepeat; // Класс автоповтора событий class CImagePainter; // Класс рисования изображений class CVisualHint; // Класс подсказки class CLabel; // Класс текстовой метки class CButton; // Класс простой кнопки class CButtonTriggered; // Класс двухпозиционной кнопки class CButtonArrowUp; // Класс кнопки со стрелкой вверх class CButtonArrowDown; // Класс кнопки со стрелкой вниз class CButtonArrowLeft; // Класс кнопки со стрелкой влево class CButtonArrowRight; // Класс кнопки со стрелкой вправо class CCheckBox; // Класс элемента управления CheckBox class CRadioButton; // Класс элемента управления RadioButton class CScrollBarThumbH; // Класс ползунка горизонтальной полосы прокрутки class CScrollBarThumbV; // Класс ползунка вертикальной полосы прокрутки class CScrollBarH; // Класс горизонтальной полосы прокрутки class CScrollBarV; // Класс вертикальной полосы прокрутки class CTableCellView; // Класс визуального представления ячейки таблицы class CTableRowView; // Класс визуального представления строки таблицы class CColumnCaptionView; // Класс визуального представления заголовка столбца таблицы class CTableHeaderView; // Класс визуального представления заголовка таблицы class CTableView; // Класс визуального представления таблицы class CTableControl; // Класс управления таблицами class CPanel; // Класс элемента управления Panel class CGroupBox; // Класс элемента управления GroupBox class CContainer; // Класс элемента управления Container
В перечисление типов графических элементов добавим новый тип:
//+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_TYPE // Перечисление типов графических элементов { ELEMENT_TYPE_BASE = 0x10000, // Базовый объект графических элементов ELEMENT_TYPE_COLOR, // Объект цвета ELEMENT_TYPE_COLORS_ELEMENT, // Объект цветов элемента графического объекта ELEMENT_TYPE_RECTANGLE_AREA, // Прямоугольная область элемента ELEMENT_TYPE_IMAGE_PAINTER, // Объект для рисования изображений ELEMENT_TYPE_COUNTER, // Объект счётчика ELEMENT_TYPE_AUTOREPEAT_CONTROL, // Объект автоповтора событий ELEMENT_TYPE_BOUNDED_BASE, // Базовый объект размеров графических элементов ELEMENT_TYPE_CANVAS_BASE, // Базовый объект холста графических элементов ELEMENT_TYPE_ELEMENT_BASE, // Базовый объект графических элементов ELEMENT_TYPE_HINT, // Подсказка ELEMENT_TYPE_LABEL, // Текстовая метка ELEMENT_TYPE_BUTTON, // Простая кнопка ELEMENT_TYPE_BUTTON_TRIGGERED, // Двухпозиционная кнопка ELEMENT_TYPE_BUTTON_ARROW_UP, // Кнопка со стрелкой вверх ELEMENT_TYPE_BUTTON_ARROW_DOWN, // Кнопка со стрелкой вниз ELEMENT_TYPE_BUTTON_ARROW_LEFT, // Кнопка со стрелкой влево ELEMENT_TYPE_BUTTON_ARROW_RIGHT, // Кнопка со стрелкой вправо ELEMENT_TYPE_CHECKBOX, // Элемент управления CheckBox ELEMENT_TYPE_RADIOBUTTON, // Элемент управления RadioButton ELEMENT_TYPE_SCROLLBAR_THUMB_H, // Ползунок горизонтальной полосы прокрутки ELEMENT_TYPE_SCROLLBAR_THUMB_V, // Ползунок вертикальной полосы прокрутки ELEMENT_TYPE_SCROLLBAR_H, // Элемент управления ScrollBarHorisontal ELEMENT_TYPE_SCROLLBAR_V, // Элемент управления ScrollBarVertical ELEMENT_TYPE_TABLE_CELL_VIEW, // Ячейка таблицы (View) ELEMENT_TYPE_TABLE_ROW_VIEW, // Строка таблицы (View) ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Заголовок столбца таблицы (View) ELEMENT_TYPE_TABLE_HEADER_VIEW, // Заголовок таблицы (View) ELEMENT_TYPE_TABLE_VIEW, // Таблица (View) ELEMENT_TYPE_TABLE_CONTROL_VIEW, // Элемент управления таблицами (View) ELEMENT_TYPE_PANEL, // Элемент управления Panel ELEMENT_TYPE_GROUPBOX, // Элемент управления GroupBox ELEMENT_TYPE_CONTAINER, // Элемент управления Container };
В функцию, возвращающую короткое имя элемента по типу, добавим новый тип объекта:
//+------------------------------------------------------------------+ //| Возвращает короткое имя элемента по типу | //+------------------------------------------------------------------+ string ElementShortName(const ENUM_ELEMENT_TYPE type) { switch(type) { case ELEMENT_TYPE_ELEMENT_BASE : return "BASE"; // Базовый объект графических элементов case ELEMENT_TYPE_HINT : return "HNT"; // Подсказка case ELEMENT_TYPE_LABEL : return "LBL"; // Текстовая метка case ELEMENT_TYPE_BUTTON : return "SBTN"; // Простая кнопка case ELEMENT_TYPE_BUTTON_TRIGGERED : return "TBTN"; // Двухпозиционная кнопка case ELEMENT_TYPE_BUTTON_ARROW_UP : return "BTARU"; // Кнопка со стрелкой вверх case ELEMENT_TYPE_BUTTON_ARROW_DOWN : return "BTARD"; // Кнопка со стрелкой вниз case ELEMENT_TYPE_BUTTON_ARROW_LEFT : return "BTARL"; // Кнопка со стрелкой влево case ELEMENT_TYPE_BUTTON_ARROW_RIGHT : return "BTARR"; // Кнопка со стрелкой вправо case ELEMENT_TYPE_CHECKBOX : return "CHKB"; // Элемент управления CheckBox case ELEMENT_TYPE_RADIOBUTTON : return "RBTN"; // Элемент управления RadioButton case ELEMENT_TYPE_SCROLLBAR_THUMB_H : return "THMBH"; // Ползунок горизонтальной полосы прокрутки case ELEMENT_TYPE_SCROLLBAR_THUMB_V : return "THMBV"; // Ползунок вертикальной полосы прокрутки case ELEMENT_TYPE_SCROLLBAR_H : return "SCBH"; // Элемент управления ScrollBarHorisontal case ELEMENT_TYPE_SCROLLBAR_V : return "SCBV"; // Элемент управления ScrollBarVertical case ELEMENT_TYPE_TABLE_CELL_VIEW : return "TCELL"; // Ячейка таблицы (View) case ELEMENT_TYPE_TABLE_ROW_VIEW : return "TROW"; // Строка таблицы (View) case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW : return "TCAPT"; // Заголовок столбца таблицы (View) case ELEMENT_TYPE_TABLE_HEADER_VIEW : return "THDR"; // Заголовок таблицы (View) case ELEMENT_TYPE_TABLE_VIEW : return "TABLE"; // Таблица (View) case ELEMENT_TYPE_TABLE_CONTROL_VIEW : return "TBLCTRL"; // Элемент управления таблицами (View) case ELEMENT_TYPE_PANEL : return "PNL"; // Элемент управления Panel case ELEMENT_TYPE_GROUPBOX : return "GRBX"; // Элемент управления GroupBox case ELEMENT_TYPE_CONTAINER : return "CNTR"; // Элемент управления Container default : return "Unknown"; // Unknown } }
В базовый класс графических элементов добавим методы, возвращающие координаты курсора:
//+------------------------------------------------------------------+ //| Базовый класс графических элементов | //+------------------------------------------------------------------+ class CBaseObj : public CObject { protected: int m_id; // Идентифткатор ushort m_name[]; // Наименование public: //--- Устанавливает (1) наименование, (2) идентификатор void SetName(const string name) { ::StringToShortArray(name,this.m_name); } virtual void SetID(const int id) { this.m_id=id; } //--- Возвращает (1) наименование, (2) идентификатор string Name(void) const { return ::ShortArrayToString(this.m_name); } int ID(void) const { return this.m_id; } //--- Возвращает координаты курсора int CursorX(void) const { return CCommonManager::GetInstance().CursorX(); } int CursorY(void) const { return CCommonManager::GetInstance().CursorY(); } //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_BASE); } //--- (1) Возвращает, (2) выводит в журнал описание объекта virtual string Description(void); virtual void Print(void); //--- Конструктор/деструктор CBaseObj (void) : m_id(-1) { this.SetName(""); } ~CBaseObj (void) {} };
Это позволит каждому графическому элементу в любой момент иметь доступ к координатам курсора. Класс-синглтон CCommonManager постоянно отслеживает координаты курсора, и обращение к нему из любого графического элемента даёт доступ элементу к этим координатам.
В классе прямоугольной области добавим метод, снимающий назначение объекта на область:
//+------------------------------------------------------------------+ //| Класс прямоугольной области | //+------------------------------------------------------------------+ class CBound : public CBaseObj { protected: CBaseObj *m_assigned_obj; // Назначенный на область объект CRect m_bound; // Структура прямоугольной области public: //--- Изменяет (1) ширину, (2) высоту, (3) размер ограничивающего прямоугольника void ResizeW(const int size) { this.m_bound.Width(size); } void ResizeH(const int size) { this.m_bound.Height(size); } void Resize(const int w,const int h) { this.m_bound.Width(w); this.m_bound.Height(h); } //--- Устанавливает координату (1) X, (2) Y, (3) обе координаты ограничивающего прямоугольника void SetX(const int x) { this.m_bound.left=x; } void SetY(const int y) { this.m_bound.top=y; } void SetXY(const int x,const int y) { this.m_bound.LeftTop(x,y); } //--- (1) Устанавливает, (2) смещает ограничивающий прямоугольник на указанные координаты/размер смещения void Move(const int x,const int y) { this.m_bound.Move(x,y); } void Shift(const int dx,const int dy) { this.m_bound.Shift(dx,dy); } //--- Возвращает координаты, размеры и границы объекта int X(void) const { return this.m_bound.left; } int Y(void) const { return this.m_bound.top; } int Width(void) const { return this.m_bound.Width(); } int Height(void) const { return this.m_bound.Height(); } int Right(void) const { return this.m_bound.right-(this.m_bound.Width() >0 ? 1 : 0);} int Bottom(void) const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);} //--- Возвращает флаг нахождения курсора внутри области bool Contains(const int x,const int y) const { return this.m_bound.Contains(x,y); } //--- (1) Назначает, (2) снимает назначение, (3) возвращает указатель на назначенный элемент void AssignObject(CBaseObj *obj) { this.m_assigned_obj=obj; } void UnassignObject(void) { this.m_assigned_obj=NULL; } CBaseObj *GetAssignedObj(void) { return this.m_assigned_obj; } //--- Возвращает описание объекта virtual string Description(void); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_RECTANGLE_AREA); } //--- Конструкторы/деструктор CBound(void) { ::ZeroMemory(this.m_bound); } CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h); } ~CBound(void) { ::ZeroMemory(this.m_bound); } };
Если какой-то графический элемент программно удаляется, то будучи назначенным на какую-либо область в другом графическом элементе, его указатель останется прописанным для этой области. Обращение к элементу по этому указателю, приведёт к критическому завершению программы. Поэтому при удалении какого-либо объекта, необходимо открепить его от области, если он был к ней прикреплён. Запись значения NULL в указатель даст нам возможность контролировать валидность указателя на уже удалённый графический элемент.
Классы графических элементов в разрабатываемой библиотеке построены так, что если объект прикреплён к какому-либо контейнеру, то при выходе элемента за пределы своего контейнера, элемент обрезается по границам своего контейнера. Если элемент находится полностью за пределами контейнера, то он попросту скрывается. Но если идёт команда для контейнера переместить его на передний план, то контейнер автоматически в цикле переносит на передний план и все прикреплённые к нему элементы. Соответственно, скрытые элементы оказываются видимыми, так как перенос объекта на передний план — это последовательное выполнение двух команд скрыть-отобразить. Чтобы не перемещать скрытые элементы на передний план, нужно дополнить свойства графического элемента флагом, что он скрыт из-за того, что находится за пределами своего контейнера. А в методе переноса объекта на передний план проверять этот флаг.
В базовом классе холста графических элементов CCanvasBase в защищённой секции объявим такой флаг:
protected: CCanvas *m_background; // Канвас для рисования фона CCanvas *m_foreground; // Канвас для рисования переднего плана CCanvasBase *m_container; // Родительский объект-контейнер CColorElement m_color_background; // Объект управления цветом фона CColorElement m_color_foreground; // Объект управления цветом переднего плана CColorElement m_color_border; // Объект управления цветом рамки CColorElement m_color_background_act; // Объект управления цветом фона активированного элемента CColorElement m_color_foreground_act; // Объект управления цветом переднего плана активированного элемента CColorElement m_color_border_act; // Объект управления цветом рамки активированного элемента CAutoRepeat m_autorepeat; // Объект управления автоповторами событий ENUM_ELEMENT_STATE m_state; // Состояние элемента (напр., кнопки (вкл/выкл)) long m_chart_id; // Идентификатор графика int m_wnd; // Номер подокна графика int m_wnd_y; // Смещение координаты Y курсора в подокне int m_obj_x; // Координата X графического объекта int m_obj_y; // Координата Y графического объекта uchar m_alpha_bg; // Прозрачность фона uchar m_alpha_fg; // Прозрачность переднего плана uint m_border_width_lt; // Ширина рамки слева uint m_border_width_rt; // Ширина рамки справа uint m_border_width_up; // Ширина рамки сверху uint m_border_width_dn; // Ширина рамки снизу string m_program_name; // Имя программы bool m_hidden; // Флаг скрытого объекта bool m_blocked; // Флаг заблокированного элемента bool m_movable; // Флаг перемещаемого элемента bool m_resizable; // Флаг разрешения изменения размеров bool m_focused; // Флаг элемента в фокусе bool m_main; // Флаг главного объекта bool m_autorepeat_flag; // Флаг автоповтора отправки событий bool m_scroll_flag; // Флаг прокрутки содержимого при помощи скроллбаров bool m_trim_flag; // Флаг обрезки элемента по границам контейнера bool m_cropped; // Флаг того, что объект скрыт за границами контейнера int m_cursor_delta_x; // Дистанция от курсора до левого края элемента int m_cursor_delta_y; // Дистанция от курсора до верхнего края элемента int m_z_order; // Z-ордер графического объекта
В публичной секции напишем метод, возвращающий этот флаг:
public: //--- (1) Устанавливает, (2) возвращает состояние void SetState(ENUM_ELEMENT_STATE state) { this.m_state=state; this.ColorsToDefault(); } ENUM_ELEMENT_STATE State(void) const { return this.m_state; } //--- (1) Устанавливает, (2) возвращает z-ордер bool ObjectSetZOrder(const int value); int ObjectZOrder(void) const { return this.m_z_order; } //--- Возвращает (1) принадлежность объекта программе, флаг (2) скрытого, (3) заблокированного, //--- (4) перемещаемого, (5) изменяемого в размерах, (6) главного элемента, (7) в фокусе, (8, 9) имя графического объекта (фон, текст) bool IsBelongsToThis(const string name) const { return(::ObjectGetString(this.m_chart_id,name,OBJPROP_TEXT)==this.m_program_name);} bool IsHidden(void) const { return this.m_hidden; } bool IsBlocked(void) const { return this.m_blocked; } bool IsMovable(void) const { return this.m_movable; } bool IsResizable(void) const { return this.m_resizable; } bool IsMain(void) const { return this.m_main; } bool IsFocused(void) const { return this.m_focused; } bool IsAutorepeat(void) const { return this.m_autorepeat_flag; } bool IsScrollable(void) const { return this.m_scroll_flag; } bool IsTrimmed(void) const { return this.m_trim_flag; } bool IsCropped(void) const { return this.m_cropped; } string NameBG(void) const { return this.m_background.ChartObjectName(); } string NameFG(void) const { return this.m_foreground.ChartObjectName(); }
метод, устанавливающий флаг, и виртуальный метод, возвращающий флаг того, что элемент расположен полностью за пределами своего контейнера:
//--- Возврат границ объекта с учётом рамки int LimitLeft(void) const { return this.ObjectX()+(int)this.m_border_width_lt; } int LimitRight(void) const { return this.ObjectRight()-(int)this.m_border_width_rt; } int LimitTop(void) const { return this.ObjectY()+(int)this.m_border_width_up; } int LimitBottom(void) const { return this.ObjectBottom()-(int)this.m_border_width_dn; } //--- Устанавливает объекту флаг (1) перемещаемости, (2) главного объекта, (3) возможности изменения размеров, //--- (4) автоповтора событий, (5) прокрутки внутри контейнера, (6) обрезки по границам контейнера void SetMovable(const bool flag) { this.m_movable=flag; } void SetAsMain(void) { this.m_main=true; } virtual void SetResizable(const bool flag) { this.m_resizable=flag; } void SetAutorepeat(const bool flag) { this.m_autorepeat_flag=flag; } void SetScrollable(const bool flag) { this.m_scroll_flag=flag; } virtual void SetTrimmered(const bool flag) { this.m_trim_flag=flag; } void SetCropped(const bool flag) { this.m_cropped=flag; } //--- Возвращает флаг того, что объект расположен за пределами своего контейнера virtual bool IsOutOfContainer(void); //--- Ограничивает графический объект по размерам контейнера virtual bool ObjectTrim(void);
Реализация метода, возвращающего флаг того, что объект расположен за пределами своего контейнера:
//+------------------------------------------------------------------+ //| CCanvasBase::Возвращает флаг того, что объект | //| расположен за пределами своего контейнера | //+------------------------------------------------------------------+ bool CCanvasBase::IsOutOfContainer(void) { //--- Возвращаем результат проверки, что объект полностью выходит за пределы контейнера return(this.Right() <= this.ContainerLimitLeft() || this.X() >= this.ContainerLimitRight() || this.Bottom()<= this.ContainerLimitTop() || this.Y() >= this.ContainerLimitBottom()); }
Метод проверяет координаты границ объекта по отношению к границам контейнера и возвращает флаг, что элемент полностью находится за пределами своего контейнера. Для некоторых графических элементов этот метод может рассчитываться иначе. Поэтому метод объявлен виртуальным.
В методе, подрезающем графический объект по контуру контейнера, будем управлять новым флагом:
//+------------------------------------------------------------------+ //| CCanvasBase::Подрезает графический объект по контуру контейнера | //+------------------------------------------------------------------+ bool CCanvasBase::ObjectTrim() { //--- Проверяем флаг разрешения обрезки элемента и, //--- если элемент не должен обрезаться по границам контейнера - возвращаем false if(!this.m_trim_flag) return false; //--- Получаем границы контейнера int container_left = this.ContainerLimitLeft(); int container_right = this.ContainerLimitRight(); int container_top = this.ContainerLimitTop(); int container_bottom = this.ContainerLimitBottom(); //--- Получаем текущие границы объекта int object_left = this.X(); int object_right = this.Right(); int object_top = this.Y(); int object_bottom = this.Bottom(); //--- Проверяем, полностью ли объект выходит за пределы контейнера и, если да - скрываем его if(this.IsOutOfContainer()) { //--- Устанавливаем флаг, что объект за пределами контейнера this.m_cropped=true; //--- Скрываем объект и восстанавливаем его размеры this.Hide(false); if(this.ObjectResize(this.Width(),this.Height())) this.BoundResize(this.Width(),this.Height()); return true; } //--- Объект полностью или частично находится внутри видимой области контейнера else { //--- Снимаем флаг расположения объекта за пределами контейнера this.m_cropped=false; //--- Если элемент полностью внутри контейнера if(object_right<=container_right && object_left>=container_left && object_bottom<=container_bottom && object_top>=container_top) { //--- Если ширина или высота графического объекта не совпадает с шириной или высотой элемента, //--- модифицируем графический объект по размерам элемента и возвращаем true if(this.ObjectWidth()!=this.Width() || this.ObjectHeight()!=this.Height()) { if(this.ObjectResize(this.Width(),this.Height())) return true; } } //--- Если элемент частично находится в видимой области контейнера else { //--- Если элемент по вертикали полностью находится в видимой области контейнера if(object_bottom<=container_bottom && object_top>=container_top) { //--- Если высота графического объекта не совпадает с высотой элемента, //--- модифицируем графический объект по высоте элемента if(this.ObjectHeight()!=this.Height()) this.ObjectResizeH(this.Height()); } else { //--- Если элемент по горизонтали полностью находится в видимой области контейнера if(object_right<=container_right && object_left>=container_left) { //--- Если ширина графического объекта не совпадает с шириной элемента, //--- модифицируем графический объект по ширине элемента if(this.ObjectWidth()!=this.Width()) this.ObjectResizeW(this.Width()); } } } } //--- Проверяем выход объекта по горизонтали и вертикали за пределы контейнера bool modified_horizontal=false; // Флаг изменений по горизонтали bool modified_vertical =false; // Флаг изменений по вертикали //--- Обрезка по горизонтали int new_left = object_left; int new_width = this.Width(); //--- Если объект выходит за левую границу контейнера if(object_left<=container_left) { int crop_left=container_left-object_left; new_left=container_left; new_width-=crop_left; modified_horizontal=true; } //--- Если объект выходит за правую границу контейнера if(object_right>=container_right) { int crop_right=object_right-container_right; new_width-=crop_right; modified_horizontal=true; } //--- Если были изменения по горизонтали if(modified_horizontal) { this.ObjectSetX(new_left); this.ObjectResizeW(new_width); } //--- Обрезка по вертикали int new_top=object_top; int new_height=this.Height(); //--- Если объект выходит за верхнюю границу контейнера if(object_top<=container_top) { int crop_top=container_top-object_top; new_top=container_top; new_height-=crop_top; modified_vertical=true; } //--- Если объект выходит за нижнюю границу контейнера if(object_bottom>=container_bottom) { int crop_bottom=object_bottom-container_bottom; new_height-=crop_bottom; modified_vertical=true; } //--- Если были изменения по вертикали if(modified_vertical) { this.ObjectSetY(new_top); this.ObjectResizeH(new_height); } //--- После рассчётов, объект может быть скрыт, но теперь находится в области контейнера - отображаем его this.Show(false); //--- Если объект был изменен, перерисовываем его if(modified_horizontal || modified_vertical) { this.Update(false); this.Draw(false); return true; } return false; }
После такого дополнения объекты, полностью находящиеся за пределами контейнера, не будут переноситься на передний план, становясь при этом видимыми в случае, если будет послана команда переноса этого объекта на передний план, либо сразу всего контейнера со всем его содержимым.
В методе, помещающем объект на передний план, проверяем флаг, устанавливаемый в методе, рассмотренном выше:
//+------------------------------------------------------------------+ //| CCanvasBase::Помещает объект на передний план | //+------------------------------------------------------------------+ void CCanvasBase::BringToTop(const bool chart_redraw) { if(this.m_cropped) return; this.Hide(false); this.Show(chart_redraw); }
Если флаг для объекта установлен, то выводить элемент на передний план не нужно — уходим из метода.
У каждого графического элемента есть общий обработчик прокрутки колёсика мышки. Этот общий обработчик вызывает виртуальный метод обработки прокрутки колёсика. В sparam при этом передаётся значение из sparam общего обработчика. Это ошибочно, так как при этом ни один из элементов управления не может определить, что колёсико мышки прокручивается именно над ним. Выход таков: в общем обработчике нам известно имя активного элемента — того, над которым находится курсор, значит, при вызове обработчика прокрутки колёсика в sparam нужно передать имя активного элемента. А в самом обработчике проверить имя элемента и значение sparam. Если они равны, то это именно тот объект, над которым прокручивается колёсико мышки. В общем обработчике событий пропишем это:
//--- Событие прокрутки колёсика мышки if(id==CHARTEVENT_MOUSE_WHEEL) { //--- Если это активный элемент - вызываем его обработчик события прокрутки колёсика if(this.IsCurrentActiveElement()) this.OnWheelEvent(id,lparam,dparam,this.ActiveElementName()); // в sparam передаём имя активного элемента }
Проверку на равенство имени объекта и значения в sparam сделаем в обработчике, который расположен в другом файле наряду с другими доработками.
Откроем файл \MQL5\Indicators\Tables\Controls\Controls.mqh — теперь в него будем вносить доработки.
В раздел макроподстановок добавим новые определения:
//+------------------------------------------------------------------+ //| Макроподстановки | //+------------------------------------------------------------------+ #define DEF_LABEL_W 50 // Ширина текстовой метки по умолчанию #define DEF_LABEL_H 16 // Высота текстовой метки по умолчанию #define DEF_BUTTON_W 60 // Ширина кнопки по умолчанию #define DEF_BUTTON_H 16 // Высота кнопки по умолчанию #define DEF_TABLE_ROW_H 16 // Высота строки таблицы по умолчанию #define DEF_TABLE_HEADER_H 20 // Высота заголовка таблицы по умолчанию #define DEF_TABLE_COLUMN_MIN_W 12 // Минимальная ширина колонки таблицы #define DEF_PANEL_W 80 // Ширина панели по умолчанию #define DEF_PANEL_H 80 // Высота панели по умолчанию #define DEF_PANEL_MIN_W 60 // Минимальная ширина панели #define DEF_PANEL_MIN_H 60 // Минимальная высота панели #define DEF_SCROLLBAR_TH 13 // Толщина полосы прокрутки по умолчанию #define DEF_THUMB_MIN_SIZE 8 // Минимальная толщина ползунка полосы прокрутки #define DEF_AUTOREPEAT_DELAY 500 // Задержка перед запуском автоповтора #define DEF_AUTOREPEAT_INTERVAL 100 // Частота автоповторов #define DEF_HINT_NAME_TOOLTIP "HintTooltip" // Наименование подсказки "тултип" #define DEF_HINT_NAME_HORZ "HintHORZ" // Наименование подсказки "Двойная горизонтальная стрелка" #define DEF_HINT_NAME_VERT "HintVERT" // Наименование подсказки "Двойная вертикальная стрелка" #define DEF_HINT_NAME_NWSE "HintNWSE" // Наименование подсказки "Двойная стрелка сверху-лево" --- низ-право (NorthWest-SouthEast) #define DEF_HINT_NAME_NESW "HintNESW" // Наименование подсказки "Двойная стрелка снизу-лево" --- верх-право (NorthEast-SouthWest) #define DEF_HINT_NAME_SHIFT_HORZ "HintShiftHORZ" // Наименование подсказки "Стрелка горизонтального смещения" #define DEF_HINT_NAME_SHIFT_VERT "HintShiftVERT" // Наименование подсказки "Стрелка вертикального смещения"
Ширина столбца таблицы не может быть менее 12 пикселей — чтобы при уменьшении размеров столбцы не были слишком узкими. Наименования подсказок удобнее задать директивой компилятора и подставлять в качестве имени, так как при необходимости изменения имени подсказки, мы изменяем только директиву, и нет необходимости искать и изменять все вхождения этого имени в разных местах кода. У нас появились два имени для двух новых подсказок — это будут подсказки, появляющиеся при наведении курсора на грань объекта, за которую можно "потянуть" для изменения размеров объекта.
Добавим новое перечисление режимов сортировки столбцов и новые константы перечислений:
//+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_SORT_BY // Сравниваемые свойства { ELEMENT_SORT_BY_ID = BASE_SORT_BY_ID, // Сравнение по идентификатору элемента ELEMENT_SORT_BY_NAME = BASE_SORT_BY_NAME, // Сравнение по наименованию элемента ELEMENT_SORT_BY_X = BASE_SORT_BY_X, // Сравнение по координате X элемента ELEMENT_SORT_BY_Y = BASE_SORT_BY_Y, // Сравнение по координате Y элемента ELEMENT_SORT_BY_WIDTH= BASE_SORT_BY_WIDTH, // Сравнение по ширине элемента ELEMENT_SORT_BY_HEIGHT= BASE_SORT_BY_HEIGHT, // Сравнение по высоте элемента ELEMENT_SORT_BY_ZORDER= BASE_SORT_BY_ZORDER, // Сравнение по Z-order элемента ELEMENT_SORT_BY_TEXT, // Сравнение по тексту элемента ELEMENT_SORT_BY_COLOR_BG, // Сравнение по цвету фона элемента ELEMENT_SORT_BY_ALPHA_BG, // Сравнение по прозрачности фона элемента ELEMENT_SORT_BY_COLOR_FG, // Сравнение по цвету переднего плана элемента ELEMENT_SORT_BY_ALPHA_FG, // Сравнение по прозрачности переднего плана элемента ELEMENT_SORT_BY_STATE, // Сравнение по состоянию элемента ELEMENT_SORT_BY_GROUP, // Сравнение по группе элемента }; enum ENUM_TABLE_SORT_MODE // Режимы сортировки столбцов таблиц { TABLE_SORT_MODE_NONE, // Сортировка отсутствует TABLE_SORT_MODE_ASC, // Сортировка по возрастанию TABLE_SORT_MODE_DESC, // Сортировка по убыванию }; enum ENUM_HINT_TYPE // Типы подсказок { HINT_TYPE_TOOLTIP, // Тултип HINT_TYPE_ARROW_HORZ, // Двойная горизонтальная стрелка HINT_TYPE_ARROW_VERT, // Двойная вертикальная стрелка HINT_TYPE_ARROW_NWSE, // Двойная стрелка сверху-лево --- низ-право (NorthWest-SouthEast) HINT_TYPE_ARROW_NESW, // Двойная стрелка снизу-лево --- верх-право (NorthEast-SouthWest) HINT_TYPE_ARROW_SHIFT_HORZ, // Стрелка горизонтального смещения HINT_TYPE_ARROW_SHIFT_VERT, // Стрелка вертикального смещения };
В классе рисования изображений CImagePainter объявим два новых метода, рисующих стрелки смещения по горизонтали и вертикали:
//--- Очищает область bool Clear(const int x,const int y,const int w,const int h,const bool update=true); //--- Рисует закрашенную стрелку (1) вверх, (2) вниз, (3) влево, (4) вправо bool ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует (1) горизонтальную 17х7, (2) вертикальную 7х17 двойную стрелку bool ArrowHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует диагональную (1) сверху-слева --- вниз-вправо, (2) снизу-слева --- вверх-вправо 17х17 двойную стрелку bool ArrowNWSE(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowNESW(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует стрелку смещения 18x18 по (1) горизонтали, (2) вертикали bool ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует (1) отмеченный, (2) неотмеченный CheckBox bool CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует (1) отмеченный, (2) неотмеченный RadioButton bool CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
За пределами тела класса напишем реализацию объявленных методов.
Метод, рисующий стрелку 18х18 горизонтального смещения:
//+------------------------------------------------------------------+ //| CImagePainter::Рисует стрелку 18х18 горизонтального смещения | //+------------------------------------------------------------------+ bool CImagePainter::ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true) { //--- Если область изображения не валидна - возвращаем false if(!this.CheckBound(__FUNCTION__)) return false; //--- Координаты фигуры int arrx[25]={0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7, 7, 4, 4, 3, 0, 0}; int arry[25]={8, 5, 5, 7, 7, 0, 0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8}; //--- Рисуем белую подложку this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha)); //--- Рисуем линию стрелок this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha)); //--- Рисуем разделительную линию this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha)); //--- Рисуем левый треугольник this.m_canvas.Line(2,7, 2,10,::ColorToARGB(clr,alpha)); this.m_canvas.Line(3,6, 3,11,::ColorToARGB(clr,alpha)); //--- Рисуем правый треугольник this.m_canvas.Line(14,6, 14,11,::ColorToARGB(clr,alpha)); this.m_canvas.Line(15,7, 15,10,::ColorToARGB(clr,alpha)); if(update) this.m_canvas.Update(false); return true; }
Метод, рисующий стрелку 18х18 вертикального смещения:
//+------------------------------------------------------------------+ //| CImagePainter::Рисует стрелку 18х18 вертикального смещения | //+------------------------------------------------------------------+ bool CImagePainter::ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true) { //--- Если область изображения не валидна - возвращаем false if(!this.CheckBound(__FUNCTION__)) return false; //--- Координаты фигуры int arrx[25]={0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8, 5, 5, 7, 7, 0, 0}; int arry[25]={7, 7, 4, 4, 3, 0, 0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7}; //--- Рисуем белую подложку this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha)); //--- Рисуем разделительную линию this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha)); //--- Рисуем линию стрелок this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha)); //--- Рисуем верхний треугольник this.m_canvas.Line(7,2, 10,2,::ColorToARGB(clr,alpha)); this.m_canvas.Line(6,3, 11,3,::ColorToARGB(clr,alpha)); //--- Рисуем нижний треугольник this.m_canvas.Line(6,14, 11,14,::ColorToARGB(clr,alpha)); this.m_canvas.Line(7,15, 10,15,::ColorToARGB(clr,alpha)); if(update) this.m_canvas.Update(false); return true; }
Оба метода рисуют подсказки со стрелками для горизонтального (
) и вертикального (
) смещения грани элемента для изменения его размеров.
В базовом классе графического элемента CElementBase два метода AddHintsArrowed и ShowCursorHint сделаем виртуальными:
//--- Добавляет существующий объект-подсказку в список CVisualHint *AddHint(CVisualHint *obj, const int dx, const int dy); //--- (1) Добавляет в список, (2) удаляет из списка объекты-подсказки со стрелками virtual bool AddHintsArrowed(void); bool DeleteHintsArrowed(void); //--- Отображает курсор изменения размеров virtual bool ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y); //--- Обработчик перетаскивания граней и углов элемента virtual void ResizeActionDragHandler(const int x, const int y);
В деструкторе класса очистим список подсказок:
//--- Конструкторы/деструктор CElementBase(void) { this.m_painter.CanvasAssign(this.GetForeground()); this.m_visible_in_container=true; } CElementBase(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CElementBase(void) { this.m_list_hints.Clear(); }
При добавлении объектов в списки, сначала производится поиск точно такого же объекта в списке. При этом, для правильного поиска, списку устанавливается сортировка по сравниваемому свойству. Но изначально список может быть отсортирован по другому свойству. Чтобы не нарушать изначальную сортировку списков, будем запоминать её, затем устанавливать нужную для поиска сортировку, а по окончании — возвращать обратно ранее запомненную.
В методе добавления указанной подсказки в список сделаем такую доработку:
//+------------------------------------------------------------------+ //| CElementBase::Добавляет указанный объект-подсказку в список | //+------------------------------------------------------------------+ bool CElementBase::AddHintToList(CVisualHint *obj) { //--- Если передан пустой указатель - сообщаем об этом и возвращаем false if(obj==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return false; } //--- Запоминаем метод сортировки списка int sort_mode=this.m_list_hints.SortMode(); //--- Устанавливаем списку флаг сортировки по идентификатору this.m_list_hints.Sort(ELEMENT_SORT_BY_ID); //--- Если такого элемента нет в списке, if(this.m_list_hints.Search(obj)==NULL) { //--- возвращаем списку изначальную сортировку и возвращаем результат его добавления в список this.m_list_hints.Sort(sort_mode); return(this.m_list_hints.Add(obj)>-1); } //--- Возвращаем списку изначальную сортировку this.m_list_hints.Sort(sort_mode); //--- Элемент с таким идентификатором уже есть в списке - возвращаем false return false; }
В методах, работающих с именами объектов-подсказок, теперь будем использовать имена объектов, ранее указанными директивами:
//+------------------------------------------------------------------+ //| CElementBase::Добавляет в список объекты-подсказки со стрелками | //+------------------------------------------------------------------+ bool CElementBase::AddHintsArrowed(void) { //--- Массивы наименований и типов подсказок string array[4]={DEF_HINT_NAME_HORZ,DEF_HINT_NAME_VERT,DEF_HINT_NAME_NWSE,DEF_HINT_NAME_NESW}; ENUM_HINT_TYPE type[4]={HINT_TYPE_ARROW_HORZ,HINT_TYPE_ARROW_VERT,HINT_TYPE_ARROW_NWSE,HINT_TYPE_ARROW_NESW}; //--- В цикле создаём четыре подсказки со стрелками bool res=true; for(int i=0;i<(int)array.Size();i++) res &=(this.CreateAndAddNewHint(type[i],array[i],0,0)!=NULL); //--- Если были ошибки при создании - возвращаем false if(!res) return false; //--- В цикле по массиву наименований объектов-подсказок for(int i=0;i<(int)array.Size();i++) { //--- получаем очередной объект по наименованию, CVisualHint *obj=this.GetHint(array[i]); if(obj==NULL) continue; //--- скрываем объект и рисуем внешний вид (стрелки в соответствии с типом объекта) obj.Hide(false); obj.Draw(false); } //--- Всё успешно return true; }
...
//+------------------------------------------------------------------+ //| CElementBase::Отображает курсор изменения размеров | //+------------------------------------------------------------------+ bool CElementBase::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y) { CVisualHint *hint=NULL; // Указатель на подсказку int hint_shift_x=0; // Смещение подсказки по X int hint_shift_y=0; // Смещение подсказки по Y //--- В зависимости от расположения курсора на границах элемента //--- указываем смещения подсказки относительно координат курсора, //--- отображаем на графике требуемую подсказку и получаем указатель на этот объект switch(edge) { //--- Курсор на правой или левой границе - горизонтальная двойная стрелка case CURSOR_REGION_RIGHT : case CURSOR_REGION_LEFT : hint_shift_x=1; hint_shift_y=18; this.ShowHintArrowed(HINT_TYPE_ARROW_HORZ,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_HORZ); break; //--- Курсор на верхней или нижней границе - вертикальная двойная стрелка case CURSOR_REGION_TOP : case CURSOR_REGION_BOTTOM : hint_shift_x=12; hint_shift_y=4; this.ShowHintArrowed(HINT_TYPE_ARROW_VERT,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_VERT); break; //--- Курсор в левом верхнем или правом нижнем углу - диагональная двойная стрелка от лево-верх до право-низ case CURSOR_REGION_LEFT_TOP : case CURSOR_REGION_RIGHT_BOTTOM : hint_shift_x=10; hint_shift_y=2; this.ShowHintArrowed(HINT_TYPE_ARROW_NWSE,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_NWSE); break; //--- Курсор в левом нижнем или правом верхнем углу - диагональная двойная стрелка от лево-низ до право-верх case CURSOR_REGION_LEFT_BOTTOM : case CURSOR_REGION_RIGHT_TOP : hint_shift_x=5; hint_shift_y=12; this.ShowHintArrowed(HINT_TYPE_ARROW_NESW,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_NESW); break; //--- По умолчанию ничего не делаем default: break; } //--- Возвращаем результат корректировки положения подсказки относительно курсора return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false); }
и т.д.
В классе объекта-подсказки объявим два новых метода, рисующих две новые подсказки:
//+------------------------------------------------------------------+ //| Класс подсказки | //+------------------------------------------------------------------+ class CVisualHint : public CButton { protected: ENUM_HINT_TYPE m_hint_type; // Тип подсказки //--- Рисует (1) тултип, (2) горизонтальную, (3) вертикальную стрелку, //--- стрелки (4) сверху-лево --- низ-право, (5) снизу-лево --- верх-право, //--- стрелки смещения по (6) горизонтали, (7) вертикали void DrawTooltip(void); void DrawArrHorz(void); void DrawArrVert(void); void DrawArrNWSE(void); void DrawArrNESW(void); void DrawArrShiftHorz(void); void DrawArrShiftVert(void); //--- Инициализация цветов для типа подсказки (1) Tooltip, (2) стрелки void InitColorsTooltip(void); void InitColorsArrowed(void); public: //--- (1) Устанавливает, (2) возвращает тип подсказки void SetHintType(const ENUM_HINT_TYPE type); ENUM_HINT_TYPE HintType(void) const { return this.m_hint_type; } //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle) { return CButton::Save(file_handle); } virtual bool Load(const int file_handle) { return CButton::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_HINT); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); virtual void InitColors(void); //--- Конструкторы/деструктор CVisualHint(void); CVisualHint(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CVisualHint (void) {} };
За пределами тела класса напишем их реализацию:
//+------------------------------------------------------------------+ //| CVisualHint::Рисует стрелки смещения по горизонтали | //+------------------------------------------------------------------+ void CVisualHint::DrawArrShiftHorz(void) { //--- Очищаем область рисунка this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Рисуем стрелки смещения по горизонтали this.m_painter.ArrowShiftHorz(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true); } //+------------------------------------------------------------------+ //| CVisualHint::Рисует стрелки смещения по вертикали | //+------------------------------------------------------------------+ void CVisualHint::DrawArrShiftVert(void) { //--- Очищаем область рисунка this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Рисуем стрелки смещения по горизонтали this.m_painter.ArrowShiftVert(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true); }
Добавим обработку новых методов рисования подсказок в методы рисования и установки типа подсказки:
//+------------------------------------------------------------------+ //| CVisualHint::Устанавливает тип подсказки | //+------------------------------------------------------------------+ void CVisualHint::SetHintType(const ENUM_HINT_TYPE type) { //--- Если переданный тип соответствует установленному - уходим if(this.m_hint_type==type) return; //--- Устанавливаем новый тип подсказки this.m_hint_type=type; //--- В зависимости от типа подсказки устанавливаем размеры объекта switch(this.m_hint_type) { case HINT_TYPE_ARROW_HORZ : this.Resize(17,7); break; case HINT_TYPE_ARROW_VERT : this.Resize(7,17); break; case HINT_TYPE_ARROW_NESW : case HINT_TYPE_ARROW_NWSE : this.Resize(13,13); break; case HINT_TYPE_ARROW_SHIFT_HORZ : case HINT_TYPE_ARROW_SHIFT_VERT : this.Resize(18,18); break; default : break; } //--- Устанавливаем смещение и размеры области изображенеия, //--- инициализируем цвета по типу подсказки this.SetImageBound(0,0,this.Width(),this.Height()); this.InitColors(); } //+------------------------------------------------------------------+ //| CVisualHint::Рисует внешний вид | //+------------------------------------------------------------------+ void CVisualHint::Draw(const bool chart_redraw) { //--- В зависимости от типа подсказки вызываем соответствующий метод рисования switch(this.m_hint_type) { case HINT_TYPE_ARROW_HORZ : this.DrawArrHorz(); break; case HINT_TYPE_ARROW_VERT : this.DrawArrVert(); break; case HINT_TYPE_ARROW_NESW : this.DrawArrNESW(); break; case HINT_TYPE_ARROW_NWSE : this.DrawArrNWSE(); break; case HINT_TYPE_ARROW_SHIFT_HORZ : this.DrawArrShiftHorz(); break; case HINT_TYPE_ARROW_SHIFT_VERT : this.DrawArrShiftVert(); break; default : this.DrawTooltip(); break; } //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Доработаем класс панели CPanel.
Добавим вспомогательные методы для работы со списками прикреплённых элементов:
//+------------------------------------------------------------------+ //| Класс панели | //+------------------------------------------------------------------+ class CPanel : public CLabel { private: CElementBase m_temp_elm; // Временный объект для поиска элементов CBound m_temp_bound; // Временный объект для поиска областей protected: CListElm m_list_elm; // Список прикреплённых элементов CListElm m_list_bounds; // Список областей //--- Добавляет новый элемент в список bool AddNewElement(CElementBase *element); public: //--- Возвращает указатель на список (1) прикреплённых элементов, (2) областей CListElm *GetListAttachedElements(void) { return &this.m_list_elm; } CListElm *GetListBounds(void) { return &this.m_list_bounds; } //--- Возвращает прикреплённый элемент по (1) индексу в списке, (2) идентификатору, (3) назначенному имени объекта CElementBase *GetAttachedElementAt(const uint index) { return this.m_list_elm.GetNodeAtIndex(index); } CElementBase *GetAttachedElementByID(const int id); CElementBase *GetAttachedElementByName(const string name); //--- Возвращает количество присоединённых элементов int AttachedElementsTotal(void) const { return this.m_list_elm.Total(); } //--- Возвращает область по (1) индексу в списке, (2) идентификатору, (3) назначенному имени области CBound *GetBoundAt(const uint index) { return this.m_list_bounds.GetNodeAtIndex(index); } CBound *GetBoundByID(const int id); CBound *GetBoundByName(const string name); //--- Создаёт и добавляет (1) новый, (2) ранее созданный элемент в список virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h); virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy); //--- Удаляет указанный элемент bool DeleteElement(const int index) { return this.m_list_elm.Delete(index); } //--- (1) Создаёт и добавляет в список новую область, (2) удаляет указанную область CBound *InsertNewBound(const string name,const int dx,const int dy,const int w,const int h); bool DeleteBound(const int index) { return this.m_list_bounds.Delete(index); } //--- (1) Назначает объект на указанную область, (2) отменяет назначение объекта с указанной области bool AssignObjectToBound(const int bound, CBaseObj *object); bool UnassignObjectFromBound(const int bound); //--- Изменяет размеры объекта virtual bool ResizeW(const int w); virtual bool ResizeH(const int h); virtual bool Resize(const int w,const int h); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_PANEL); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); virtual void InitColors(void); //--- Устанавливает объекту новые координаты XY virtual bool Move(const int x,const int y); //--- Смещает объект по осям XY на указанное смещение virtual bool Shift(const int dx,const int dy); //--- Устанавливает одновременно координаты и размеры элемента virtual bool MoveXYWidthResize(const int x,const int y,const int w,const int h); //--- (1) Скрывает (2) отображает объект на всех периодах графика, //--- (3) помещает объект на передний план, (4) блокирует, (5) разблокирует элемент, virtual void Hide(const bool chart_redraw); virtual void Show(const bool chart_redraw); virtual void BringToTop(const bool chart_redraw); virtual void Block(const bool chart_redraw); virtual void Unblock(const bool chart_redraw); //--- Выводит в журнал описание объекта virtual void Print(void); //--- Распечатывает список (1) присоединённых объектов, (2) областей void PrintAttached(const uint tab=3); void PrintBounds(void); //--- Обработчик событий virtual void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Обработчик события таймера virtual void TimerEventHandler(void); //--- Конструкторы/деструктор CPanel(void); CPanel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CPanel (void) { this.m_list_elm.Clear(); this.m_list_bounds.Clear(); } };
В методе изменения ширины элемента ResizeW() есть потенциальная ошибка:
//--- Получаем указатель на базовый элемент и, если он есть, и его тип - контейнер, //--- проверяем отношение размеров текущего элемента относительно размеров контейнера //--- для отображения полос прокрутки в контейнере при необходимости CContainer *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) base.CheckElementSizes(&this);
В случае, если контейнером, получаемым методом GetContainer(), не является класс CContainer, то программа завершится по критической ошибке из-за невозможности преобразования типов объектов.
Исправим ситуацию:
//+------------------------------------------------------------------+ //| CPanel::Изменяет ширину объекта | //+------------------------------------------------------------------+ bool CPanel::ResizeW(const int w) { if(!this.ObjectResizeW(w)) return false; this.BoundResizeW(w); this.SetImageSize(w,this.Height()); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } //--- Получаем указатель на базовый элемент и, если он есть, и его тип - контейнер, //--- проверяем отношение размеров текущего элемента относительно размеров контейнера //--- для отображения полос прокрутки в контейнере при необходимости CContainer *container=NULL; CCanvasBase *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) { container=base; container.CheckElementSizes(&this); } //--- В цикле по присоединённым элементам обрезаем каждый элемент по границам контейнера int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.ObjectTrim(); } //--- Всё успешно return true; }
Теперь мы можем безошибочно назначить указатель на правильный тип объекта.
В методе добавления нового элемента в список, запомним изначальную сортировку списка, и вернём её после добавления объекта в список:
//+------------------------------------------------------------------+ //| CPanel::Добавляет новый элемент в список | //+------------------------------------------------------------------+ bool CPanel::AddNewElement(CElementBase *element) { //--- Если передан пустой указатель - сообщаем об этом и возвращаем false if(element==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return false; } //--- Запоминаем метод сортировки списка int sort_mode=this.m_list_elm.SortMode(); //--- Устанавливаем списку флаг сортировки по идентификатору this.m_list_elm.Sort(ELEMENT_SORT_BY_ID); //--- Если такого элемента нет в списке, if(this.m_list_elm.Search(element)==NULL) { //--- возвращаем списку изначальную сортировку и возвращаем результат его добавления в список this.m_list_elm.Sort(sort_mode); return(this.m_list_elm.Add(element)>-1); } //--- Возвращаем списку изначальную сортировку this.m_list_elm.Sort(sort_mode); //--- Элемент с таким идентификатором уже есть в списке - возвращаем false return false; }
Доработаем метод, создающий и добавляющий в список новую область:
//+------------------------------------------------------------------+ //| Создаёт и добавляет в список новую область | //+------------------------------------------------------------------+ CBound *CPanel::InsertNewBound(const string name,const int dx,const int dy,const int w,const int h) { //--- Проверяем есть ли в списке область с указанным именем и, если да - сообщаем об этом и возвращаем NULL this.m_temp_bound.SetName(name); //--- Запоминаем метод сортировки списка int sort_mode=this.m_list_bounds.SortMode(); //--- Устанавливаем списку флаг сортировки по наименованию this.m_list_bounds.Sort(ELEMENT_SORT_BY_NAME); if(this.m_list_bounds.Search(&this.m_temp_bound)!=NULL) { //--- Возвращаем списку изначальную сортировку, сообщаем, что такой объект уже существует и возвращаем NULL this.m_list_bounds.Sort(sort_mode); ::PrintFormat("%s: Error. An area named \"%s\" is already in the list",__FUNCTION__,name); return NULL; } //--- Возвращаем списку изначальную сортировку this.m_list_bounds.Sort(sort_mode); //--- Создаём новый объект-область; при неудаче - сообщаем об этом и возвращаем NULL CBound *bound=new CBound(dx,dy,w,h); if(bound==NULL) { ::PrintFormat("%s: Error. Failed to create CBound object",__FUNCTION__); return NULL; } //--- Устанавливаем имя области и идентификатор, и возвращаем указатель на объект bound.SetName(name); bound.SetID(this.m_list_bounds.Total()); //--- Если новый объект не удалось добавить в список - сообщаем об этом, удаляем объект и возвращаем NULL if(this.m_list_bounds.Add(bound)==-1) { ::PrintFormat("%s: Error. Failed to add CBound object to list",__FUNCTION__); delete bound; return NULL; } return bound; }
В классе есть два метода, которые были объявлены, но не были реализованы. Исправим.
Метод, возвращающий область по идентификатору:
//+------------------------------------------------------------------+ //| CPanel::Возвращает область по идентификатору | //+------------------------------------------------------------------+ CBound *CPanel::GetBoundByID(const int id) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { CBound *bound=this.GetBoundAt(i); if(bound!=NULL && bound.ID()==id) return bound; } return NULL; }
В простом цикле по объектам областей элемента ищем область с указанным идентификатором и возвращаем указатель на найденный объект.
Метод, возвращающий область по назначенному имени области:
//+------------------------------------------------------------------+ //| CPanel::Возвращает область по назначенному имени области | //+------------------------------------------------------------------+ CBound *CPanel::GetBoundByName(const string name) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { CBound *bound=this.GetBoundAt(i); if(bound!=NULL && bound.Name()==name) return bound; } return NULL; }
В простом цикле по объектам областей элемента ищем область с указанным именем и возвращаем указатель на найденный объект.
Напишем реализацию двух объявленных методов.
Метод, назначающий объект на указанную область:
//+------------------------------------------------------------------+ //| CPanel::Назначает объект на указанную область | //+------------------------------------------------------------------+ bool CPanel::AssignObjectToBound(const int bound,CBaseObj *object) { CBound *bound_obj=this.GetBoundAt(bound); if(bound_obj==NULL) { ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound); return false; } bound_obj.AssignObject(object); return true; }
Получаем область по идентификатору и вызываем метод объекта области, назначающий объект на область.
Метод, отменяющий назначение объекта с указанной области:
//+------------------------------------------------------------------+ //| CPanel::Отменяет назначение объекта с указанной области | //+------------------------------------------------------------------+ bool CPanel::UnassignObjectFromBound(const int bound) { CBound *bound_obj=this.GetBoundAt(bound); if(bound_obj==NULL) { ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound); return false; } bound_obj.UnassignObject(); return true; }
Получаем область по идентификатору и вызываем метод объекта области, отменяющий ранее назначенный объект на область.
В классах полос прокрутки — горизонтальной и вертикальной, есть недочёт, приводящий к тому, что при прокрутке ползунка могут смещаться сразу несколько полос прокрутки, если на графике находится не один контейнер. Для исправления такого поведения, нужно считывать из параметра sparam имя активного элемента и сравнивать с именем ползунка. Если в составе имени ползунка есть подстрока с именем активного элемента, то именно этот ползунок прокручивается. Внесём правки в обработчики прокрутки колёсика мышки в оба класса ползунков полос прокрутки:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Обработчик прокрутки колёсика | //+------------------------------------------------------------------+ void CScrollBarThumbH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Получаем указатель на базовый объект (элемент управления "горизонтальная полоса прогрутки") CCanvasBase *base_obj=this.GetContainer(); //--- Получаем имя главного объекта в иерархии по значению в sparam string array_names[]; string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : ""); //--- Если главный объект в иерархии не наш - уходим if(::StringFind(this.NameFG(),name_main)!=0) return; //--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим if(!this.IsMovable() || base_obj==NULL) return; //--- Получаем ширину базового объекта и рассчитываем границы пространства для ползунка int base_w=base_obj.Width(); int base_left=base_obj.X()+base_obj.Height(); int base_right=base_obj.Right()-base_obj.Height()+1; //--- Задаём направление смещения в зависимости от направления вращения колёсика мышки int dx=(dparam<0 ? 2 : dparam>0 ? -2 : 0); if(dx==0) dx=(int)lparam; //--- Если при смещении ползунок выйдет за левый край своей области - устанавливаем его на левый край if(dx<0 && this.X()+dx<=base_left) this.MoveX(base_left); //--- иначе, если при смещении ползунок выйдет за правый край своей области - позиционируем его по правому краю else if(dx>0 && this.Right()+dx>=base_right) this.MoveX(base_right-this.Width()); //--- Иначе, если ползунок в пределах своей области - смещаем его на величину смещения else { this.ShiftX(dx); } //--- Рассчитываем позицию ползунка int thumb_pos=this.X()-base_left; //--- Получаем координаты курсора int x=CCommonManager::GetInstance().CursorX(); int y=CCommonManager::GetInstance().CursorY(); //--- Если курсор попадает на ползунок - меняем цвет на "В фокусе", if(this.Contains(x,y)) this.OnFocusEvent(id,lparam,dparam,sparam); //--- иначе - возвращаем цвет на "По умолчанию" else this.OnReleaseEvent(id,lparam,dparam,sparam); //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG()); //--- Перерисовываем график if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); }
//+------------------------------------------------------------------+ //| CScrollBarThumbV::Обработчик прокрутки колёсика | //+------------------------------------------------------------------+ void CScrollBarThumbV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Получаем указатель на базовый объект (элемент управления "вертикальная полоса прогрутки") CCanvasBase *base_obj=this.GetContainer(); //--- Получаем имя главного объекта в иерархии по значению в sparam string array_names[]; string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : ""); //--- Если главный объект в иерархии не наш - уходим if(::StringFind(this.NameFG(),name_main)!=0) return; //--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим if(!this.IsMovable() || base_obj==NULL) return; //--- Получаем высоту базового объекта и рассчитываем границы пространства для ползунка int base_h=base_obj.Height(); int base_top=base_obj.Y()+base_obj.Width(); int base_bottom=base_obj.Bottom()-base_obj.Width()+1; //--- Задаём направление смещения в зависимости от направления вращения колёсика мышки int dy=(dparam<0 ? 2 : dparam>0 ? -2 : 0); if(dy==0) dy=(int)lparam; //--- Если при смещении ползунок выйдет за верхний край своей области - устанавливаем его на верхний край if(dy<0 && this.Y()+dy<=base_top) this.MoveY(base_top); //--- иначе, если при смещении ползунок выйдет за нижний край своей области - позиционируем его по нижнему краю else if(dy>0 && this.Bottom()+dy>=base_bottom) this.MoveY(base_bottom-this.Height()); //--- Иначе, если ползунок в пределах своей области - смещаем его на величину смещения else { this.ShiftY(dy); } //--- Рассчитываем позицию ползунка int thumb_pos=this.Y()-base_top; //--- Получаем координаты курсора int x=CCommonManager::GetInstance().CursorX(); int y=CCommonManager::GetInstance().CursorY(); //--- Если курсор попадает на ползунок - меняем цвет на "В фокусе", if(this.Contains(x,y)) this.OnFocusEvent(id,lparam,dparam,sparam); //--- иначе - возвращаем цвет на "По умолчанию" else this.OnReleaseEvent(id,lparam,dparam,sparam); //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG()); //--- Перерисовываем график if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); }
В классе контейнера CContainer в методе смещения содержимого контейнера по горизонтали, важно учитывать, что для таблицы, при её прокрутке по горизонтали, также необходимо прокручивать и заголовок таблицы. Реализуем это:
//+-------------------------------------------------------------------+ //|CContainer::Смещает содержимое по горизонтали на указанное значение| //+-------------------------------------------------------------------+ bool CContainer::ContentShiftHorz(const int value) { //--- Получаем указатель на содержимое контейнера CElementBase *elm=this.GetAttachedElement(); if(elm==NULL) return false; //--- Для элемента CTableView получаем заголовок таблицы CElementBase *elm_container=elm.GetContainer(); CTableHeaderView *table_header=NULL; if(elm_container!=NULL && ::StringFind(elm.Name(),"Table")==0) { CElementBase *obj=elm_container.GetContainer(); if(obj!=NULL && obj.Type()==ELEMENT_TYPE_TABLE_VIEW) { CTableView *table_view=obj; table_header=table_view.GetHeader(); } } //--- Рассчитываем величину смещения по положению ползунка int content_offset=this.CalculateContentOffsetHorz(value); //--- Сдвигаем заголовок bool res=true; if(table_header!=NULL) { res &=table_header.MoveX(this.X()-content_offset); } //--- Возвращаем результат сдвига содержимого на рассчитанную величину res &=elm.MoveX(this.X()-content_offset); return res; }
В методе, возвращающем тип элемента, отправившего событие, в классе объекта контейнера был неправильно выполнен поиск базового элемента:
//--- Получаем имена всех элементов в иерархии (при ошибке - возвращаем -1) string names[]={}; int total = GetElementNames(name,"_",names); if(total==WRONG_VALUE) return WRONG_VALUE; //--- Если имя базового элемента в иерархии не совпадает с именем контейнера, то это не наше событие - уходим string base_name=names[0]; if(base_name!=this.NameFG()) return WRONG_VALUE;
Не всегда базовый объект находится самым первым в иерархии графических элементов контейнера. Могут быть ситуации многократного вложения одного элемента в другой, и тогда для последних элементов в иерархии их базовый объект не будет первым в списке имён всех вложенных элементов контейнера. Выполним поиск базового объекта правильно:
//+------------------------------------------------------------------+ //| Возвращает тип элемента, отправившего событие | //+------------------------------------------------------------------+ ENUM_ELEMENT_TYPE CContainer::GetEventElementType(const string name) { //--- Получаем имена всех элементов в иерархии (при ошибке - возвращаем -1) string names[]={}; int total = GetElementNames(name,"_",names); if(total==WRONG_VALUE) return WRONG_VALUE; //--- Найдём в массиве наименование контейнера, ближайшее к имени элемента с событием int cntr_index=-1; // Индекс наименования контейнера в массиве имён в иерархии элементов string cntr_name=""; // Наименование контейнера в массиве имён в иерархии элементов //--- Ищем в цикле самое первое с конца вхождение подстроки "CNTR" for(int i=total-1;i>=0;i--) { if(::StringFind(names[i],"CNTR")==0) { cntr_name=names[i]; cntr_index=i; break; } } //--- Если наименование контейнера не найдено в массиве (индекс равен -1) - возвращаем -1 if(cntr_index==WRONG_VALUE) return WRONG_VALUE; //--- Если в имени элемента нет подстроки с именем базового элемента, то это не наше событие - уходим string base_name=names[cntr_index]; if(::StringFind(this.NameFG(),base_name)==WRONG_VALUE) return WRONG_VALUE; //--- События, пришедшие не от скроллбаров, пропускаем string check_name=::StringSubstr(names[cntr_index+1],0,4); if(check_name!="SCBH" && check_name!="SCBV") return WRONG_VALUE; //--- Получаем имя элемента, от которого пришло событие и инициализируем тип элемента string elm_name=names[names.Size()-1]; ENUM_ELEMENT_TYPE type=WRONG_VALUE; //--- Проверяем и записываем тип элемента //--- Кнопка со стрелкой вверх if(::StringFind(elm_name,"BTARU")==0) type=ELEMENT_TYPE_BUTTON_ARROW_UP; //--- Кнопка со стрелкой вниз else if(::StringFind(elm_name,"BTARD")==0) type=ELEMENT_TYPE_BUTTON_ARROW_DOWN; //--- Кнопка со стрелкой влево else if(::StringFind(elm_name,"BTARL")==0) type=ELEMENT_TYPE_BUTTON_ARROW_LEFT; //--- Кнопка со стрелкой вправо else if(::StringFind(elm_name,"BTARR")==0) type=ELEMENT_TYPE_BUTTON_ARROW_RIGHT; //--- Ползунок горизонтальной полосы прокрутки else if(::StringFind(elm_name,"THMBH")==0) type=ELEMENT_TYPE_SCROLLBAR_THUMB_H; //--- Ползунок вертикальной полосы прокрутки else if(::StringFind(elm_name,"THMBV")==0) type=ELEMENT_TYPE_SCROLLBAR_THUMB_V; //--- Элемент управления ScrollBarHorisontal else if(::StringFind(elm_name,"SCBH")==0) type=ELEMENT_TYPE_SCROLLBAR_H; //--- Элемент управления ScrollBarVertical else if(::StringFind(elm_name,"SCBV")==0) type=ELEMENT_TYPE_SCROLLBAR_V; //--- Возвращаем тип элемента return type; }
Теперь доработаем класс визуального представления ячейки таблицы CTableCellView. Доработки коснутся ограничения отрисовки ячейки — рисовать ячейку, которая находится за пределами своего контейнера, не нужно, чтобы не тратить впустую ресурсы на рисование за пределами графического объекта. Также дадим возможность изменения цвета выводимого в ячейке текста и исправим вывод текста в ячейку при различных типах точки привязки текста.
Объявим новые переменные и методы:
//+------------------------------------------------------------------+ //| Класс визуального представления ячейки таблицы | //+------------------------------------------------------------------+ class CTableCellView : public CBoundedObj { protected: CTableCell *m_table_cell_model; // Указатель на модель ячейки CImagePainter *m_painter; // Указатель на объект рисования CTableRowView *m_element_base; // Указатель на базовый элемент (строка таблицы) CCanvas *m_background; // Указатель на канвас фона CCanvas *m_foreground; // Указатель на канвас переднего плана int m_index; // Индекс в списке ячеек ENUM_ANCHOR_POINT m_text_anchor; // Точка привязки текста (выравнивание в ячейке) int m_text_x; // Координата X текста (смещение относительно левой границы области объекта) int m_text_y; // Координата Y текста (смещение относительно верхней границы области объекта) ushort m_text[]; // Текст color m_fore_color; // Цвет переднего плана //--- Возвращает смещения начальных координат рисования на холсте относительно канваса и координат базового элемента int CanvasOffsetX(void) const { return(this.m_element_base.ObjectX()-this.m_element_base.X()); } int CanvasOffsetY(void) const { return(this.m_element_base.ObjectY()-this.m_element_base.Y()); } //--- Возвращает скорректированную координату точки на холсте с учётом смещения холста относительно базового элемента int AdjX(const int x) const { return(x-this.CanvasOffsetX()); } int AdjY(const int y) const { return(y-this.CanvasOffsetY()); } //--- Возвращает координаты X и Y текста в зависимости от точки привязки bool GetTextCoordsByAnchor(int &x, int &y, int &dir_x, int dir_y); //--- Возвращает указатель на контейнер панели строк таблицы CContainer *GetRowsPanelContainer(void); public: //--- Возвращает указатель на назначенный канвас (1) фона, (2) переднего плана CCanvas *GetBackground(void) { return this.m_background; } CCanvas *GetForeground(void) { return this.m_foreground; } //--- Получение границ родительского объекта-контейнера int ContainerLimitLeft(void) const { return(this.m_element_base==NULL ? this.X() : this.m_element_base.LimitLeft()); } int ContainerLimitRight(void) const { return(this.m_element_base==NULL ? this.Right() : this.m_element_base.LimitRight()); } int ContainerLimitTop(void) const { return(this.m_element_base==NULL ? this.Y() : this.m_element_base.LimitTop()); } int ContainerLimitBottom(void) const { return(this.m_element_base==NULL ? this.Bottom() : this.m_element_base.LimitBottom()); } //--- Возвращает флаг того, что объект расположен за пределами своего контейнера virtual bool IsOutOfContainer(void); //--- (1) Устанавливает, (2) возвращает текст ячейки void SetText(const string text) { ::StringToShortArray(text,this.m_text); } string Text(void) const { return ::ShortArrayToString(this.m_text); } //--- (1) Устанавливает, (2) возвращает цвет текста ячейки void SetForeColor(const color clr) { this.m_fore_color=clr; } color ForeColor(void) const { return this.m_fore_color; } //--- Устанавливает идентификатор virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Устанавливает, (2) возвращает индекс ячейки void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Устанавливает, (2) возвращает смещение текста по оси X void SetTextShiftX(const int shift) { this.m_text_x=shift; } int TextShiftX(void) const { return this.m_text_x; } //--- (1) Устанавливает, (2) возвращает смещение текста по оси Y void SetTextShiftY(const int shift) { this.m_text_y=shift; } int TextShiftY(void) const { return this.m_text_y; } //--- (1) Устанавливает, (2) возвращает точку привязки текста void SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw); int TextAnchor(void) const { return this.m_text_anchor; } //--- Устанавливает точку привязки и смещения текста void SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw); //--- Назначает базовый элемент (строку таблицы) void RowAssign(CTableRowView *base_element); //--- (1) Назначает, (2) возвращает модель ячейки bool TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h); CTableCell *GetTableCellModel(void) { return this.m_table_cell_model; } //--- Распечатывает в журнале назначенную модель ячейки void TableCellModelPrint(void); //--- (1) Заливает объект цветом фона, (2) обновляет объект для отображения изменений, (3) рисует внешний вид virtual void Clear(const bool chart_redraw); virtual void Update(const bool chart_redraw); virtual void Draw(const bool chart_redraw); //--- Выводит текст virtual void DrawText(const int dx, const int dy, const string text, const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_CELL_VIEW); } //--- Инициализация объекта класса void Init(const string text); //--- Возвращает описание объекта virtual string Description(void); //--- Конструкторы/деструктор CTableCellView(void); CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h); ~CTableCellView (void){} };
В методе, назначающем ячейке строку, канвасы фона и переднего плана, зададим цвет текста равным цвету переднего плана:
//+------------------------------------------------------------------+ //| CTableCellView::Назначает строку, канвасы фона и переднего плана | //+------------------------------------------------------------------+ void CTableCellView::RowAssign(CTableRowView *base_element) { if(base_element==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return; } this.m_element_base=base_element; this.m_background=this.m_element_base.GetBackground(); this.m_foreground=this.m_element_base.GetForeground(); this.m_painter=this.m_element_base.Painter(); this.m_fore_color=this.m_element_base.ForeColor(); }
В метод, возвращающий координаты X и Y текста в зависимости от точки привязки, добавим переменные, в которые будут записываться знаки направления смещения (+1 / -1) по осям X и Y:
//+------------------------------------------------------------------+ //| CTableCellView::Возвращает координаты X и Y текста | //| в зависимости от точки привязки | //+------------------------------------------------------------------+ bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y, int &dir_x,int dir_y) { //--- Получаем размеры текста в ячейке int text_w=0, text_h=0; this.m_foreground.TextSize(this.Text(),text_w,text_h); if(text_w==0 || text_h==0) return false; //--- В зависимости от точки привязки текста в ячейке //--- рассчитываем его начальные координаты (верхний левый угол) switch(this.m_text_anchor) { //--- Точка привязки слева по центру case ANCHOR_LEFT : x=0; y=(this.Height()-text_h)/2; dir_x=1; dir_y=1; break; //--- Точка привязки в левом нижнем углу case ANCHOR_LEFT_LOWER : x=0; y=this.Height()-text_h; dir_x= 1; dir_y=-1; break; //--- Точка привязки снизу по центру case ANCHOR_LOWER : x=(this.Width()-text_w)/2; y=this.Height()-text_h; dir_x= 1; dir_y=-1; break; //--- Точка привязки в правом нижнем углу case ANCHOR_RIGHT_LOWER : x=this.Width()-text_w; y=this.Height()-text_h; dir_x=-1; dir_y=-1; break; //--- Точка привязки справа по центру case ANCHOR_RIGHT : x=this.Width()-text_w; y=(this.Height()-text_h)/2; dir_x=-1; dir_y= 1; break; //--- Точка привязки в правом верхнем углу case ANCHOR_RIGHT_UPPER : x=this.Width()-text_w; y=0; dir_x=-1; dir_y= 1; break; //--- Точка привязки сверху по центру case ANCHOR_UPPER : x=(this.Width()-text_w)/2; y=0; dir_x=1; dir_y=1; break; //--- Точка привязки строго по центру объекта case ANCHOR_CENTER : x=(this.Width()-text_w)/2; y=(this.Height()-text_h)/2; dir_x=1; dir_y=1; break; //--- Точка привязки в левом верхнем углу //---ANCHOR_LEFT_UPPER default: x=0; y=0; dir_x=1; dir_y=1; break; } return true; }
В методе, рисующем внешний вид, теперь используем полученные значения в качестве множителя для смещения текста относительно осей X и Y, а также не рисуем ячейку, если она находится за пределами своего контейнера:
//+------------------------------------------------------------------+ //| CTableCellView::Рисует внешний вид | //+------------------------------------------------------------------+ void CTableCellView::Draw(const bool chart_redraw) { //--- Если ячейка за пределами контейнера строк таблицы - уходим if(this.IsOutOfContainer()) return; //--- Получаем координаты текста и направление смещения в зависимости от точки привязки int text_x=0, text_y=0; int dir_horz=0, dir_vert=0; if(!this.GetTextCoordsByAnchor(text_x,text_y,dir_horz,dir_vert)) return; //--- Корректируем координаты текста int x=this.AdjX(this.X()+text_x); int y=this.AdjY(this.Y()+text_y); //--- Устанавливаем координаты разделительной линии int x1=this.AdjX(this.X()); int x2=this.AdjX(this.X()); int y1=this.AdjY(this.Y()); int y2=this.AdjY(this.Bottom()); //--- Выводим текст на канвасе переднего плана с учётом направления смещения без обновления графика this.DrawText(x+this.m_text_x*dir_horz,y+this.m_text_y*dir_vert,this.Text(),false); //--- Если это не крайняя справа ячейка - рисуем у ячейки справа вертикальную разделительную полосу if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1) { int line_x=this.AdjX(this.Right()); this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG())); } //--- Обновляем канвас фона с указанным флагом перерисовки графика this.m_background.Update(chart_redraw); }
Теперь текст в ячейке будет правильно позиционироваться, независимо от точки привязки текста.
Метод, возвращающий указатель на контейнер панели строк таблицы:
//+------------------------------------------------------------------+ //| CTableCellView::Возвращает указатель | //| на контейнер панели строк таблицы | //+------------------------------------------------------------------+ CContainer *CTableCellView::GetRowsPanelContainer(void) { //--- Проверяем строку if(this.m_element_base==NULL) return NULL; //--- Получаем панель для размещения строк CPanel *rows_area=this.m_element_base.GetContainer(); if(rows_area==NULL) return NULL; //--- Возвращаем контейнер панели со строками return rows_area.GetContainer(); }
Ячейка расположена внутри строки. Строка, наряду с другими строками, расположена на панели. Панель в свою очередь прикреплена к контейнеру, внутри которого может прокручиваться полосами прокрутки. Метод возвращает указатель на контейнер с полосами прокрутки.
Метод, возвращающий флаг того, что объект расположен за пределами своего контейнера:
//+------------------------------------------------------------------+ //| CTableCellView::Возвращает флаг того, что объект | //| расположен за пределами своего контейнера | //+------------------------------------------------------------------+ bool CTableCellView::IsOutOfContainer(void) { //--- Проверяем строку if(this.m_element_base==NULL) return false; //--- Получаем контейнер панели со строками CContainer *container=this.GetRowsPanelContainer(); if(container==NULL) return false; //--- Получаем границы ячейки по всем сторонам int cell_l=this.m_element_base.X()+this.X(); int cell_r=this.m_element_base.X()+this.Right(); int cell_t=this.m_element_base.Y()+this.Y(); int cell_b=this.m_element_base.Y()+this.Bottom(); //--- Возвращаем результат проверки, что объект полностью выходит за пределы контейнера return(cell_r <= container.X() || cell_l >= container.Right() || cell_b <= container.Y() || cell_t >= container.Bottom()); }
Ячейка находится внутри своей области, расположенной в строке. Метод рассчитывает координаты ячейки относительно строки и возвращает флаг того, что рассчитанные координаты ячейки выходят за пределы контейнера.
Теперь доработаем класс визуального представления строки таблицы CTableRowView.
Объявим новые методы и в деструкторе очистим список ячеек:
//+------------------------------------------------------------------+ //| Класс визуального представления строки таблицы | //+------------------------------------------------------------------+ class CTableRowView : public CPanel { protected: CTableCellView m_temp_cell; // Временный объект ячейки для поиска CTableRow *m_table_row_model; // Указатель на модель строки CListElm m_list_cells; // Список ячеек int m_index; // Индекс в списке строк //--- Создаёт и добавляет в список новый объект представления ячейки CTableCellView *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h); //--- Удаляет указанную область строки и ячейку с соответствующим индексом bool BoundCellDelete(const int index); public: //--- Возвращает (1) список, (2) количество ячеек, (3) ячейку CListElm *GetListCells(void) { return &this.m_list_cells; } int CellsTotal(void) const { return this.m_list_cells.Total(); } CTableCellView *GetCellView(const uint index) { return this.m_list_cells.GetNodeAtIndex(index); } //--- Устанавливает идентификатор virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Устанавливает, (2) возвращает индекс строки void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Устанавливает, (2) возвращает модель строки bool TableRowModelAssign(CTableRow *row_model); CTableRow *GetTableRowModel(void) { return this.m_table_row_model; } //--- Обновляет строку с обновлённой моделью bool TableRowModelUpdate(CTableRow *row_model); //--- Перерассчитывает области ячеек bool RecalculateBounds(CListElm *list_bounds); //--- Распечатывает в журнале назначенную модель строки void TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0)const { return CLabel::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_ROW_VIEW); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); virtual void InitColors(void); //--- Конструкторы/деструктор CTableRowView(void); CTableRowView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableRowView (void){ this.m_list_cells.Clear(); } };
В методе создания и добавления в список нового объекта представления ячейки запомним режим сортировки списка, а после добавления вернём списку изначальную сортировку:
//+------------------------------------------------------------------+ //| CTableRowView::Создаёт и добавляет в список | //| новый объект представления ячейки | //+------------------------------------------------------------------+ CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h) { //--- Проверяем есть ли в списке объект с указанным идентификатором и, если да - сообщаем об этом и возвращаем NULL this.m_temp_cell.SetIndex(index); //--- Запоминаем метод сортировки списка int sort_mode=this.m_list_cells.SortMode(); //--- Устанавливаем списку флаг сортировки по идентификатору this.m_list_cells.Sort(ELEMENT_SORT_BY_ID); if(this.m_list_cells.Search(&this.m_temp_cell)!=NULL) { //--- Возвращаем списку изначальную сортировку, сообщаем, что такой объект уже существует и возвращаем NULL this.m_list_cells.Sort(sort_mode); ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index); return NULL; } //--- Возвращаем списку изначальную сортировку this.m_list_cells.Sort(sort_mode); //--- Создаём имя объекта ячейки string name="TableCellView"+(string)this.Index()+"x"+(string)index; //--- Создаём новый объект TableCellView; при неудаче - сообщаем об этом и возвращаем NULL CTableCellView *cell_view=new CTableCellView(index,name,text,dx,dy,w,h); if(cell_view==NULL) { ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__); return NULL; } //--- Если новый объект не удалось добавить в список - сообщаем об этом, удаляем объект и возвращаем NULL if(this.m_list_cells.Add(cell_view)==-1) { ::PrintFormat("%s: Error. Failed to add CTableCellView object to list",__FUNCTION__); delete cell_view; return NULL; } //--- Назначаем базовый элемент (строку) и возвращаем указатель на объект cell_view.RowAssign(&this); return cell_view; }
В методе установки модели строки ширину столбцов будем рассчитывать по ширине панели строк, а не самой строки, так как строка немного уже панели. Ширина заголовка таблицы также равна ширине панели строк, поэтому для совпадения ширины ячеек и столбцов используем ширину панели:
//+------------------------------------------------------------------+ //| CTableRowView::Устанавливает модель строки | //+------------------------------------------------------------------+ bool CTableRowView::TableRowModelAssign(CTableRow *row_model) { //--- Если передан пустой объект - сообщаем об этом и возвращаем false if(row_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Если в переданной модели строки нет ни одной ячейки - сообщаем об этом и возвращаем false int total=(int)row_model.CellsTotal(); if(total==0) { ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__); return false; } //--- Сохраняем указатель на переданную модель строки this.m_table_row_model=row_model; //--- рассчитываем ширину ячейки по ширине панели строк CCanvasBase *base=this.GetContainer(); int w=(base!=NULL ? base.Width() : this.Width()); int cell_w=(int)::fmax(::round((double)w/(double)total),DEF_TABLE_COLUMN_MIN_W); //--- В цикле по количеству ячеек в модели строки for(int i=0;i<total;i++) { //--- получаем модель очередной ячейки, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- рассчитываем координату и создаём имя для области ячейки int x=cell_w*i; string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i; //--- Создаём новую область ячейки CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height()); if(cell_bound==NULL) return false; //--- Создаём новый объект визуального представления ячейки CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height()); if(cell_view==NULL) return false; //--- На текущую область ячейки назначаем соответствующий объект визуального представления ячейки cell_bound.AssignObject(cell_view); } //--- Всё успешно return true; }
Ячейки в строке таблицы могут обновлять свой внешний вид, например, при добавлении или удалении столбца таблицы. Такие изменения, произведённые в модели таблицы, необходимо отобразить и в визуальном представлении таблицы.
Создадим для этого специальный метод.
Метод, обновляющий строку с обновлённой моделью:
//+------------------------------------------------------------------+ //| CTableRowView::Обновляет строку с обновлённой моделью | //+------------------------------------------------------------------+ bool CTableRowView::TableRowModelUpdate(CTableRow *row_model) { //--- Если передан пустой объект - сообщаем об этом и возвращаем false if(row_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Если в переданной модели строки нет ни одной ячейки - сообщаем об этом и возвращаем false int total_model=(int)row_model.CellsTotal(); // Количество ячеек в модели строки if(total_model==0) { ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__); return false; } //--- Сохраняем указатель на переданную модель строки this.m_table_row_model=row_model; //--- Рассчитываем ширину ячейки по ширине панели строк CCanvasBase *base=this.GetContainer(); int w=(base!=NULL ? base.Width() : this.Width()); int cell_w=(int)::fmax(::round((double)w/(double)total_model),DEF_TABLE_COLUMN_MIN_W); CBound *cell_bound=NULL; int total_bounds=this.m_list_bounds.Total(); // Количество областей int diff=total_model-total_bounds; // Разница между количеством областей в строке и ячеек в модели строки //--- Если в модели больше ячеек, чем областей в списке - создадим недостающие области и ячейки в конце списков if(diff>0) { //--- В цикле по количеству недостающих областей for(int i=total_bounds;i<total_bounds+diff;i++) { //--- создаём и добавляем в список diff количество областей ячеек строки. //--- Получаем модель очередной ячейки, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- рассчитываем координату и создаём имя для области ячейки int x=cell_w*i; string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i; //--- Создаём новую область ячейки CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height()); if(cell_bound==NULL) return false; //--- Создаём новый объект визуального представления ячейки CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height()); if(cell_view==NULL) return false; } } //--- Если в списке больше областей, чем ячеек в модели - удалим лишние области в конце списка if(diff<0) { int start=total_bounds-1; int end=start-diff; bool res=true; for(int i=start;i>end;i--) { if(!this.BoundCellDelete(i)) return false; } } //--- В цикле по количеству ячеек в модели строки for(int i=0;i<total_model;i++) { //--- получаем модель очередной ячейки, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- рассчитываем координату ячейки int x=cell_w*i; //--- Получаем очередную область ячейки CBound *cell_bound=this.GetBoundAt(i); if(cell_bound==NULL) return false; //--- Получаем из списка объект визуального представления ячейки CTableCellView *cell_view=this.m_list_cells.GetNodeAtIndex(i); if(cell_view==NULL) return false; //--- На текущую область ячейки назначаем соответствующий объект визуального представления ячейки и его текст cell_bound.AssignObject(cell_view); cell_view.SetText(cell_model.Value()); } //--- Всё успешно return true; }
Логика метода расписана в комментариях к коду. Если в модели таблицы были добавлены или удалены столбцы, то и в визуальном представлении строки добавляется или удаляется нужное количество ячеек. Затем в цикле по количеству ячеек строки в модели таблицы назначаем на область ячейки соответствующий объект визуального представления ячейки.
Метод, удаляющий указанную область строки и ячейку с соответствующим индексом:
//+------------------------------------------------------------------+ //| CTableRowView::Удаляет указанную область строки | //| и ячейку с соответствующим индексом | //+------------------------------------------------------------------+ bool CTableRowView::BoundCellDelete(const int index) { if(!this.m_list_cells.Delete(index)) return false; return this.m_list_bounds.Delete(index); }
Если объект ячейки успешно удалён из списка — удаляем и соответствующую область из списка областей.
В методе, рисующем внешний вид строки, проверяем выход строки за пределы контейнера:
//+------------------------------------------------------------------+ //| CTableRowView::Рисует внешний вид | //+------------------------------------------------------------------+ void CTableRowView::Draw(const bool chart_redraw) { //--- Если строка за пределами контейнера - уходим if(this.IsOutOfContainer()) return; //--- Заливаем объект цветом фона, рисуем линию строки и обновляем канвас фона this.Fill(this.BackColor(),false); this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG())); //--- Рисуем ячейки строки int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Получаем область очередной ячейки CBound *cell_bound=this.GetBoundAt(i); if(cell_bound==NULL) continue; //--- Из области ячейки получаем присоединённый объект ячейки CTableCellView *cell_view=cell_bound.GetAssignedObj(); //--- Рисуем визуальное представление ячейки if(cell_view!=NULL) cell_view.Draw(false); } //--- Обновляем канвасы фона и переднего плана с указанным флагом перерисовки графика this.Update(chart_redraw); }
Строка, которая не находится в видимой области контейнера, рисоваться не должна.
При изменении ширины столбца необходимо изменить ширину всех ячеек, соответствующих столбцу, и сместить прилегающие ячейки на новые координаты. Напишем для этого новый метод.
Метод, перерассчитывающий области ячеек:
//+------------------------------------------------------------------+ //| CTableRowView::Перерассчитывает области ячеек | //+------------------------------------------------------------------+ bool CTableRowView::RecalculateBounds(CListElm *list_bounds) { //--- Проверяем список if(list_bounds==NULL) return false; //--- В цикле по количеству областей в списке for(int i=0;i<list_bounds.Total();i++) { //--- получаем очередную область заголовка и соответствующую ей область ячейки CBound *capt_bound=list_bounds.GetNodeAtIndex(i); CBound *cell_bound=this.GetBoundAt(i); if(capt_bound==NULL || cell_bound==NULL) return false; //--- В область ячейки устанавливаем координату и размер области заголовка cell_bound.SetX(capt_bound.X()); cell_bound.ResizeW(capt_bound.Width()); //--- Из области ячейки получаем присоединённый объект ячейки CTableCellView *cell_view=cell_bound.GetAssignedObj(); if(cell_view==NULL) return false; //--- В объект визуального представления ячейки устанавливаем координату и размер области ячейки cell_view.BoundSetX(cell_bound.X()); cell_view.BoundResizeW(cell_bound.Width()); } //--- Всё успешно return true; }
В метод передаётся список областей заголовков столбцов. Относительно свойств областей столбцов из переданного списка меняются области ячеек таблицы — их размеры и координаты. И новые размеры, и координаты областей устанавливаются в соответствующие им объекты ячеек.
В парадигме MVC заголовок таблицы является не просто частью таблицы, а представляет собой отдельный элемент управления, при помощи которого можно воздействовать и управлять внешним видом столбцов таблицы. Заголовок столбца в этом контексте тоже является элементом управления. В контексте данной разработки заголовок столбца позволяет изменять размер столбца, устанавливать для всех ячеек столбца одинаковые свойства и т.п.
Таким образом, в классе заголовка столбца и классе заголовка таблицы аккумулированы основные доработки, позволяющие "оживить" таблицу.
Доработаем класс визуального представления заголовка столбца таблицы CColumnCaptionView.
Заголовок столбца, как элемент управления, будет управлять размером (шириной) столбца и направлением сортировки, либо её отсутствием. При щелчке по заголовку на нём будет появляться стрелочка, указывающая направление сортировки. При отсутствии сортировки (если щёлкнуть по другому заголовку) стрелка не показывается. Если навести курсор на правую границу заголовка, то у курсора появится стрелка-подсказка направления смещения. При этом, если зажать кнопку мышки и тащить грань, то ширина столбца будет изменяться, а расположение столбцов справа будет корректироваться — они будут смещаться, следуя за правым краем изменяемого столбца.
Объявим новые переменные и методы:
//+------------------------------------------------------------------+ //| Класс визуального представления заголовка столбца таблицы | //+------------------------------------------------------------------+ class CColumnCaptionView : public CButton { protected: CColumnCaption *m_column_caption_model; // Указатель на модель заголовка столбца CBound *m_bound_node; // Указатель на область заголовка int m_index; // Индекс в списке столбцов ENUM_TABLE_SORT_MODE m_sort_mode; // Режим сортировки столбца таблицы //--- Добавляет в список объекты-подсказки со стрелками virtual bool AddHintsArrowed(void); //--- Отображает курсор изменения размеров virtual bool ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y); public: //--- Устанавливает идентификатор virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Устанавливает, (2) возвращает индекс ячейки void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Назначает, (2) возвращает область заголовка, которой назначен объект void AssignBoundNode(CBound *bound) { this.m_bound_node=bound; } CBound *GetBoundNode(void) { return this.m_bound_node; } //--- (1) Назначает, (2) возвращает модель заголовка столбца bool ColumnCaptionModelAssign(CColumnCaption *caption_model); CColumnCaption *ColumnCaptionModel(void) { return this.m_column_caption_model; } //--- Распечатывает в журнале назначенную модель заголовка столбца void ColumnCaptionModelPrint(void); //--- (1) Устанавливает, (2) возвращает режим сортировки void SetSortMode(const ENUM_TABLE_SORT_MODE mode) { this.m_sort_mode=mode; } ENUM_TABLE_SORT_MODE SortMode(void) const { return this.m_sort_mode; } //--- Устанавливает противоположное направление сортировки void SetSortModeReverse(void); //--- Рисует (1) внешний вид, (2) стрелку направления сортировки virtual void Draw(const bool chart_redraw); protected: void DrawSortModeArrow(void); public: //--- Обработчик изменения размеров элемента по правой стороне virtual bool ResizeZoneRightHandler(const int x, const int y); //--- Обработчики изменения размеров элемента по сторонам и углам virtual bool ResizeZoneLeftHandler(const int x, const int y) { return false; } virtual bool ResizeZoneTopHandler(const int x, const int y) { return false; } virtual bool ResizeZoneBottomHandler(const int x, const int y) { return false; } virtual bool ResizeZoneLeftTopHandler(const int x, const int y) { return false; } virtual bool ResizeZoneRightTopHandler(const int x, const int y) { return false; } virtual bool ResizeZoneLeftBottomHandler(const int x, const int y) { return false; } virtual bool ResizeZoneRightBottomHandler(const int x, const int y){ return false; } //--- Изменяет ширину объекта virtual bool ResizeW(const int w); //--- Обработчик событий нажатий кнопок мышки (Press) virtual void OnPressEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0)const { return CButton::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW);} //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); virtual void InitColors(void); //--- Возвращает описание объекта virtual string Description(void); //--- Конструкторы/деструктор CColumnCaptionView(void); CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CColumnCaptionView (void){} };
Рассмотрим доработки уже написанных и реализацию новых методов класса.
В конструкторе установим сортировку по умолчанию как отсутствующую:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Конструктор по умолчанию. Строит объект | //| в главном окне текущего графика в координатах 0,0 | //| с размерами по умолчанию | //+------------------------------------------------------------------+ CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE) { //--- Инициализация this.Init("Caption"); this.SetID(0); this.SetName("ColumnCaption"); } //+------------------------------------------------------------------+ //| CColumnCaptionView::Конструктор параметрический. | //| Строит объект в указанном окне указанного графика с | //| указанными текстом, координатами и размерами | //+------------------------------------------------------------------+ CColumnCaptionView::CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE) { //--- Инициализация this.Init(text); this.SetID(0); }
В методе инициализации установим возможность изменения размеров, невозможность перемещения объекта, и зададим область изображения для стрелки направления сортировки:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Инициализация | //+------------------------------------------------------------------+ void CColumnCaptionView::Init(const string text) { //--- Смещения текста по умолчанию this.m_text_x=4; this.m_text_y=2; //--- Устанавливаем цвета различных состояний this.InitColors(); //--- Возможно изменять размеры this.SetResizable(true); this.SetMovable(false); this.SetImageBound(this.ObjectWidth()-14,4,8,11); }
В методе рисования внешнего вида проверим, что объект не находится за пределами контейнера и вызовем метод рисования стрелки направления сортировки:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Рисует внешний вид | //+------------------------------------------------------------------+ void CColumnCaptionView::Draw(const bool chart_redraw) { //--- Если объект за пределами своего контейнера - уходим if(this.IsOutOfContainer()) return; //--- Заливаем объект цветом фона, рисуем слева светлую вертикальную линию, справа - тёмную this.Fill(this.BackColor(),false); color clr_dark =this.BorderColor(); // "Тёмный цвет" color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100); // "Светлый цвет" this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG())); // Линия слева this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Линия справа //--- обновляем канвас фона this.m_background.Update(false); //--- Выводим текст заголовка CLabel::Draw(false); //--- Рисуем стрелки направления сортировки this.DrawSortModeArrow(); //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Метод, рисующий стрелку направления сортировки:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Рисует стрелку направления сортировки | //+------------------------------------------------------------------+ void CColumnCaptionView::DrawSortModeArrow(void) { //--- Задаём цвет стрелки для обычного и заблокированного состояний объекта color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor()); switch(this.m_sort_mode) { //--- Сортировка по возрастанию case TABLE_SORT_MODE_ASC : //--- Очищаем область рисунка и рисуем стрелку вниз this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); this.m_painter.ArrowDown(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true); break; //--- Сортировка по убыванию case TABLE_SORT_MODE_DESC : //--- Очищаем область рисунка и рисуем стрелку вверх this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); this.m_painter.ArrowUp(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true); break; //--- Нет сортировки default : //--- Очищаем область рисунка this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); break; } }
В зависимости от направления сортировки рисуем соответствующую стрелку:
- по возрастанию — стрелку вниз,
- по убыванию — стрелку вверх,
- нет сортировки — просто очищаем рисунок со стрелкой.
Метод, разворачивающий направление сортировки:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Разворачивает направление сортировки | //+------------------------------------------------------------------+ void CColumnCaptionView::SetSortModeReverse(void) { switch(this.m_sort_mode) { case TABLE_SORT_MODE_ASC : this.m_sort_mode=TABLE_SORT_MODE_DESC; break; case TABLE_SORT_MODE_DESC : this.m_sort_mode=TABLE_SORT_MODE_ASC; break; default : break; } }
Если текущая сортировка установлена по возрастанию, то устанавливаем её по убыванию, и наоборот. Отсутствующую сортировку устанавливаем в другом методе, так как этот метод предназначен только для переключения сортировки при щелчке по заголовку столбца.
Метод, добавляющий в список объекты-подсказки со стрелками:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Добавляет в список | //| объекты-подсказки со стрелками | //+------------------------------------------------------------------+ bool CColumnCaptionView::AddHintsArrowed(void) { //--- Создаём подсказку стрелки горизонтального смещения CVisualHint *hint=this.CreateAndAddNewHint(HINT_TYPE_ARROW_SHIFT_HORZ,DEF_HINT_NAME_SHIFT_HORZ,18,18); if(hint==NULL) return false; //--- Устанавливаем размер области изображения подсказки hint.SetImageBound(0,0,hint.Width(),hint.Height()); //--- скрываем подсказку и рисуем внешний вид hint.Hide(false); hint.Draw(false); //--- Всё успешно return true; }
Создаётся новая подсказка и добавляется в список. Далее её можно получить из списка по имени.
Метод, отображающий курсор изменения размеров:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Отображает курсор изменения размеров | //+------------------------------------------------------------------+ bool CColumnCaptionView::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y) { CVisualHint *hint=NULL; // Указатель на подсказку int hint_shift_x=0; // Смещение подсказки по X int hint_shift_y=0; // Смещение подсказки по Y //--- В зависимости от расположения курсора на границах элемента //--- указываем смещения подсказки относительно координат курсора, //--- отображаем на графике требуемую подсказку и получаем указатель на этот объект if(edge!=CURSOR_REGION_RIGHT) return false; hint_shift_x=-8; hint_shift_y=-12; this.ShowHintArrowed(HINT_TYPE_ARROW_SHIFT_HORZ,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ); //--- Возвращаем результат корректировки положения подсказки относительно курсора return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false); }
Метод работает, только когда курсор находится над правой гранью элемента. Отображает подсказку и смещает её на позицию курсора с установленным смещением.
Обработчик изменения размеров за правую грань:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Обработчик изменения размеров за правую грань| //+------------------------------------------------------------------+ bool CColumnCaptionView::ResizeZoneRightHandler(const int x,const int y) { //--- Рассчитываем и устанавливаем новую ширину элемента int width=::fmax(x-this.X()+1,DEF_TABLE_COLUMN_MIN_W); if(!this.ResizeW(width)) return false; //--- Получаем указатель на подсказку CVisualHint *hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ); if(hint==NULL) return false; //--- Смещаем подсказку на указанные величины относительно курсора int shift_x=-8; int shift_y=-12; CTableHeaderView *header=this.m_container; if(header==NULL) return false; bool res=header.RecalculateBounds(this.GetBoundNode(),this.Width()); res &=hint.Move(x+shift_x,y+shift_y); if(res) ::ChartRedraw(this.m_chart_id); return res; }
При перетаскивании правой грани элемента, он должен изменять свою ширину в зависимости от направления смещения курсора. Сначала изменяется ширина элемента, затем отображается подсказка, и вызывается метод перерасчёта областей заголовков столбцов RecalculateBounds() заголовка таблицы. Этот метод устанавливает новый размер области изменённого заголовка и смещает все прилегающие к нему области заголовков столбцов на новые координаты.
Виртуальный метод, изменяющий ширину объекта:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Изменяет ширину объекта | //+------------------------------------------------------------------+ bool CColumnCaptionView::ResizeW(const int w) { if(!CCanvasBase::ResizeW(w)) return false; //--- Очищаем область рисунка в прошлом месте this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Устанавливаем новую область рисунка this.SetImageBound(this.Width()-14,4,8,11); return true; }
При изменении размеров заголовка столбца необходимо сместить область рисунка (стрелки направления сортировки), чтобы стрелка рисовалась в нужном месте возле правой грани элемента.
Обработчик событий нажатий кнопок мышки:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Обработчик событий нажатий кнопок мышки | //+------------------------------------------------------------------+ void CColumnCaptionView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Если кнопка мышки отпущена в области перетаскивания правой грани элемента - уходим if(this.ResizeRegion()==CURSOR_REGION_RIGHT) return; //--- Меняем стрелку направления сортировки на обратную и вызываем обработчик щелчка мышки this.SetSortModeReverse(); CCanvasBase::OnPressEvent(id,lparam,dparam,sparam); }
Если событие щелчка вызвано отпусканием кнопки мышки в области перетаскивания — значит только что завершено действие по изменению размеров заголовка — уходим. Далее — меняем стрелку направления сортировки на обратную и вызываем обработчик событий кнопок мышки базового объекта.
В методах работы с файлами сохраняем и загружаем значение направления сортировки:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Сохранение в файл | //+------------------------------------------------------------------+ bool CColumnCaptionView::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CButton::Save(file_handle)) return false; //--- Сохраняем номер заголовка if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return false; //--- Сохраняем направление сортировки if(::FileWriteInteger(file_handle,this.m_sort_mode,INT_VALUE)!=INT_VALUE) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CColumnCaptionView::Загрузка из файла | //+------------------------------------------------------------------+ bool CColumnCaptionView::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CButton::Load(file_handle)) return false; //--- Загружаем номер заголовка this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем направление сортировки this.m_id=this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE); //--- Всё успешно return true; }
Теперь доработаем класс визуального представления заголовка таблицы CTableHeaderView.
Объект этого класса содержит в себе список заголовков столбцов, которыми управляет этот объект, давая пользователю элемент управления внешним видом столбцов таблицы и сортировкой по любому из них.
Объявим новые методы класса:
//+------------------------------------------------------------------+ //| Класс визуального представления заголовка таблицы | //+------------------------------------------------------------------+ class CTableHeaderView : public CPanel { protected: CColumnCaptionView m_temp_caption; // Временный объект заголовка столбца для поиска CTableHeader *m_table_header_model; // Указатель на модель заголовка таблицы //--- Создаёт и добавляет в список новый объект представления заголовка столбца CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h); public: //--- (1) Устанавливает, (2) возвращает модель заголовка таблицы bool TableHeaderModelAssign(CTableHeader *header_model); CTableHeader *GetTableHeaderModel(void) { return this.m_table_header_model; } //--- Перерассчитывает области заголовков bool RecalculateBounds(CBound *bound,int new_width); //--- Распечатывает в журнале назначенную модель заголовка таблицы void TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Устанавливает заголовку столбца флаг сортировки void SetSortedColumnCaption(const uint index); //--- Получает заголовок столбца (1) по индексу, (2) с флагом сортировки CColumnCaptionView *GetColumnCaption(const uint index); CColumnCaptionView *GetSortedColumnCaption(void); //--- Возвращает индекс заголовка столбца с флагом сортировки int IndexSortedColumnCaption(void); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode); } virtual bool Save(const int file_handle) { return CPanel::Save(file_handle); } virtual bool Load(const int file_handle) { return CPanel::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_HEADER_VIEW); } //--- Обработчик пользовательского события элемента при щелчке на области объекта virtual void MousePressHandler(const int id, const long lparam, const double dparam, const string sparam); //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); virtual void InitColors(void); //--- Конструкторы/деструктор CTableHeaderView(void); CTableHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableHeaderView (void){} };
Рассмотрим доработки имеющихся и реализацию объявленных методов.
В методе, устанавливающем модель заголовка, ширину заголовков будем рассчитывать с учётом минимальной ширины заголовка, правильно установим идентификатор заголовка, установим в заголовок соответствующую ему область, и для самого первого заголовка поставим флаг сортировки по возрастанию:
//+------------------------------------------------------------------+ //| CTableHeaderView::Устанавливает модель заголовка | //+------------------------------------------------------------------+ bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model) { //--- Если передан пустой объект - сообщаем об этом и возвращаем false if(header_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Если в переданной модели заголовка нет ни одного заголовка столбца - сообщаем об этом и возвращаем false int total=(int)header_model.ColumnsTotal(); if(total==0) { ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__); return false; } //--- Сохраняем указатель на переданную модель заголовка таблицы и рассчитываем ширину каждого заголовка столбца this.m_table_header_model=header_model; int caption_w=(int)::fmax(::round((double)this.Width()/(double)total),DEF_TABLE_COLUMN_MIN_W); //--- В цикле по количеству заголовков столбцов в модели заголовка таблицы for(int i=0;i<total;i++) { //--- получаем модель очередного заголовка столбца, CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i); if(caption_model==NULL) return false; //--- рассчитываем координату и создаём имя для области заголовка столбца int x=caption_w*i; string name="CaptionBound"+(string)i; //--- Создаём новую область заголовка столбца CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height()); if(caption_bound==NULL) return false; caption_bound.SetID(i); //--- Создаём новый объект визуального представления заголовка столбца CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height()); if(caption_view==NULL) return false; //--- На текущую область заголовка столбца назначаем соответствующий объект визуального представления заголовка столбца caption_bound.AssignObject(caption_view); caption_view.AssignBoundNode(caption_bound); //--- Для самого первого заголовка устанавливаем флаг сортировки по возрастанию if(i==0) caption_view.SetSortMode(TABLE_SORT_MODE_ASC); } //--- Всё успешно return true; }
Метод, перерассчитывающий области заголовков:
//+------------------------------------------------------------------+ //| CTableHeaderView::Перерассчитывает области заголовков | //+------------------------------------------------------------------+ bool CTableHeaderView::RecalculateBounds(CBound *bound,int new_width) { //--- Если передан пустой объект области или его ширина не изменилась - возвращаем false if(bound==NULL || bound.Width()==new_width) return false; //--- Получаем индекс области в списке int index=this.m_list_bounds.IndexOf(bound); if(index==WRONG_VALUE) return false; //--- Вычисляем смещение и, если его нет - возвращаем false int delta=new_width-bound.Width(); if(delta==0) return false; //--- Изменяем ширину текущей области и назначенного на область объекта bound.ResizeW(new_width); CElementBase *assigned_obj=bound.GetAssignedObj(); if(assigned_obj!=NULL) assigned_obj.ResizeW(new_width); //--- Получаем следующую область после текущей CBound *next_bound=this.m_list_bounds.GetNextNode(); //--- Пересчитываем координаты X для всех последующих областей while(!::IsStopped() && next_bound!=NULL) { //--- Сдвигаем область на значение delta int new_x = next_bound.X()+delta; int prev_width=next_bound.Width(); next_bound.SetX(new_x); next_bound.Resize(prev_width,next_bound.Height()); //--- Если в области есть назначенный объект, обновляем его положение CElementBase *assigned_obj=next_bound.GetAssignedObj(); if(assigned_obj!=NULL) { assigned_obj.Move(assigned_obj.X()+delta,assigned_obj.Y()); //--- Этот блок кода - часть мероприятий по поиску и устранению артефактов при перетаскивании заголовков CCanvasBase *base_obj=assigned_obj.GetContainer(); if(base_obj!=NULL) { if(assigned_obj.X()>base_obj.ContainerLimitRight()) assigned_obj.Hide(false); else assigned_obj.Show(false); } } //--- Переходим к следующей области next_bound=this.m_list_bounds.GetNextNode(); } //--- Рассчитаем новую ширину заголовка таблицы по ширине заголовков столбцов int header_width=0; for(int i=0;i<this.m_list_bounds.Total();i++) { CBound *bound=this.GetBoundAt(i); if(bound!=NULL) header_width+=bound.Width(); } //--- Если рассчитанная ширина заголовка таблицы отличается от текущей - изменяем ширину if(header_width!=this.Width()) { if(!this.ResizeW(header_width)) return false; } //--- Получаем указатель на объект таблицы (View) CTableView *table_view=this.GetContainer(); if(table_view==NULL) return false; //--- Из объекта таблицы получаем указатель на панель со строками таблицы CPanel *table_area=table_view.GetTableArea(); if(table_area==NULL) return false; //--- Меняем размер панели строк таблицы под общий размер заголовков столбцов if(!table_area.ResizeW(header_width)) return false; //--- Получаем список строк таблицы и проходим в цикле по всем строкам CListElm *list=table_area.GetListAttachedElements(); int total=list.Total(); for(int i=0;i<total;i++) { //--- Получаем очередную строку таблицы CTableRowView *row=table_area.GetAttachedElementAt(i); if(row!=NULL) { //--- Меняем размер строки под размер панели и перерассчитываем области ячеек row.ResizeW(table_area.Width()); row.RecalculateBounds(&this.m_list_bounds); } } //--- Перерисовываем все строки таблицы table_area.Draw(false); return true; }
Логика метода расписана в комментариях. В метод передаётся указатель на изменённую область заголовка и её новая ширина. Размер области и назначенного на неё заголовка изменяются, и, начиная от следующей области в списке, каждая последующая область сдвигается на новую координату вместе с назначенными на них заголовками. По окончании смещения всех областей, рассчитывается новый размер заголовка таблицы по ширинам всех входящих в неё областей заголовков столбцов. В последующем этот новый размер влияет на ширину горизонтальной полосы прокрутки таблицы. По новому размеру заголовка таблицы устанавливается ширина панели строк таблицы, все строки изменяют размер под новый размер панели, и для них пересчитываются все области ячеек по списку областей заголовка таблицы. В итоге таблица перерисовывается (её видимая в контейнере часть).
Метод, устанавливающий заголовку столбца флаг сортировки:
//+------------------------------------------------------------------+ //| CTableHeaderView::Устанавливает заголовку столбца флаг сортировки| //+------------------------------------------------------------------+ void CTableHeaderView::SetSortedColumnCaption(const uint index) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Получаем область очередного заголовка столбца и //--- из неё получаем присоединённый объект заголовка столбца CColumnCaptionView *caption_view=this.GetColumnCaption(i); if(caption_view==NULL) continue; //--- Если индекс цикла равен требуемому индексу - устанавливаем флаг сортировки по возрастанию if(i==index) { caption_view.SetSortMode(TABLE_SORT_MODE_ASC); caption_view.Draw(false); } //--- Иначе - сбрасываем флаг сортировки else { caption_view.SetSortMode(TABLE_SORT_MODE_NONE); caption_view.Draw(false); } } this.Draw(true); }
В цикле по списку областей заголовков столбцов получаем объект заголовка. Если это искомый по индексу заголовок — устанавливаем для него флаг сортировки по возрастанию, иначе — снимаем флаг сортировки. Таким образом для всех заголовков столбцов будет снят флаг сортировки, а для указанного по индексу — установлена сортировка по возрастанию. Иными словами — при нажатии на заголовок столбца, для него будет установлена сортировка по возрастанию. При повторном нажатии на этот же заголовок, ему будет установлен флаг сортировки по убыванию, но уже в обработчике события щелчка по элементу.
Метод, возвращающий заголовок столбца по индексу:
//+------------------------------------------------------------------+ //| CTableHeaderView::Получает заголовок столбца по индексу | //+------------------------------------------------------------------+ CColumnCaptionView *CTableHeaderView::GetColumnCaption(const uint index) { //--- Получаем область заголовка столбца по индексу CBound *capt_bound=this.GetBoundAt(index); if(capt_bound==NULL) return NULL; //--- Из области заголовка столбца возвращаем указатель на присоединённый объект заголовка столбца return capt_bound.GetAssignedObj(); }
Метод, возвращающий заголовок столбца с флагом сортировки:
//+------------------------------------------------------------------+ //| CTableHeaderView::Получает заголовок столбца с флагом сортировки | //+------------------------------------------------------------------+ CColumnCaptionView *CTableHeaderView::GetSortedColumnCaption(void) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Получаем область очередного заголовка столбца и //--- из неё получаем присоединённый объект заголовка столбца CColumnCaptionView *caption_view=this.GetColumnCaption(i); //--- Если объект получен и у него установлен флаг сортировки - возвращаем указатель на него if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE) return caption_view; } return NULL; }
Метод, возвращающий индекс сортированного столбца:
//+------------------------------------------------------------------+ //| CTableHeaderView::Возвращает индекс сортированного столбца | //+------------------------------------------------------------------+ int CTableHeaderView::IndexSortedColumnCaption(void) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Получаем область очередного заголовка столбца и //--- из неё получаем присоединённый объект заголовка столбца CColumnCaptionView *caption_view=this.GetColumnCaption(i); //--- Если объект получен и у него установлен флаг сортировки - возвращаем индекс области if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE) return i; } return WRONG_VALUE; }
Представленные выше три метода в простом цикле ищут заголовок столбца с искомым флагом, либо по искомому индексу, и возвращают соответствующие методу данные.
Обработчик пользовательского события элемента при щелчке на области объекта:
//+------------------------------------------------------------------+ //| CTableHeaderView::Обработчик пользовательского события элемента | //| при щелчке на области объекта | //+------------------------------------------------------------------+ void CTableHeaderView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam) { //--- Получаем из sparam наименование объекта заголовка таблицы int len=::StringLen(this.NameFG()); string header_str=::StringSubstr(sparam,0,len); //--- Если извлечённое имя не совпадает с именем этого объекта - не наше событие, уходим if(header_str!=this.NameFG()) return; //--- Найдём в sparam индекс заголовка столбца string capt_str=::StringSubstr(sparam,len+1); string index_str=::StringSubstr(capt_str,5,capt_str.Length()-7); //--- Не нашли индекс в строке - уходим if(index_str=="") return; //--- Записываем индекс заголовка столбца int index=(int)::StringToInteger(index_str); //--- Получаем заголовок столбца по индексу CColumnCaptionView *caption=this.GetColumnCaption(index); if(caption==NULL) return; //--- Если заголовок не имеет флага сортировки - ставим флаг сортировки по возрастанию if(caption.SortMode()==TABLE_SORT_MODE_NONE) { this.SetSortedColumnCaption(index); } //--- Отправляем пользовательское событие на график с индексом заголовка в lparam, режимом сортировки в dparam и именем объекта в sparam //--- Так как в стандартном событии OBJECT_CLICK в lparam и dparam передаются координаты курсора, то здесь будем передавать отрицательные значения ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, -(1000+index), -(1000+caption.SortMode()), this.NameFG()); ::ChartRedraw(this.m_chart_id); }
Метод отправляет пользовательское событие о включении сортировки по столбцу. Так как в lparam и dparam в событии щелчка по объекту передаются координаты курсора, то, чтобы понимать, что это событие включения сортировки, будем передавать в lparam и dparam отрицательное значение:
- В lparam (-1000) + индекс столбца,
- В dparam (-1000) + тип сортировки.
Доработаем класс визуального представления таблицы CTableView.
Класс представляет собой полноценный элемент управления "Таблица". Доработками будет подключение функционала по изменению ширины столбцов и сортировке по выбранному столбцу таблицы.
Объявим новые методы класса:
//+------------------------------------------------------------------+ //| Класс визуального представления таблицы | //+------------------------------------------------------------------+ class CTableView : public CPanel { protected: //--- Получаемые данные таблицы CTable *m_table_obj; // Указатель на объект таблицы (включает модели таблицы и заголовка) CTableModel *m_table_model; // Указатель на модель таблицы (получаем из CTable) CTableHeader *m_header_model; // Указатель на модель заголовка таблицы (получаем из CTable) //--- Данные компонента View CTableHeaderView *m_header_view; // Указатель на заголовок таблицы (View) CPanel *m_table_area; // Панель для размещения строк таблицы CContainer *m_table_area_container; // Контейнер для размещения панели со строками таблицы //--- (1) Устанавливает, (2) возвращает модель таблицы bool TableModelAssign(CTableModel *table_model); CTableModel *GetTableModel(void) { return this.m_table_model; } //--- (1) Устанавливает, (2) возвращает модель заголовка таблицы bool HeaderModelAssign(CTableHeader *header_model); CTableHeader *GetHeaderModel(void) { return this.m_header_model; } //--- Создаёт из модели объект (1) заголовка, (2) таблицы, (3) обновляет изменённую таблицу bool CreateHeader(void); bool CreateTable(void); bool UpdateTable(void); public: //--- (1) Устанавливает, (2) возвращает объект таблицы bool TableObjectAssign(CTable *table_obj); CTable *GetTableObj(void) { return this.m_table_obj; } //--- Возвращает (1) заголовок, (2) область размещения таблицы, (3) контейнер области таблицы CTableHeaderView *GetHeader(void) { return this.m_header_view; } CPanel *GetTableArea(void) { return this.m_table_area; } CContainer *GetTableAreaContainer(void) { return this.m_table_area_container; } //--- Распечатывает в журнале назначенную модель (1) таблицы, (2) заголовка, (3) объекта таблицы void TableModelPrint(const bool detail); void HeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); void TablePrint(const int column_width=CELL_WIDTH_IN_CHARS); //--- Получает заголовок столбца (1) по индексу, (2) с флагом сортировки CColumnCaptionView *GetColumnCaption(const uint index) { return(this.GetHeader()!=NULL ? this.GetHeader().GetColumnCaption(index) : NULL); } CColumnCaptionView *GetSortedColumnCaption(void) { return(this.GetHeader()!=NULL ? this.GetHeader().GetSortedColumnCaption(): NULL); } //--- Возвращает объект визуальгного представления указанной (1) строки, (2) ячейки CTableRowView *GetRowView(const uint index) { return(this.GetTableArea()!=NULL ? this.GetTableArea().GetAttachedElementAt(index) : NULL); } CTableCellView *GetCellView(const uint row,const uint col) { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).GetCellView(col) : NULL); } //--- Возвращает количество строк таблицы int RowsTotal(void) { return(this.GetTableArea()!=NULL ? this.GetTableArea().AttachedElementsTotal() : 0); } //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_VIEW); } //--- Обработчик пользовательского события элемента при щелчке на области объекта virtual void MousePressHandler(const int id, const long lparam, const double dparam, const string sparam); //--- Сортирует таблицу по значению столбца и направлению bool Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode); //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); //--- Конструкторы/деструктор CTableView(void); CTableView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableView (void){} };
Рассмотрим объявленные методы.
Метод, обновляющий изменённую таблицу:
//+------------------------------------------------------------------+ //| CTableView::Обновляет изменённую таблицу | //+------------------------------------------------------------------+ bool CTableView::UpdateTable(void) { if(this.m_table_area==NULL) return false; int total_model=(int)this.m_table_model.RowsTotal(); // Количество строк в модели int total_view =this.m_table_area.AttachedElementsTotal(); // Количество строк в визуальном представлении int diff=total_model-total_view; // Разница в количестве строк двух компонентов int y=1; // Смещение по вертикали int table_height=0; // Рассчитываемая высота панели CTableRowView *row=NULL; // Указатель на объект визуального представления строки //--- Если в модели больше строк, чем в визуальном представлении - создадим недостающие строки в визуальном предаставлении в конце списка if(diff>0) { //--- Получаем последнюю строку визуального представления таблицы (на основе её координат будут размещены добавляемые строки) row=this.m_table_area.GetAttachedElementAt(total_view-1); //--- В цикле по количеству недостающих строк for(int i=total_view;i<total_view+diff;i++) { //--- создаём и присоединяем к панели diff количество объектов визуального представления строки таблицы row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_VIEW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H); if(row==NULL) return false; } } //--- Если в визуальном представлении больше строк, чем в модели - удалим лишние строки в визуальном предаставлении в конце списка if(diff<0) { CListElm *list=this.m_table_area.GetListAttachedElements(); if(list==NULL) return false; int start=total_view-1; int end=start-diff; bool res=true; for(int i=start;i>end;i--) res &=list.Delete(i); if(!res) return false; } //--- В цикле по списку строк модели таблицы for(int i=0;i<total_model;i++) { //--- получаем из списка панели строк очередной объект визуального представления строки таблицы row=this.m_table_area.GetAttachedElementAt(i); if(row==NULL) return false; //--- Проверим тип объекта if(row.Type()!=ELEMENT_TYPE_TABLE_ROW_VIEW) continue; //--- Устанавливаем идентификатор строки row.SetID(i); //--- В зависимости от номера строки (чет/нечет) устанавливаем цвет её фона if(row.ID()%2==0) row.InitBackColorDefault(clrWhite); else row.InitBackColorDefault(C'242,242,242'); row.BackColorToDefault(); row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4)); //--- Получаем модель строки из объекта таблицы CTableRow *row_model=this.m_table_model.GetRow(i); if(row_model==NULL) return false; //--- Обновляем ячейки объекта строки таблицы по модели строки row.TableRowModelUpdate(row_model); //--- Рассчитываем новое значение высоты панели table_height+=row.Height(); } //--- Возвращаем результат изменения размера панели на рассчитанное в цикле значение return this.m_table_area.ResizeH(table_height+y); }
Логика метода прокомментирована в коде. Сравнивается количество строк в модели таблицы и в визуальном представлении. Если есть разница, то в визуальном представлении либо добавляются недостающие, либо удаляются лишние строки таблицы. Затем в цикле по строкам модели таблицы обновляются ячейки в визуальном представлении таблицы. По окончании обновления всех строк визуального представления, возвращается результат изменения размера таблицы под новое количество строк.
Обработчик пользовательского события элемента при щелчке на области объекта:
//+------------------------------------------------------------------+ //| CTableView::Обработчик пользовательского события элемента | //| при щелчке на области объекта | //+------------------------------------------------------------------+ void CTableView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam) { if(id==CHARTEVENT_OBJECT_CLICK && lparam>=0 && dparam>=0) return; //--- Получаем из sparam наименование объекта заголовка таблицы int len=::StringLen(this.NameFG()); string header_str=::StringSubstr(sparam,0,len); //--- Если извлечённое имя не совпадает с именем этого объекта - не наше событие, уходим if(header_str!=this.NameFG()) return; //--- Записываем индекс заголовка столбца //--- Так как в стандартном событии OBJECT_CLICK в lparam и dparam передаются координаты курсора, //--- то для этого обработчика передаётся отрицательное значение индекса заголовка, по которому было событие int index=(int)::fabs(lparam+1000); //--- Получаем заголовок столбца по индексу CColumnCaptionView *caption=this.GetColumnCaption(index); if(caption==NULL) return; //--- Сортируем список строк по значению сортировки в заголовке столбца и обновляем таблицу this.Sort(index,caption.SortMode()); if(this.UpdateTable()) this.Draw(true); }
Метод не обрабатывает события, если lparam и dparam не меньше нуля. Далее получаем из lparam индекс столбца и сортируем таблицу по режиму сортировки, указанному в полученном заголовке столбца, после чего таблица обновляется.
Доработку классов мы завершили. Теперь сделаем новый класс, который позволит нам достаточно комфортно создавать таблицы из заранее подготовленных данных. И управление таблицами тоже будет сделано в этом же классе. Таким образом, и исходя из назначения, это будет класс управления таблицами. Один объект этого класса может содержать в себе множество различных таблиц, доступ к которым будет осуществляться по идентификатору таблицы, либо по её пользовательскому имени.
Класс для упрощенного создания таблиц
Продолжим писать код в файле \MQL5\Indicators\Tables\Controls\Controls.mqh.
//+------------------------------------------------------------------+ //| Класс управления таблицами | //+------------------------------------------------------------------+ class CTableControl : public CPanel { protected: CListObj m_list_table_model; //--- Добавляет объект (1) модели (CTable), (2) визуального представления (CTableView) таблицы в список bool TableModelAdd(CTable *table_model,const int table_id,const string source); CTableView *TableViewAdd(CTable *table_model,const string source); //--- Обновляет указанный столбец указанной таблицы bool ColumnUpdate(const string source, CTable *table_model, const uint table, const uint col, const bool cells_redraw); public: //--- Возвращает (1) модель, (2) объект визуального представления таблицы, (3) тип объекта CTable *GetTable(const uint index) { return this.m_list_table_model.GetNodeAtIndex(index); } CTableView *GetTableView(const uint index) { return this.GetAttachedElementAt(index); } //--- Создание таблицы на основании переданных данных template<typename T> CTableView *TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE); CTableView *TableCreate(const uint num_rows, const uint num_columns,const int table_id=WRONG_VALUE); CTableView *TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE); CTableView *TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE); //--- Возвращает (1) строковое значение указанной ячейки (Model), указанную (2) строку, (3) ячейку таблицы (View) string CellValueAt(const uint table, const uint row, const uint col); CTableRowView *GetRowView(const uint table, const uint index); CTableCellView *GetCellView(const uint table, const uint row, const uint col); //--- Устанавливает (1) значение, (2) точность, (3) флаги отображения времени, (4) флаг отображения имён цветов в указанную ячейку (Model + View) template<typename T> void CellSetValue(const uint table, const uint row, const uint col, const T value, const bool chart_redraw); void CellSetDigits(const uint table, const uint row, const uint col, const int digits, const bool chart_redraw); void CellSetTimeFlags(const uint table, const uint row, const uint col, const uint flags, const bool chart_redraw); void CellSetColorNamesFlag(const uint table, const uint row, const uint col, const bool flag, const bool chart_redraw); //--- Устанавливает цвет переднего плана в указанную ячейку (View) void CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw); //--- (1) Устанавливает, (2) возвращает точку привязки текста в указанной ячейке (View) void CellSetTextAnchor(const uint table, const uint row, const uint col, const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw); ENUM_ANCHOR_POINT CellTextAnchor(const uint table, const uint row, const uint col); //--- Устанавливает (1) точность, (2) флаги отображения времени, (3) флаг отображения имён цветов, (4) точку привязки текста, (5) тип данных в указанном столбце (View) void ColumnSetDigits(const uint table, const uint col, const int digits, const bool cells_redraw, const bool chart_redraw); void ColumnSetTimeFlags(const uint table, const uint col, const uint flags, const bool cells_redraw, const bool chart_redraw); void ColumnSetColorNamesFlag(const uint table, const uint col, const bool flag, const bool cells_redraw, const bool chart_redraw); void ColumnSetTextAnchor(const uint table, const uint col, const ENUM_ANCHOR_POINT anchor, const bool cells_redraw, const bool chart_redraw); void ColumnSetDatatype(const uint table, const uint col, const ENUM_DATATYPE type, const bool cells_redraw, const bool chart_redraw); //--- Тип объекта virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_CONTROL_VIEW); } //--- Конструкторы/деструктор CTableControl(void) { this.m_list_table_model.Clear(); } CTableControl(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableControl(void) {} };
Класс представляет собой Панель, на которой размещаются таблицы (одна, или несколько), и состоит из двух списков:
- список моделей таблиц,
- список визуального представления таблиц, созданных на основе соответствующих моделей.
Методы класса позволяют получить нужную таблицу и управлять её внешним видом, строками, столбцами и ячейками.
Рассмотрим объявленные методы класса.
В конструкторе класса очищается список моделей таблиц и устанавливается наименование по умолчанию:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CTableControl::CTableControl(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,"",chart_id,wnd,x,y,w,h) { this.m_list_table_model.Clear(); this.SetName("Table Control"); }
После создания объекта, и если планируется не один объект этого класса, то наименование следует установить собственное уникальное — чтобы в списке прикреплённых к панели объектов не было двух и более элементов с одним и тем же наименованием.
Метод, добавляющий объект модели таблицы в список:
//+------------------------------------------------------------------+ //| Добавляет объект модели таблицы (CTable) в список | //+------------------------------------------------------------------+ bool CTableControl::TableModelAdd(CTable *table_model,const int table_id,const string source) { //--- Проверяем объект модели таблицы if(table_model==NULL) { ::PrintFormat("%s::%s: Error. Failed to create Table Model object",source,__FUNCTION__); return false; } //--- Устанавливаем в модель таблицы идентификатор - либо по размеру списка, либо заданный table_model.SetID(table_id<0 ? this.m_list_table_model.Total() : table_id); //--- Если модель таблицы с установленным идентификатором есть в списке - сообщаем об этом, удаляем объект и возвращаем false this.m_list_table_model.Sort(0); if(this.m_list_table_model.Search(table_model)!=NULL) { ::PrintFormat("%s::%s: Error: Table Model object with ID %d already exists in the list",source,__FUNCTION__,table_id); delete table_model; return false; } //--- Если модель таблицы не добавлена в список - сообщаем об этом, удаляем объект и возвращаем false if(this.m_list_table_model.Add(table_model)<0) { ::PrintFormat("%s::%s: Error. Failed to add Table Model object to list",source,__FUNCTION__); delete table_model; return false; } //--- Всё успешно return true; }
Метод, создающий новый и добавляющий в список объект визуального представления таблицы:
//+------------------------------------------------------------------+ //| Создаёт новый и добавляет в список объект | //| визуального представления таблицы (CTableView) | //+------------------------------------------------------------------+ CTableView *CTableControl::TableViewAdd(CTable *table_model,const string source) { //--- Проверяем объект модели таблицы if(table_model==NULL) { ::PrintFormat("%s::%s: Error. An invalid Table Model object was passed",source,__FUNCTION__); return NULL; } //--- Создаём новый элемент - визуальное представление таблицы, прикреплённый к панели CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,"","TableView"+(string)table_model.ID(),1,1,this.Width()-2,this.Height()-2); if(table_view==NULL) { ::PrintFormat("%s::%s: Error. Failed to create Table View object",source,__FUNCTION__); return NULL; } //--- Графическому элементу "Таблица" (View) назначаем объект таблицы (Model) и его идентификатор table_view.TableObjectAssign(table_model); table_view.SetID(table_model.ID()); return table_view; }
Оба вышерассмотренных метода используются в методах создания таблицы.
Метод, создающий таблицу с указанием массива таблицы и массива заголовков:
//+-------------------------------------------------------------------+ //| Создаёт таблицу с указанием массива таблицы и массива заголовков. | //| Определяет количество и наименования колонок согласно column_names| //| Количество строк определены размером массива данных row_data, | //| который используется и для заполнения таблицы | //+-------------------------------------------------------------------+ template<typename T> CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE) { //--- Создаём объект таблицы по указанным параметрам CTable *table_model=new CTable(row_data,column_names); //--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Создаём и возвращаем таблицу return this.TableViewAdd(table_model,__FUNCTION__); }
Метод, создающий таблицу с определением количества колонок и строк:
//+------------------------------------------------------------------+ //| Создаёт таблицу с определением количества колонок и строк. | //| Колонки будут иметь Excel-наименования "A", "B", "C" и т.д. | //+------------------------------------------------------------------+ CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,const int table_id=WRONG_VALUE) { CTable *table_model=new CTable(num_rows,num_columns); //--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Создаём и возвращаем таблицу return this.TableViewAdd(table_model,__FUNCTION__); }
Метод, создающий таблицу из матрицы:
//+------------------------------------------------------------------+ //| Создаёт таблицу с инициализацией колонок согласно column_names | //| Количество строк определены параметром row_data, с типом matrix | //+------------------------------------------------------------------+ CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE) { CTable *table_model=new CTable(row_data,column_names); //--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Создаём и возвращаем таблицу return this.TableViewAdd(table_model,__FUNCTION__); }
Метод, создающий таблицу на основе списка пользовательских параметров и массива заголовков столбцов:
//+------------------------------------------------------------------+ //| Создаёт таблицу с указанием массива таблицы на основе | //| списка row_data, содержащего объекты с данными полей структуры. | //| Определяет количество и наименования колонок согласно количеству | //| наименований столбцов в массиве column_names | //+------------------------------------------------------------------+ CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE) { CTableByParam *table_model=new CTableByParam(row_data,column_names); //--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Создаём и возвращаем таблицу return this.TableViewAdd(table_model,__FUNCTION__); }
Логика всех представленных методов создания таблицы одинакова: сначала на основании входных параметров метода создаётся и добавляется в список объект модели таблицы, а затем — визуальное представление. Это основные методы для создания таблиц из широкого набора данных. Создаваемая рассмотренными методами таблица помещается на панели, являющейся подложкой для размещения создаваемых таблиц.
Метод, устанавливающий значение в указанную ячейку:
//+------------------------------------------------------------------+ //| Устанавливает значение в указанную ячейку (Model + View) | //+------------------------------------------------------------------+ template<typename T> void CTableControl::CellSetValue(const uint table,const uint row,const uint col,const T value,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Из модели таблицы получаем модель ячейки CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL) return; //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Сравниваем установленное в ячейке значение с переданным bool equal=false; ENUM_DATATYPE datatype=cell_model.Datatype(); switch(datatype) { case TYPE_LONG : case TYPE_DATETIME: case TYPE_COLOR : equal=(cell_model.ValueL()==value); break; case TYPE_DOUBLE : equal=(::NormalizeDouble(cell_model.ValueD()-value,cell_model.Digits())==0); break; //---TYPE_STRING default : equal=(::StringCompare(cell_model.ValueS(),(string)value)==0); break; } //--- Если значения равны - уходим if(equal) return; //--- В модель ячейки устанавливаем новое значение; //--- в объект визуального представления ячейки вписываем значение из модели ячейки //--- Перерисовываем ячейку с флагом обновления графика table_model.CellSetValue(row,col,value); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
Логика метода расписана в комментариях к коду. Если в ячейке установлено такое же значение, что и передано в метод для записи в ячейку, то осуществляется выход из метода. Новое значение сначала записывается в ячейку модели таблицы, затем в ячейку визуального представления таблицы, и эта ячейка перерисовывается.
Метод, устанавливающий точность отображения дробных чисел в указанную ячейку:
//+------------------------------------------------------------------+ //| Устанавливает точность в указанную ячейку (Model + View) | //+------------------------------------------------------------------+ void CTableControl::CellSetDigits(const uint table,const uint row,const uint col,const int digits,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Из модели таблицы получаем модель ячейки CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL || cell_model.Digits()==digits) return; //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- В модель ячейки устанавливаем новое значение точности; //--- в объект визуального представления ячейки вписываем значение из модели ячейки //--- Перерисовываем ячейку с флагом обновления графика table_model.CellSetDigits(row,col,digits); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
Метод подобен вышерассмотренному. Если указанная точность дробных чисел равна фактической, то уходим из метода. Далее точность записывается в модель ячейки, в визуальное представление записывается текст из модели ячейки (для дробных чисел там уже изменённая точность), и ячейка перерисовывается.
Метод, устанавливающий флаги отображения времени в указанную ячейку:
//+------------------------------------------------------------------+ //| Устанавливает флаги отображения времени | //| в указанную ячейку (Model + View) | //+------------------------------------------------------------------+ void CTableControl::CellSetTimeFlags(const uint table,const uint row,const uint col,const uint flags,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Из модели таблицы получаем модель ячейки CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL || cell_model.DatetimeFlags()==flags) return; //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- В модель ячейки устанавливаем новое значение флагов отображения времени; //--- в объект визуального представления ячейки вписываем значение из модели ячейки //--- Перерисовываем ячейку с флагом обновления графика table_model.CellSetTimeFlags(row,col,flags); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
Логика метода идентична методу, устанавливающему точность в ячейку.
Метод, устанавливающий флаг отображения имён цветов в указанную ячейку:
//+------------------------------------------------------------------+ //| Устанавливает флаг отображения имён цветов | //| в указанную ячейку (Model + View) | //+------------------------------------------------------------------+ void CTableControl::CellSetColorNamesFlag(const uint table,const uint row,const uint col,const bool flag,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Из модели таблицы получаем модель ячейки CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL || cell_model.ColorNameFlag()==flag) return; //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- В модель ячейки устанавливаем новое значение флага отображения имён цветов; //--- в объект визуального представления ячейки вписываем значение из модели ячейки //--- Перерисовываем ячейку с флагом обновления графика table_model.CellSetColorNamesFlag(row,col,flag); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
Точно такой же метод, как и вышерассмотренные.
Метод, устанавливающий цвет переднего плана в указанную ячейку:
//+------------------------------------------------------------------+ //| Устанавливает цвет переднего плана в указанную ячейку (View) | //+------------------------------------------------------------------+ void CTableControl::CellSetForeColor(const uint table,const uint row,const uint col,const color clr,const bool chart_redraw) { //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- В объект визуального представления ячейки устанавливаем цвет фона ячейки //--- Перерисовываем ячейку с флагом обновления графика cell_view.SetForeColor(clr); cell_view.Draw(chart_redraw); }
Получаем объект визуального представления ячейки, устанавливаем для него новый цвет переднего плана и перерисовываем ячейку с новым цветом текста.
Метод, устанавливающий точку привязки текста в указанную ячейку:
//+------------------------------------------------------------------+ //| Устанавливает точку привязки текста в указанную ячейку (View) | //+------------------------------------------------------------------+ void CTableControl::CellSetTextAnchor(const uint table,const uint row,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw) { //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- В объект визуального представления ячейки устанавливаем точку привязки текста //--- Перерисовываем ячейку с флагом обновления графика cell_view.SetTextAnchor(anchor,cell_redraw,chart_redraw); }
Получаем объект визуального представления ячейки, устанавливаем для него новую точку привязки и перерисовываем ячейку с новым расположением текста в ней.
Метод, возвращающий точку привязки текста в указанной ячейке:
//+------------------------------------------------------------------+ //| Возвращает точку привязки текста в указанной ячейке (View) | //+------------------------------------------------------------------+ ENUM_ANCHOR_POINT CTableControl::CellTextAnchor(const uint table,const uint row,const uint col) { //--- Получаем объект визуального представления ячейки CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return ANCHOR_LEFT_UPPER; //--- Возвращаем точку привязки текста return((ENUM_ANCHOR_POINT)cell_view.TextAnchor()); }
Получаем объект визуального представления ячейки и возвращаем установленную для него точку привязки текста.
Метод, обновляющий указанный столбец указанной таблицы:
//+------------------------------------------------------------------+ //| Обновляет указанный столбец указанной таблицы | //+------------------------------------------------------------------+ bool CTableControl::ColumnUpdate(const string source,CTable *table_model,const uint table,const uint col,const bool cells_redraw) { //--- Проверяем модель таблицы if(::CheckPointer(table_model)==POINTER_INVALID) { ::PrintFormat("%s::%s: Error. Invalid table model pointer passed",source,__FUNCTION__); return false; } //--- Получаем визуальное представление таблицы CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s::%s: Error. Failed to get CTableView object",source,__FUNCTION__); return false; } //--- В цикле по строкам визуального представления таблицы int total=table_view.RowsTotal(); for(int i=0;i<total;i++) { //--- получаем из очередной строки таблицы объект визуального представление ячейки в указанном столбце CTableCellView *cell_view=this.GetCellView(table,i,col); if(cell_view==NULL) { ::PrintFormat("%s::%s: Error. Failed to get CTableCellView object (row %d, col %u)",source,__FUNCTION__,i,col); return false; } //--- Получаем модель соответствующей ячейки из модели строки CTableCell *cell_model=table_model.GetCell(i,col); if(cell_model==NULL) { ::PrintFormat("%s::%s: Error. Failed to get CTableCell object (row %d, col %u)",source,__FUNCTION__,i,col); return false; } //--- В объект визуального представление ячейки записываем значение из модели ячейки cell_view.SetText(cell_model.Value()); //--- Если указано - перерисовываем визуальное представление ячейки if(cells_redraw) cell_view.Draw(false); } return true; }
Логика метода расписана в комментариях. После того, как модель таблицы обновлена, передаём её в этот метод. И здесь в цикле по всем строкам визуального представления таблицы получаем поочерёдно каждую новую строку, а из соответствующей строки модели — указанную ячейку. В визуальное представление ячейки записываем данные из модели ячейки, и визуальное представление ячейки перерисовывается. Таким образом перерисовывается весь столбец таблицы.
Метод, устанавливающий точность в указанном столбце:
//+------------------------------------------------------------------+ //| Устанавливает точность в указанном столбце (Model + View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetDigits(const uint table,const uint col,const int digits,const bool cells_redraw,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Устанавливаем Digits для указанного столбца в модели таблицы table_model.ColumnSetDigits(col,digits); //--- Обновляем отображение данных столбца и, если указано, перерисовываем график if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
В модели таблицы устанавливаем для столбца указанную точность и вызываем метод обновления столбца таблицы, рассмотренный выше.
Метод, устанавливающий флаги отображения времени в указанном столбце:
//+------------------------------------------------------------------+ //| Устанавливает флаги отображения времени | //| в указанном столбце (Model + View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetTimeFlags(const uint table,const uint col,const uint flags,const bool cells_redraw,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Устанавливаем флаги отображения времени для указанного столбца в модели таблицы table_model.ColumnSetTimeFlags(col,flags); //--- Обновляем отображение данных столбца и, если указано, перерисовываем график if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
В модели таблицы устанавливаем для столбца указанные флаги отображения времени и вызываем метод обновления столбца таблицы.
Метод, устанавливающий флаг отображения имён цвета в указанном столбце:
//+------------------------------------------------------------------+ //| Устанавливает флаг отображения имён цвета | //| в указанном столбце (Model + View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetColorNamesFlag(const uint table,const uint col,const bool flag,const bool cells_redraw,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Устанавливаем флаги отображения времени для указанного столбца в модели таблицы table_model.ColumnSetColorNamesFlag(col,flag); //--- Обновляем отображение данных столбца и, если указано, перерисовываем график if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
В модели таблицы устанавливаем для столбца указанный флаг отображения имён цвета и вызываем метод обновления столбца таблицы.
Метод, устанавливающий тип данных в указанном столбце:
//+------------------------------------------------------------------+ //| Устанавливает тип данных в указанном столбце ( (Model + View)) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetDatatype(const uint table,const uint col,const ENUM_DATATYPE type,const bool cells_redraw,const bool chart_redraw) { //--- Получаем модель таблицы CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Устанавливаем тип данных для указанного столбца в модели таблицы table_model.ColumnSetDatatype(col,type); //--- Обновляем отображение данных столбца и, если указано, перерисовываем график if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
В модели таблицы устанавливаем для столбца указанный тип данных и вызываем метод обновления столбца таблицы.
Метод, устанавливающий точку привязки текста в указанном столбце:
//+------------------------------------------------------------------+ //| Устанавливает точку привязки текста в указанном столбце (View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetTextAnchor(const uint table,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cells_redraw,const bool chart_redraw) { //--- Получаем визуальное представление таблицы CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__); return; } //--- В цикле по всем строкам таблицы int total=table_view.RowsTotal(); for(int i=0;i<total;i++) { //--- получаем очередной объект визуального представления ячейки //--- и вписываем в объект новую точку привязки CTableCellView *cell_view=this.GetCellView(table,i,col); if(cell_view!=NULL && cell_view.TextAnchor()!=anchor) cell_view.SetTextAnchor(anchor,cells_redraw,false); } //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
В цикле по всем строкам визуального представления таблицы из очередной строки получаем указанную ячейку и вписываем в неё новую точку привязки. По окончании цикла обновляем график.
Метод, возвращающий строковое значение указанной ячейки:
//+------------------------------------------------------------------+ //| Возвращает строковое значение указанной ячейки (Model) | //+------------------------------------------------------------------+ string CTableControl::CellValueAt(const uint table,const uint row,const uint col) { CTable *table_model=this.GetTable(table); return(table_model!=NULL ? table_model.CellValueAt(row,col) : ::StringFormat("%s: Error. Failed to get table model",__FUNCTION__)); }
Из модели таблицы получаем требуемую ячейку и возвращаем её значение в виде строки. При ошибке возвращается текст ошибки.
Метод, возвращающий указанную строку таблицы:
//+------------------------------------------------------------------+ //| Возвращает указанную строку таблицы (View) | //+------------------------------------------------------------------+ CTableRowView *CTableControl::GetRowView(const uint table,const uint index) { CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__); return NULL; } return table_view.GetRowView(index); }
Получаем визуальное представление таблицы по индексу и возвращаем из него указатель на требуемую строку.
Метод, возвращающий указанную ячейку таблицы:
//+------------------------------------------------------------------+ //| Возвращает указанную ячейку таблицы (View) | //+------------------------------------------------------------------+ CTableCellView *CTableControl::GetCellView(const uint table,const uint row,const uint col) { CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__); return NULL; } return table_view.GetCellView(row,col); }
Получаем визуальное представление таблицы по индексу и возвращаем из него указатель на требуемую ячейку указанной строки.
Как видим, этот класс является простым вспомогательным классом для построения таблиц и работы с ними. Все его методы представляют сервисный функционал для обработки, изменения, установки и получения табличных данных, используя методы из ранее написанных классов модели и визуального представления таблицы.
Давайте проверим, что у нас получилось.
Тестируем результат
Откроем файл тестового индикатора \MQL5\Indicators\Tables\iTestTable.mq5 и перезапишем его в таком виде (ранее подготовленные данные и создание таблицы выделены соответствующими цветами):
//+------------------------------------------------------------------+ //| iTestTable.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0 //+------------------------------------------------------------------+ //| Включаемые библиотеки | //+------------------------------------------------------------------+ #include "Controls\Controls.mqh" // Библиотека элементов управления //--- Указатель на объект CTableControl CTableControl *table_ctrl; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Ищем подокно графика int wnd=ChartWindowFind(); //--- Создаём данные для таблицы //--- Объявляем и заполняем массив заголовков столбцов с размерностью 4 string captions[]={"Column 0","Column 1","Column 2","Column 3"}; //--- Объявляем и заполняем массив данных с размерностью 15x4 //--- Тип массива может быть double, long, datetime, color, string long array[15][4]={{ 1, 2, 3, 4}, { 5, 6, 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24}, {25, 26, 27, 28}, {29, 30, 31, 32}, {33, 34, 35, 36}, {37, 38, 39, 40}, {41, 42, 43, 44}, {45, 46, 47, 48}, {49, 50, 51, 52}, {53, 54, 55, 56}, {57, 58, 59, 60}}; //--- Создаём графический элемент управления таблицами table_ctrl=new CTableControl("TableControl0",0,wnd,30,30,460,184); if(table_ctrl==NULL) return INIT_FAILED; //--- На графике обязательно должен быть один главный элемент table_ctrl.SetAsMain(); //--- Можно установить параметры элемента управления таблицами table_ctrl.SetID(0); // Идентификатор table_ctrl.SetName("Table Control 0"); // Наименование //--- Создаём объект таблицы 0 (компонент Model + View) из вышесозданного long-массива array 15x4 и string-массива заголовков столбцов if(table_ctrl.TableCreate(array,captions)==NULL) return INIT_FAILED; //--- Дополнительно установим для столбцов 1,2,3 вывод текста по центру ячейки, а для столбца 0 - по левому краю table_ctrl.ColumnSetTextAnchor(0,0,ANCHOR_LEFT,true,false); table_ctrl.ColumnSetTextAnchor(0,1,ANCHOR_CENTER,true,false); table_ctrl.ColumnSetTextAnchor(0,2,ANCHOR_CENTER,true,false); table_ctrl.ColumnSetTextAnchor(0,3,ANCHOR_CENTER,true,false); //--- Нарисуем таблицу table_ctrl.Draw(true); //--- Получим таблицу с индексом 0 и распечатаем в журнале CTable *table=table_ctrl.GetTable(0); table.Print(); //--- Успешно return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom deindicator initialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Удаляем элемент управления таблицами и уничтожаем менеджер общих ресурсов библиотеки delete table_ctrl; CCommonManager::DestroyInstance(); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Вызываем обработчик OnChartEvent элемента управления таблицами table_ctrl.OnChartEvent(id,lparam,dparam,sparam); //--- Если событие - перемещение курсора мышки if(id==CHARTEVENT_MOUSE_MOVE) { //--- получаем координаты курсора int x=table_ctrl.CursorX(); int y=table_ctrl.CursorY(); //--- значение координаты X устанавливаем в ячейку 0 строки 1 table_ctrl.CellSetValue(0,1,0,x,false); //--- значение координаты Y устанавливаем в ячейку 1 строки 1 //--- цвет текста в ячейке зависит от знака координаты Y (при отрицательном значении - красный текст) table_ctrl.CellSetForeColor(0,1,1,(y<0 ? clrRed : table_ctrl.ForeColor()),false); table_ctrl.CellSetValue(0,1,1,y,true); } } //+------------------------------------------------------------------+ //| Таймер | //+------------------------------------------------------------------+ void OnTimer(void) { //--- Вызываем обработчик OnTimer элемента управления таблицами table_ctrl.OnTimer(); } //+------------------------------------------------------------------+
В обработчике событий проверим, как можно записывать в ячейки некоторые данные в реальном времени. Будем считывать координаты курсора и записывать их в две первые ячейки второй сверху строки. При этом отрицательное значение координаты Y курсора будем выводить красным цветом.
Скомпилируем индикатор и запустим его на графике:

Как видим, заявленное взаимодействие таблицы с пользователем работает.
Конечно же, есть некоторые нюансы при отображении визуального представления таблицы в процессе взаимодействия с курсором, но всё это постепенно будет исправлено и доработано.
Заключение
На данный момент мы создали удобный инструмент для отображения различных табличных данных, позволяющий в некоторой степени настроить отображение созданной по умолчанию таблицы.
Есть несколько вариантов данных для создания на их основе таблицы:
- двумерный массив таблицы и массив заголовков столбцов,
- количество колонок и строк,
- матрица: заголовок автоматически создаётся в Excel-стиле,
- список пользовательских параметров CList и массив заголовков столбцов.
В будущем, возможно, классы таблиц будут доработаны для удобного добавления и удаления строк, столбцов и отдельных ячеек. Но пока остановимся на таком формате объекта для создания и отображения таблиц.
Программы, используемые в статье:
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Tables.mqh | Библиотека классов | Классы для создания модели таблицы |
| 2 | Base.mqh | Библиотека классов | Классы для создания базового объекта элементов управления |
| 3 | Controls.mqh | Библиотека классов | Классы элементов управления |
| 4 | iTestTable.mq5 | Тестовый индикатор | Индикатор для тестирования работы с элементом управления TableView |
| 5 | MQL5.zip | Архив | Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От новичка до эксперта: Анимированный советник News Headline с использованием MQL5 (XI) - Корреляция при торговле на новостях
Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Окончание)
Нейросети в трейдинге: Модели многократного уточнения прогнозов (RAFT)
От новичка до эксперта: Создание анимированного советника для новостей в MQL5 (X) — Представление графика с несколькими символами для торговли на новостях
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
щелчок мышкой по заголовку столбца таблицы (работа компонента Controller) повлечёт за собой изменение в расположении данных в модели таблицы (реорганизация компонента Model),