Графические интерфейсы X: Сортировка, реконструкция таблицы и элементы управления в ячейках (build 11)
Содержание
- Введение
- Сортировка таблицы
- Добавление и удаление столбцов и строк
- Событие двойного нажатия левой кнопкой мыши
- Элементы управления в ячейках таблицы
- Заключение
Введение
О том, для чего предназначена эта библиотека, более подробно можно прочитать в самой первой статье: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце статей каждой части представлен список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.
Продолжаем развивать нарисованную таблицу. Перечислим новые возможности, которыми она будет дополнена.
- Сортировка данных таблицы.
- Управление количеством столбцов и строк: добавление и удаление столбцов и строк по указанному индексу; полная очистка таблицы (остаётся только один столбец и одна строка); реконструкция таблицы (полная очистка и установка новой размерности).
- Расширим возможности пользовательского управления графическим интерфейсом: добавляется обработка двойного клика мышью.
- В ячейки таблицы начнем добавлять элементы управления: чекбоксы и кнопки.
Если вы уже создаете графические интерфейсы с помощью этой библиотеки и используете для отображения данных таблицу типа CTable, то теперь вам предлагается перейти на таблицу типа CCanvasTable. Начиная с этой статьи, она полностью сравнялась с другими типами таблиц в этой библиотеке, а по некоторым параметрам — превзошла их.
Сортировка таблицы
Большинство методов для сортировки табличных данных остались такими же, как и в CTable. Как это всё устроено, вы можете подробно изучить в статье Графические интерфейсы X: Элемент "Время", элемент "Список из чекбоксов" и сортировка таблицы. Здесь кратко коснёмся изменений и дополнений, которые относятся к нарисованной таблице типа CCanvasTable.
Для отрисовки признака отсортированной таблицы понадобится статический массив типа CTImage из двух элементов. В нём будут храниться пути к изображениям, которые должны отображать направление сортировки. Если пользователь не установил свой вариант изображений, то будут использоваться картинки по умолчанию. Инициализация значениями по умолчанию и заполнение массивов изображений осуществляются в методе создания заголовков CCanvasTable::CreateHeaders().
//+------------------------------------------------------------------+ //| Класс для создания нарисованной таблицы | //+------------------------------------------------------------------+ class CCanvasTable : public CElement { private: //--- Ярлыки для признака отсортированных данных CTImage m_sort_arrows[2]; //--- public: //--- Установка картинок для признака отсортированных данных void SortArrowFileAscend(const string path) { m_sort_arrows[0].m_bmp_path=path; } void SortArrowFileDescend(const string path) { m_sort_arrows[1].m_bmp_path=path; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) { ... //--- Инициализация структуры признака сортировки m_sort_arrows[0].m_bmp_path=""; m_sort_arrows[1].m_bmp_path=""; } //+------------------------------------------------------------------+ //| Создаёт заголовки таблицы | //+------------------------------------------------------------------+ bool CCanvasTable::CreateHeaders(void) { //--- Выйти, если заголовки отключены if(!m_show_headers) return(true); //--- Формирование имени объекта string name=CElementBase::ProgramName()+"_table_headers_"+(string)CElementBase::Id(); //--- Координаты int x =m_x+1; int y =m_y+1; //--- Определим картинки как признак возможности сортировки таблицы if(m_sort_arrows[0].m_bmp_path=="") m_sort_arrows[0].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp"; if(m_sort_arrows[1].m_bmp_path=="") m_sort_arrows[1].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp"; //--- for(int i=0; i<2; i++) { ::ResetLastError(); if(!::ResourceReadImage(m_sort_arrows[i].m_bmp_path,m_sort_arrows[i].m_image_data, m_sort_arrows[i].m_image_width,m_sort_arrows[i].m_image_height)) { ::Print(__FUNCTION__," > error: ",::GetLastError()); } } //--- Создание объекта ::ResetLastError(); if(!m_headers.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,m_table_x_size,m_header_y_size,COLOR_FORMAT_ARGB_NORMALIZE)) { ::Print(__FUNCTION__," > Не удалось создать холст для рисования заголовков таблицы: ",::GetLastError()); return(false); } //--- Прикрепить к графику //--- Установим свойства //--- Координаты //--- Сохраним размеры //--- Отступы от крайней точки панели //--- Сохраним указатель объекта //--- Установим размеры видимой области //--- Зададим смещение фрейма внутри изображения по осям X и Y ... return(true); }
Для отрисовки признака отсортированного массива будет использоваться метод CCanvasTable::DrawSignSortedData().Этот элемент отрисовывается только если (1) режим сортировки включен и (2) сортировка данных таблицы уже была. Отступы можно контролировать с помощью методов CCanvasTable::SortArrowXGap() и CCanvasTable::SortArrowYGap(). Картинка обозначает, что сортировка по возрастанию будет иметь индекс 0, а сортировка по убыванию – 1. Далее следует двойной цикл рисования изображения. Эта тема уже была подробно рассмотрена.
class CCanvasTable : public CElement { private: //--- Отступы для ярлыка-признака отсортированных данных int m_sort_arrow_x_gap; int m_sort_arrow_y_gap; //--- public: //--- Отступы для признака отсортированной таблицы void SortArrowXGap(const int x_gap) { m_sort_arrow_x_gap=x_gap; } void SortArrowYGap(const int y_gap) { m_sort_arrow_y_gap=y_gap; } //--- private: //--- Рисует признак возможности сортировки таблицы void DrawSignSortedData(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) : m_sort_arrow_x_gap(20), m_sort_arrow_y_gap(6) { ... } //+------------------------------------------------------------------+ //| Рисует признак возможности сортировки таблицы | //+------------------------------------------------------------------+ void CCanvasTable::DrawSignSortedData(void) { //--- Выйти, если (1) сортировка отключена или (2) ещё не была осуществлена if(!m_is_sort_mode || m_is_sorted_column_index==WRONG_VALUE) return; //--- Расчёт координат int x =m_columns[m_is_sorted_column_index].m_x2-m_sort_arrow_x_gap; int y =m_sort_arrow_y_gap; //--- Выбранная картинка по направлению сортировки int image_index=(m_last_sort_direction==SORT_ASCEND)? 0 : 1; //--- Рисуем for(uint ly=0,i=0; ly<m_sort_arrows[image_index].m_image_height; ly++) { for(uint lx=0; lx<m_sort_arrows[image_index].m_image_width; lx++,i++) { //--- Если нет цвета, перейти к следующему пикселю if(m_sort_arrows[image_index].m_image_data[i]<1) continue; //--- Получаем цвет нижнего слоя (фона заголовка) и цвет указанного пикселя картинки uint background =m_headers.PixelGet(x+lx,y+ly); uint pixel_color =m_sort_arrows[image_index].m_image_data[i]; //--- Смешиваем цвета uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); //--- Рисуем пиксель наслаиваемого изображения m_headers.PixelSet(x+lx,y+ly,foreground); } } }
При сортировке значений таблицы, чтобы поменять их местами, используется метод CCanvasTable::Swap(). Он отличается тем, что здесь нужно перемещать не только отображаемый в ячейке текст и/или картинки, но и большее количество свойств ячейки. При перемещении картинок для удобства, чтобы исключить повторяющиеся фрагменты кода, понадобится вспомогательный метод CCanvasTable::ImageCopy(). В него передаются два массива типа CTImage — приемник и источник данных. В качестве третьего аргумента передаётся индекс копируемого изображения, так как в одной ячейке их может быть несколько.
class CCanvasTable : public CElement { private: //--- Копирует данные изображения из одного массива в другой void ImageCopy(CTImage &destination[],CTImage &source[],const int index); }; //+------------------------------------------------------------------+ //| Копирует данные изображения из одного массива в другой | //+------------------------------------------------------------------+ void CCanvasTable::ImageCopy(CTImage &destination[],CTImage &source[],const int index) { //--- Копируем пиксели картинки ::ArrayCopy(destination[index].m_image_data,source[index].m_image_data); //--- Копируем свойства картинки destination[index].m_image_width =source[index].m_image_width; destination[index].m_image_height =source[index].m_image_height; destination[index].m_bmp_path =source[index].m_bmp_path; }
В итоге код метода CCanvasTable::Swap() выглядит так, как показано в листинге кода ниже. Перед тем, как переместить изображения, нужно сначала проверить, есть ли они, и если нет, то перейти к следующему столбцу. Если же в одной из ячеек есть картинки, то с помощью метода CCanvasTable::ImageCopy() они перемещаются. Эта операция такая же, как и в случае с перемещением других свойств ячейки. То есть, сначала запоминаем свойство первой ячейки. Затем на место первой копируем это же свойство из второй ячейки. И наконец, на место второй ячейки помещаем ранее сохранённое значение из первой.
class CCanvasTable : public CElement { private: //--- Поменять значения в указанных ячейках местами void Swap(uint r1,uint r2); }; //+------------------------------------------------------------------+ //| Меняет элементы местами | //+------------------------------------------------------------------+ void CCanvasTable::Swap(uint r1,uint r2) { //--- Пройдёмся в цикле по всем столбцам for(uint c=0; c<m_columns_total; c++) { //--- Меняем местами полный текст string temp_text =m_columns[c].m_rows[r1].m_full_text; m_columns[c].m_rows[r1].m_full_text =m_columns[c].m_rows[r2].m_full_text; m_columns[c].m_rows[r2].m_full_text =temp_text; //--- Меняем местами краткий текст temp_text =m_columns[c].m_rows[r1].m_short_text; m_columns[c].m_rows[r1].m_short_text =m_columns[c].m_rows[r2].m_short_text; m_columns[c].m_rows[r2].m_short_text =temp_text; //--- Меняем местами количество знаков после запятой uint temp_digits =m_columns[c].m_rows[r1].m_digits; m_columns[c].m_rows[r1].m_digits =m_columns[c].m_rows[r2].m_digits; m_columns[c].m_rows[r2].m_digits =temp_digits; //--- Меняем местами цвет текста color temp_text_color =m_columns[c].m_rows[r1].m_text_color; m_columns[c].m_rows[r1].m_text_color =m_columns[c].m_rows[r2].m_text_color; m_columns[c].m_rows[r2].m_text_color =temp_text_color; //--- Меняем местами индекс выбранной картинки int temp_selected_image =m_columns[c].m_rows[r1].m_selected_image; m_columns[c].m_rows[r1].m_selected_image =m_columns[c].m_rows[r2].m_selected_image; m_columns[c].m_rows[r2].m_selected_image =temp_selected_image; //--- Проверим, есть ли картинки в ячейках int r1_images_total=::ArraySize(m_columns[c].m_rows[r1].m_images); int r2_images_total=::ArraySize(m_columns[c].m_rows[r2].m_images); //--- Переходим к следующему столбцу, если в обеих ячейках нет картинок if(r1_images_total<1 && r2_images_total<1) continue; //--- Меняем местами картинки CTImage r1_temp_images[]; //--- ::ArrayResize(r1_temp_images,r1_images_total); for(int i=0; i<r1_images_total; i++) ImageCopy(r1_temp_images,m_columns[c].m_rows[r1].m_images,i); //--- ::ArrayResize(m_columns[c].m_rows[r1].m_images,r2_images_total); for(int i=0; i<r2_images_total; i++) ImageCopy(m_columns[c].m_rows[r1].m_images,m_columns[c].m_rows[r2].m_images,i); //--- ::ArrayResize(m_columns[c].m_rows[r2].m_images,r1_images_total); for(int i=0; i<r1_images_total; i++) ImageCopy(m_columns[c].m_rows[r2].m_images,r1_temp_images,i); } }
Нажатие на один из заголовков таблицы вызывает метод CCanvasTable::OnClickHeaders(), который запускает сортировку данных. В этом классе он значительно проще, чем в таблице типа CTable: сравните и убедитесь в этом сами.
class CCanvasTable : public CElement { private: //--- Обработка нажатия на заголовке bool OnClickHeaders(const string clicked_object); }; //+------------------------------------------------------------------+ //| Обработка нажатия на заголовке | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickHeaders(const string clicked_object) { //--- Выйти, если (1) режим сортировки отключен или (2) в процессе изменения ширины столбца if(!m_is_sort_mode || m_column_resize_control!=WRONG_VALUE) return(false); //--- Выйдем, если полоса прокрутки в активном режиме if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false); //--- Выйдем, если чужое имя объекта if(m_headers.Name()!=clicked_object) return(false); //--- Для определения индекса столбца uint column_index=0; //--- Получим относительную X-координату под курсором мыши int x=m_mouse.RelativeX(m_headers); //--- Определим заголовок, на котором нажали for(uint c=0; c<m_columns_total; c++) { //--- Если нашли заголовок, запомним его индекс if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2) { column_index=c; break; } } //--- Сортировка данных по указанному столбцу SortData(column_index); //--- Отправим сообщение об этом ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index))); return(true); }
Остальные методы для сортировки данных остались без изменения и ничем не отличаются от ранее рассмотренных. Поэтому рассмотрим только список объявлений этих полей и методов в классе CCanvasTable:
class CCanvasTable : public CElement { private: //--- Режим сортируемых данных по столбцам bool m_is_sort_mode; //--- Индекс отсортированного столбца (WRONG_VALUE - таблица не отсортирована) int m_is_sorted_column_index; //--- Последнее направление сортировки ENUM_SORT_MODE m_last_sort_direction; //--- public: //--- Режим сортируемых данных void IsSortMode(const bool flag) { m_is_sort_mode=flag; } //--- Получение/установка типа данных ENUM_DATATYPE DataType(const uint column_index); void DataType(const uint column_index,const ENUM_DATATYPE type); //--- Сортировать данные по указанному столбцу void SortData(const uint column_index=0); //--- private: //--- Обработка нажатия на заголовке bool OnClickHeaders(const string clicked_object); //--- Метод быстрой сортировки void QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND); //--- Проверка условия сортировки bool CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction); };
Ниже вы можете посмотреть, как работает сортировка в этом типе таблиц:
Рис. 1. Демонстрация сортировки в таблице типа CCanvasTable.
Добавление и удаление столбцов и строк
В одной из предыдущих статей для таблиц типа CTable рассматривались методы для добавления и удаления столбцов и строк. В той версии добавление было возможно только в конце таблицы. Сейчас устраним это досадное неудобство: сделаем так, чтобы можно было добавлять столбец или строку, указывая индекс места их добавления.
Пример: добавим столбец в начало таблицы с данными: то есть, новый столбец должен стать первым (индекс 0). Массив столбцов должен увеличиться на один элемент, а свойства и значения всех ячеек таблицы — переместиться на один элемент вправо, оставив пустыми ячейки только нового столбца. Такой же принцип действует и при добавлении строк таблицы.
Обратный принцип работает, когда нужно удалить столбец или строку. Попробуем удалить первый столбец из предыдущего нашего примера: сначала данные и свойства ячеек сместятся на один элемент влево, и уже после этого размер массива столбцов уменьшится на один элемент.
Здесь для удобства создания методов добавления и удаления столбцов и строк реализованы вспомогательные методы. Для инициализации ячеек, добавленных столбцов и строк значениями по умолчанию будут использоваться методы CCanvasTable::ColumnInitialize() и CCanvasTable::CellInitialize().
class CCanvasTable : public CElement { private: //--- Инициализация указанного столбца значениями по умолчанию void ColumnInitialize(const uint column_index); //--- Инициализация указанной ячейки значениями по умолчанию void CellInitialize(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Инициализация указанного столбца значениями по умолчанию | //+------------------------------------------------------------------+ void CCanvasTable::ColumnInitialize(const uint column_index) { //--- Инициализация свойств столбцов значениями по умолчанию m_columns[column_index].m_x =0; m_columns[column_index].m_x2 =0; m_columns[column_index].m_width =100; m_columns[column_index].m_type =TYPE_STRING; m_columns[column_index].m_text_align =ALIGN_CENTER; m_columns[column_index].m_text_x_offset =m_text_x_offset; m_columns[column_index].m_image_x_offset =m_image_x_offset; m_columns[column_index].m_image_y_offset =m_image_y_offset; m_columns[column_index].m_header_text =""; } //+------------------------------------------------------------------+ //| Инициализация указанной ячейки значениями по умолчанию | //+------------------------------------------------------------------+ void CCanvasTable::CellInitialize(const uint column_index,const uint row_index) { m_columns[column_index].m_rows[row_index].m_full_text =""; m_columns[column_index].m_rows[row_index].m_short_text =""; m_columns[column_index].m_rows[row_index].m_selected_image =0; m_columns[column_index].m_rows[row_index].m_text_color =m_cell_text_color; m_columns[column_index].m_rows[row_index].m_digits =0; m_columns[column_index].m_rows[row_index].m_type =CELL_SIMPLE; //--- По умолчанию у ячейки нет картинок ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images); }
Для того, чтобы скопировать свойства одного столбца в другой столбец нужно использовать метод CCanvasTable::ColumnCopy(), код которого показан в листинге ниже:
class CCanvasTable : public CElement { private: //--- Делает копию указанного столбца (source) в новое место (dest.) void ColumnCopy(const uint destination,const uint source); }; //+------------------------------------------------------------------+ //| Делает копию указанного столбца (source) в новое место (dest.) | //+------------------------------------------------------------------+ void CCanvasTable::ColumnCopy(const uint destination,const uint source) { m_columns[destination].m_header_text =m_columns[source].m_header_text; m_columns[destination].m_width =m_columns[source].m_width; m_columns[destination].m_type =m_columns[source].m_type; m_columns[destination].m_text_align =m_columns[source].m_text_align; m_columns[destination].m_text_x_offset =m_columns[source].m_text_x_offset; m_columns[destination].m_image_x_offset =m_columns[source].m_image_x_offset; m_columns[destination].m_image_y_offset =m_columns[source].m_image_y_offset; }
Метод CCanvasTable::CellCopy() копирует одну указанную ячейку в другую. Для этого нужно передать индексы столбца и строки ячейки-приёмника и индексы столбца и строки ячейки-источника.
class CCanvasTable : public CElement { private: //--- Делает копию указанной ячейки (source) в новое место (dest.) void CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source); }; //+------------------------------------------------------------------+ //| Делает копию указанной ячейки (source) в новое место (dest.) | //+------------------------------------------------------------------+ void CCanvasTable::CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source) { m_columns[column_dest].m_rows[row_dest].m_type =m_columns[column_source].m_rows[row_source].m_type; m_columns[column_dest].m_rows[row_dest].m_digits =m_columns[column_source].m_rows[row_source].m_digits; m_columns[column_dest].m_rows[row_dest].m_full_text =m_columns[column_source].m_rows[row_source].m_full_text; m_columns[column_dest].m_rows[row_dest].m_short_text =m_columns[column_source].m_rows[row_source].m_short_text; m_columns[column_dest].m_rows[row_dest].m_text_color =m_columns[column_source].m_rows[row_source].m_text_color; m_columns[column_dest].m_rows[row_dest].m_selected_image =m_columns[column_source].m_rows[row_source].m_selected_image; //--- Копируем размер массива из источника в приёмник int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images); ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total); //--- for(int i=0; i<images_total; i++) { //--- Копировать, если есть картинки if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1) continue; //--- Делаем копию картинки ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i); } }
В рамках исключения повторяемого из метода в метод кода реализован ещё один простой метод, который будет вызываться после внесённых в реконструкции таблицы изменений. Каждый раз после добавления и удаления строк и столбцов нужно пересчитать и затем установить новые размеры таблицы, а после этого перерисовать таблицу для отображения этих изменений. Для этого будет использоваться метод CCanvasTable::RecalculateAndResizeTable(). Здесь в качестве единственного параметра указывается, полностью ли нужно перерисовать таблицу (true) или просто обновить её (false).
class CCanvasTable : public CElement { private: //--- Расчёт с учётом последних изменений и изменение размеров таблицы void RecalculateAndResizeTable(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Расчёт с учётом последних изменений и изменение размеров таблицы | //+------------------------------------------------------------------+ void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false) { //--- Рассчитать размеры таблицы CalculateTableSize(); //--- Установить новый размер таблице ChangeTableSize(); //--- Обновить таблицу UpdateTable(redraw); }
Методы для добавления и удаления столбцов и строк намного проще и понятнее, чем версии этих методов в таблице типа CTable. Сравнить код этих методов вы можете самостоятельно, а здесь приведём для примера лишь код методов для добавления и удаления столбцов. Код для работы со строками имеет такой же порядок действий, который был описан в начале этого раздела. Уточню только, что признак отсортированной таблицы будет смещаться вместе с отсортированным столбцом и при добавлении, и при удалении. В случае же, когда удаляется отсортированный столбец, поле, где хранится индекс отсортированного столбца, обнуляется.
class CCanvasTable : public CElement { public: //--- Добавляет столбец в таблицу по указанному индексу void AddColumn(const int column_index,const bool redraw=false); //--- Удаляет столбец в таблице по указанному индексу void DeleteColumn(const int column_index,const bool redraw=false); }; //+------------------------------------------------------------------+ //| Добавляет столбец в таблицу по указанному индексу | //+------------------------------------------------------------------+ void CCanvasTable::AddColumn(const int column_index,const bool redraw=false) { //--- Увеличим размер массива на один элемент int array_size=(int)ColumnsTotal(); m_columns_total=array_size+1; ::ArrayResize(m_columns,m_columns_total); //--- Установить размер массивам рядов ::ArrayResize(m_columns[array_size].m_rows,m_rows_total); //--- Корректировка индекса в случае выхода из диапазона int checked_column_index=(column_index>=(int)m_columns_total)? (int)m_columns_total-1 : column_index; //--- Сместить другие столбцы (двигаемся от конца массива к индексу добавляемого столбца) for(int c=array_size; c>=checked_column_index; c--) { //--- Сместить признак отсортированного массива if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index++; //--- Индекс предыдущего столбца int prev_c=c-1; //--- В новом столбце инициализация значениями по умолчанию if(c==checked_column_index) ColumnInitialize(c); //--- Перемещаем данные из предыдущего столбца в текущий else ColumnCopy(c,prev_c); //--- for(uint r=0; r<m_rows_total; r++) { //--- Инициализация ячеек нового столбца значениями по умолчанию if(c==checked_column_index) { CellInitialize(c,r); continue; } //--- Перемещаем данные из ячейки предыдущего столбца в ячейку текущего CellCopy(c,r,prev_c,r); } } //--- Рассчитать и установить новые размеры таблицы RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Удаляет столбец в таблице по указанному индексу | //+------------------------------------------------------------------+ void CCanvasTable::DeleteColumn(const int column_index,const bool redraw=false) { //--- Получим размер массива столбцов int array_size=(int)ColumnsTotal(); //--- Корректировка индекса в случае выхода из диапазона int checked_column_index=(column_index>=array_size)? array_size-1 : column_index; //--- Сместить другие столбцы (двигаемся от указанного индекса до последнего столбца) for(int c=checked_column_index; c<array_size-1; c++) { //--- Сместить признак отсортированного массива if(c!=checked_column_index) { if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index--; } //--- Обнулить, если удалён отсортированный столбец else m_is_sorted_column_index=WRONG_VALUE; //--- Индекс следующего столбца int next_c=c+1; //--- Перемещаем данные из следующего столбца в текущий ColumnCopy(c,next_c); //--- Перемещаем данные из ячеек следующего столбца в ячейки текущего for(uint r=0; r<m_rows_total; r++) CellCopy(c,r,next_c,r); } //--- Уменьшим массив столбцов на один элемент m_columns_total=array_size-1; ::ArrayResize(m_columns,m_columns_total); //--- Рассчитать и установить новые размеры таблицы RecalculateAndResizeTable(redraw); }
Методы для полной реконструкции и очистки таблицы тоже очень просты, в отличие от аналогичных методов в таблице типа CTable. Здесь достаточно установить новый размер вызовом метода CCanvasTable::TableSize(). При очистке таблицы устанавливается минимальный размер, когда остаётся только один столбец и одна строка. При очистке обнуляются и поля, в которых хранятся значения выделенной строки, направление сортировки и индекс отсортированного столбца. В конце обоих методов рассчитываются и устанавливаются новые размеры таблицы.
class CCanvasTable : public CElement { public: //--- Реконструкция таблицы void Rebuilding(const int columns_total,const int rows_total,const bool redraw=false); //--- Очищает таблицу. Остаётся только один столбец и одна строка. void Clear(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Реконструкция таблицы | //+------------------------------------------------------------------+ void CCanvasTable::Rebuilding(const int columns_total,const int rows_total,const bool redraw=false) { //--- Установить новый размер TableSize(columns_total,rows_total); //--- Рассчитать и установить новые размеры таблицы RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Очищает таблицу. Остаётся только один столбец и одна строка. | //+------------------------------------------------------------------+ void CCanvasTable::Clear(const bool redraw=false) { //--- Установить минимальный размер 1x1 TableSize(1,1); //--- Установить значения по умолчанию m_selected_item_text =""; m_selected_item =WRONG_VALUE; m_last_sort_direction =SORT_ASCEND; m_is_sorted_column_index =WRONG_VALUE; //--- Рассчитать и установить новые размеры таблицы RecalculateAndResizeTable(redraw); }
Это работает так:
Рис. 2. Демонстрация управления размерностью таблицы.
В конце статьи можно загрузить к себе на компьютер тестовое приложение, изображённое на анимации выше.
Событие двойного нажатия левой кнопкой мыши
Иногда может понадобиться вызвать какое-либо действие через двойной клик мыши. Реализуем в нашей библиотеке генерацию такого события. Для этого в файле Defines.mqh добавим идентификатор ON_DOUBLE_CLICK для события двойного клика левой кнопкой мыши:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ ... #define ON_DOUBLE_CLICK (34) // Двойной клик левой кнопки мыши ...
Событие с идентификатором ON_DOUBLE_CLICK будет генерироваться в классе CMouse. Обычно в ОС такое событие генерируется при действиях “нажатие-отжатие-нажатие” левой кнопкой мыши. Но тестирование в среде терминала показало, что не всегда получается одномоментно отследить событие нажатия левой кнопки мыши по приходу события CHARTEVENT_MOUSE_MOVE (параметр sparam). Поэтому решено реализовать генерацию события через действия “нажатие-отжатие-нажатие-отжатие”. Точно отследить эти действия можно по приходу события CHARTEVENT_CLICK.
По умолчанию между двумя нажатиями должно пройти не менее 300 миллисекунд. Для отслеживания события CHARTEVENT_CLICK в обработчике событий мыши будет вызываться метод CMouse::CheckDoubleClick(). В начале этого метода объявлены две статические переменные, в которых каждый раз при вызове метода будут обновляться значения предыдущего и текущего тика (количество миллисекунд, прошедших с момента старта системы). Если между этими значениями окажется меньшее количество миллисекунд, чем указано в поле m_pause_between_clicks, то будет сгенерировано событие двойного нажатия левой кнопкой мыши.
//+------------------------------------------------------------------+ //| Класс для получения параметров мыши | //+------------------------------------------------------------------+ class CMouse { private: //--- Пауза между кликами левой кнопкой мыши (для определения двойного нажатия) uint m_pause_between_clicks; //--- private: //--- Проверка двойного нажатия левой кнопки мыши bool CheckDoubleClick(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CMouse::CMouse(void) : m_pause_between_clicks(300) { ... } //+------------------------------------------------------------------+ //| Обработка событий мыши | //+------------------------------------------------------------------+ void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Обработка события нажатия на графике if(id==CHARTEVENT_CLICK) { //--- Проверим двойное нажатие левой кнопкой мыши CheckDoubleClick(); return; } } //+------------------------------------------------------------------+ //| Проверка двойного нажатия левой кнопки мыши | //+------------------------------------------------------------------+ void CMouse::CheckDoubleClick(void) { static uint prev_depressed =0; static uint curr_depressed =::GetTickCount(); //--- Обновим значения prev_depressed =curr_depressed; curr_depressed =::GetTickCount(); //--- Определим время между нажатиями uint counter=curr_depressed-prev_depressed; //--- Если между кликами прошло меньше времени, чем указано, отправим сообщение о двойном нажатии if(counter<m_pause_between_clicks) ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,0.0,""); }
Таким образом, в обработчиках событий всех классов библиотеки можно отслеживать двойной клик левой кнопкой мыши в любом месте области графика, независимо от того, есть ли под курсором графический объект.
Элементы управления в ячейках таблицы
В этой статье мы откроем тему элементов управления в ячейках таблицы. Например, такая возможность может понадобиться, когда нужно создать многопараметрическую экспертную систему. Удобно реализовать графический интерфейс этой системы в виде таблицы. Начнем добавлять такие элементы в ячейки таблицы с чекбоксов и кнопок.
В первую очередь, понадобится новое перечисление ENUM_TYPE_CELL, в котором будут определены типы ячеек:
//+------------------------------------------------------------------+ //| Перечисление типов ячейки таблицы | //+------------------------------------------------------------------+ enum ENUM_TYPE_CELL { CELL_SIMPLE =0, CELL_BUTTON =1, CELL_CHECKBOX =2 };
По умолчанию при инициализации ячейкам присваивается простой тип – CELL_SIMPLE, что означает “ячейка без элемента управления”. Чтобы установить или получить тип ячейки, используйте методы CCanvasTable::CellType(), код которых показан в листинге ниже. Установив для ячейки тип CELL_CHECKBOX (чекбокс), нужно также установить и картинки для состояний чекбокса. Минимальное количество картинок для этого типа ячейки – две. Можно установить более двух картинок, тогда будет возможность работать с многопараметрическими чекбоксами.
class CCanvasTable : public CElement { //--- Установка/получение типа ячейки void CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type); ENUM_TYPE_CELL CellType(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Устанавливает тип ячейки | //+------------------------------------------------------------------+ void CCanvasTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type) { //--- Проверка на выход из диапазона if(!CheckOutOfRange(column_index,row_index)) return; //--- Установить тип ячейки m_columns[column_index].m_rows[row_index].m_type=type; } //+------------------------------------------------------------------+ //| Получение типа ячейки | //+------------------------------------------------------------------+ ENUM_TYPE_CELL CCanvasTable::CellType(const uint column_index,const uint row_index) { //--- Проверка на выход из диапазона if(!CheckOutOfRange(column_index,row_index)) return(WRONG_VALUE); //--- Вернуть тип данных для указанного столбца return(m_columns[column_index].m_rows[row_index].m_type); }
Серии картинок для чекбоксов и кнопок в каждом столбце могут быть разных размеров, поэтому нужна возможность установить X- и Y-отступы картинок для каждого столбца отдельно. Это можно сделать с помощью методов CCanvasTable::ImageXOffset() и CCanvasTable::ImageYOffset():
class CCanvasTable : public CElement { public: //--- Смещение картинок по X- и Y-осям void ImageXOffset(const int &array[]); void ImageYOffset(const int &array[]); };
Ещё одним дополнением будет режим, при включении которого можно исключить отмену выделения строки при повторном нажатии. Этот режим работает, только если включен режим выделения строки.
class CCanvasTable : public CElement { private: //--- Без отмены выделения строки при повторном нажатии bool m_is_without_deselect; //--- public: //--- Режим «Без отмены выделения строки при повторном нажатии» void IsWithoutDeselect(const bool flag) { m_is_without_deselect=flag; } };
Для определения индекса строки и столбца, на котором был клик, теперь используются отдельные методы CCanvasTable::PressedRowIndex() и CCanvasTable::PressedCellColumnIndex(). Они уже рассматривались ранее как блоки метода CCanvasTable::OnClickTable(). Полный их код с новыми дополнениями вы можете изучить самостоятельно в приложенных к статье файлах. Отмечу отдельно только, что в совокупности с помощью этих методов можно определить ячейку, на которой было нажатие левой кнопкой мыши. Далее рассмотрим, куда будут передаваться полученные индексы столбца и строки.
class CCanvasTable : public CElement { private: //--- Возвращает индекс нажатой строки int PressedRowIndex(void); //--- Возвращает индекс столбца нажатой ячейки int PressedCellColumnIndex(void); };
При нажатии на ячейку таблицы нужно получить её тип, и если она содержит элемент управления, то нужно также определить, был ли он задействован. Для этого реализован ряд приватных методов, основной из которых — CCanvasTable::CheckCellElement(). Именно в него передаются полученные с помощью методов CCanvasTable::PressedRowIndex() и CCanvasTable::PressedCellColumnIndex() индексы столбца и строки. Метод двухрежимный. В зависимости от того, при обработке какого события он вызывается, используется третий параметр, которым можно указать, было ли это событие двойным кликом мыши.
Далее проверяется тип ячейки, и в зависимости от её типа вызывается соответствующий метод. Если в ячейке элемент управления “Кнопка”, то будет вызван метод CCanvasTable::CheckPressedButton(). Для ячеек-чекбоксов предназначен метод CCanvasTable::CheckPressedCheckBox().
class CCanvasTable : public CElement { private: //--- Проверяет, был ли задействован элемент в ячейке при нажатии bool CheckCellElement(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Проверяет, был ли при нажатии задействован элемент в ячейке | //+------------------------------------------------------------------+ bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false) { //--- Выйти, если в ячейке нет элемента управления if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE) return(false); //--- switch(m_columns[column_index].m_rows[row_index].m_type) { //--- Если это ячейка-кнопка case CELL_BUTTON : { if(!CheckPressedButton(column_index,row_index,double_click)) return(false); //--- break; } //--- Если это ячейка-чекбокс case CELL_CHECKBOX : { if(!CheckPressedCheckBox(column_index,row_index,double_click)) return(false); //--- break; } } //--- return(true); }
Рассмотрим, как устроены методы CCanvasTable::CheckPressedButton() и CCanvasTable::CheckPressedCheckBox(). В самом начале обоих методов стоит проверка на количество картинок в ячейке. В ячейках-кнопках должна быть минимум одна картинка, а в ячейках-чекбоксах – минимум две. Затем нужно определить, было ли нажатие именно на картинке. В случае с чекбоксом реализуем два способа его переключения. Одиночное нажатие будет работать, только если нажать на картинку с изображением чекбокса. Двойной клик переключает чекбокс в любом месте ячейки. В обоих методах в случае исполнения всех условий генерируется событие с соответствующим элементу идентификатором. В качестве double-параметра отправляется индекс картинки, а в string-параметре формируется строка с индексами столбца и строки.
class CCanvasTable : public CElement { private: //--- Проверяет, было ли нажатие на кнопке в ячейке bool CheckPressedButton(const int column_index,const int row_index,const bool double_click=false); //--- Проверяет, было ли нажатие на чекбоксе в ячейке bool CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Проверяет, было ли нажатие на кнопке в ячейке | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedButton(const int column_index,const int row_index,const bool double_click=false) { //--- Выйти, если нет картинок в ячейке if(ImagesTotal(column_index,row_index)<1) { ::Print(__FUNCTION__," > Установите минимум одну картинку для ячейки-кнопки!"); return(false); } //--- Получим относительные координаты под курсором мыши int x=m_mouse.RelativeX(m_table); //--- Получим правую границу картинки int image_x =int(m_columns[column_index].m_x+m_columns[column_index].m_image_x_offset); int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width); //--- Выйти, если нажали не на картинке if(x>image_x2) return(false); else { //--- Если это не двойной клик, отправим сообщение if(!double_click) { int image_index=m_columns[column_index].m_rows[row_index].m_selected_image; ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElementBase::Id(),image_index,string(column_index)+"_"+string(row_index)); } } //--- return(true); } //+------------------------------------------------------------------+ //| Проверяет, было ли нажатие на чекбоксе в ячейке | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false) { //--- Выйти, если нет картинок в ячейке if(ImagesTotal(column_index,row_index)<2) { ::Print(__FUNCTION__," > Установите минимум две картинки для ячейки-чекбокса!"); return(false); } //--- Получим относительные координаты под курсором мыши int x=m_mouse.RelativeX(m_table); //--- Получим правую границу картинки int image_x =int(m_columns[column_index].m_x+m_image_x_offset); int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width); //--- Выйти, если (1) нажали не на картинке и (2) это не двойное клик if(x>image_x2 && !double_click) return(false); else { //--- Текущий индекс выбранной картинки int image_i=m_columns[column_index].m_rows[row_index].m_selected_image; //--- Определим следующий индекс для картинки int next_i=(image_i<ImagesTotal(column_index,row_index)-1)? ++image_i : 0; //--- Выбрать следующую картинку и обновить таблицу ChangeImage(column_index,row_index,next_i,true); m_table.Update(false); //--- Отправим сообщение об этом ::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,string(column_index)+"_"+string(row_index)); } //--- return(true); }
В итоге код методов CCanvasTable::OnClickTable() и CCanvasTable::OnDoubleClickTable(), обрабатывающих нажатия на таблице, стал намного более понятным и читаемым (см. листинг ниже). В зависимости от того, какие режимы включены и на ячейке какого типа было нажатие, генерируется соответствующее событие.
class CCanvasTable : public CElement { private: //--- Обработка нажатия на таблице bool OnClickTable(const string clicked_object); //--- Обработка двойного нажатия на таблице bool OnDoubleClickTable(const string clicked_object); }; //+------------------------------------------------------------------+ //| Обработка нажатия на таблице | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickTable(const string clicked_object) { //--- Выйти, если (1) отключен режим выделения ряда или (2) в процессе изменения ширины столбца if(m_column_resize_control!=WRONG_VALUE) return(false); //--- Выйдем, если полоса прокрутки в активном режиме if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false); //--- Выйдем, если чужое имя объекта if(m_table.Name()!=clicked_object) return(false); //--- Определим ряд, на котором нажали int r=PressedRowIndex(); //--- Определим ячейку, на которой нажали int c=PressedCellColumnIndex(); //--- Проверим был ли задействован элемент в ячейке bool is_cell_element=CheckCellElement(c,r); //--- Если (1) включен режим выделения строки и (2) не задействован элемент в ячейке if(m_selectable_row && !is_cell_element) { //--- Изменить цвет RedrawRow(true); m_table.Update(); //--- Отправим сообщение об этом ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,string(c)+"_"+string(r)); } //--- return(true); } //+------------------------------------------------------------------+ //| Обработка двойного нажатия на таблице | //+------------------------------------------------------------------+ bool CCanvasTable::OnDoubleClickTable(const string clicked_object) { if(!m_table.MouseFocus()) return(false); //--- Определим ряд, на котором нажали int r=PressedRowIndex(); //--- Определим ячейку, на которой нажали int c=PressedCellColumnIndex(); //--- Проверим был ли задействован элемент в ячейке и вернём результат return(CheckCellElement(c,r,true)); }
Обработка двойного нажатия левой кнопкой мыши на ячейке осуществляется в обработчике элемента по событию ON_DOUBLE_CLICK:
//+------------------------------------------------------------------+ //| Обработчик событий | //+------------------------------------------------------------------+ void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Обработка двойного нажатия левой кнопки мыши if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK) { //--- Нажатие на таблице if(OnDoubleClickTable(sparam)) return; //--- return; } }
Работает всё в конечном итоге так:
Рис. 3. Демонстрация взаимодействия с элементами управления в ячейках таблицы.
В конце статьи мы можете загрузить на свой компьютер приложения, продемонстрированные в этой статье.
Заключение
На текущем этапе разработки библиотеки для создания графических интерфейсов её общая схема выглядит так:
Рис. 4. Структура библиотеки на текущей стадии разработки.
Ниже вы можете загрузить к себе на компьютер последнюю версию библиотеки и файлы для тестов.
При возникновении вопросов по использованию материала, предоставленного в этих файлах, вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей этой серии или задать вопрос в комментариях к статье.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
вопрос снят, сам ступил в индексации:
Толь, заметил странности при изменении размеров колонок - неверно обрезается текст. И ещё случайно обнаружил постоянное изменение размера колонки при удержании клавиши Ctrl.
1. Толь, заметил странности при изменении размеров колонок - неверно обрезается текст.
2. И ещё случайно обнаружил постоянное изменение размера колонки при удержании клавиши Ctrl.
1. Попробовал потестировать с различными режимами выравнивания текста в столбцах, но не удалось воспроизвести.
2. С клавишей Ctrl воспроизводится, но непонятно, почему такое поведение. Эта клавиша не прописана в коде таблицы.
Здравствуйте, подскажите, как обновлять таблицу, если в ней динамические данные https://www.mql5.com/ru/forum/165152/page10#comment_7488387
Посмотрите здесь: Графические интерфейсы X: Обновления для нарисованной таблицы и оптимизация кода (build 10)