Вспомогательный класс
CEnterEditDialog предназначен для прямого редактирования текста ячеек. Он является классом, производным от класса
CDialog. Для него нужен диалог-ресурс, который сделать очень просто при помощи редактора ресурсов:
1) вставляем в ресурсы новый диалог,
ID == IDD_ENTER_EDIT_DIALOG.
2) удаляем всё с поверхности диалога (хотя, можно и не удалять, само всё удалится в классе).
3) у диалога в свойствах -> окно
Border -> устанавливаем
None.
Тестовый проект для этой статьи его можно найти по ссылке в конце статьи, но здесь будет подробно описано, как производится вставка компонента и работа с ним.
Итак, создаём тестовый проект GridTest (MFC , Dialog-based). Копируем в папку с проектом 4 файла, описанных выше. Добавляем файлы также в дерево файлов проекта. Добавляем ресурс для диалога, как это описано выше. После этого рекомендуется удалить из папки проекта файл "
имя_проекта.clw" (можно даже не закрывая студию). Затем в студии нажать Ctrl+W для того, чтобы обновилось дерево классов.
Описываемый класс является производным от MFC-класса
CStatic. Поэтому, чтобы поместить компонент на диалоговое окно в режиме редактирования ресурсов, кладём сначала на диалог
CStatic из стандартной палитры элементов управления. Зададим идентификатор
IDC_GRID. Устанавливаем размер и положение будущего грида. (Потом, конечно, можно будет поменять программно при помощи
CWnd::MoveWindow() ) Можно также по желанию поставить в свойствах статика галочку
Sunken - будет модная каёмочка вокруг грида ).
Добавляем для статика связанный с ним член-переменную: удерживая
Ctrl дважды щелкаем по статику, пишем имя переменной
m_IDC_GRID , в окне
Category -> выбираем
Control, в окне
Variable Type -> выбираем наш класс
CGridEdit1153. Также не забываем добавить в заголовочный файл диалога перед описание класса диалога строчку:
#include "GridEdit1153.h"
Компилируем проект (нет ли ошибок ?) и сохраняем.
Перед началом работы с компонентом, необходимо вызвать метод
Create(). Если этого не сделать, компонент отобразит себя в виде белого прямоугольника с красным квадратиком в левом верхнем углу.
Теперь надо создать (инициализировать то есть) грид. Если бы главное окно было потомком от
CView , то создание компонента нужно было делать в методе
OnInitialUpdate(). Ну а так как у нас диалог, то делаем создание в методе диалога
OnInitDialog() (обработчик сообщения
WM_INITDIALOG). В примере представлено заполнение большого количества настроек сразу (на то и пример), но всё это можно делать в любой последующий момент времени, а не только в этом обработчике. Самое главное - тут должна быть вызвана
Create().
BOOL CGridTestDlg::OnInitDialog()
{
CDialog::OnInitDialog();
//инициализация грида/////////////
CGridEdit1153* pGR=&m_IDC_GRID;
//создаём грид1153
if(pGR->Create(this))
{
//задание настроек и параметров
//создаём грид1153
if(pGR->Create(this))
{
//задание шрифта 12 , Times New Roman
LOGFONT lf={
-(12*LOGPIXELSY)/72,
0,0,0,FW_NORMAL,0,0,0,RUSSIAN_CHARSET,
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,
DRAFT_QUALITY,FIXED_PITCH,
"Times New Roman"
};
//задаём шрифт таблицы
pGR->SetTableLogFont(&lf);
//задаём шрифт заголовков
pGR->SetHeaderLogFont(&lf);
//высота ячеек
pGR->SetRowHeight(20);
//сдвиг при вращении колеса мыши - 3 строки
pGR->SetWheelMoveDistance(3);
//настройка обработчиков
pGR->SetMessageCallBack(CGridEdit1153::mes_OnLButtonDblClk, grid_mes_OnLButtonDblClk);
pGR->SetMessageCallBack(CGridEdit1153::mes_OnMouseWheel, grid_mes_OnMouseWheel);
//включить обработку сообщений
pGR->EnableNotifyParent(true);
//установка количества строк и столбцов
if(!pGR->SetRowsCols(1000,10,true,true))
{
return 0;
}
//тексты заголовков
CString txt;
for(DWORD i=0;i<pGR->GetCols();i++)
{
txt.Format("%d",i);
pGR->SetColText(i,txt);
}
//все колонки делаем шириной 30
pGR->SetColWidth_ToAll(30);
}
//....
//....
Обработка сообщения от грида реализована через функции обратного вызова (
callback). Имеется следующий набор сообщений (его вы , конечно же, можете расширить):
m_pVBI указывает на структуру
CVBarInfo, содержащую:
//границы для вертикальной полосы прокрутки
//(тип переменных - const int)
minpos //минимальная позиция ползунка
maxpos //максимальная позиция ползунка
//при перетаскивании ползунка вертикальной полосы прокрутки
//(тип переменных - const int)
posfrom //позиция, откуда начато перемещение
poswhere //позиция, куда ползунок переместили
//для нажатия стрелок и пространства снизу или сверху от ползунка
//вертикальной полосы прокрутки
//(тип переменных - const int)
tomovevalue //на сколько требуется подвинуть ползунок (со знаком)
Что значит "мгновенное логическое количество строк в таблице" ? К примеру, создана таблица на 100 строк. Но заполнены ещё только первые 60. В данном случае , 60 - это и есть логическое количество строк, только они будут участвовать в прокрутке. Остальные 40 строк в конце - они не будут видны на экране. Значение 0 в
dwdLogicNumberOfRows равносильно указанию максимального количества строк (100).
Если обработчик возвращает 0, то встроенный обработчик не выполняется. Если же вернёт 1, то после этого выполниться ещё и встроенный обработчик.
В обработчике сообщения
mes_OnLButtonDblClk у нас сделано непосредственное редактирование содержимого ячейки.
//обработчик сообщения CGridEdit1153::mes_OnLButtonDblClk
BOOL CGridTestDlg::grid_mes_OnLButtonDblClk(const CGridEdit1153::sCBinfo* pInfo)
{
pInfo->m_pGrid->EditCell(
pInfo->m_pGrid->GetCursorRow_zb(),
pInfo->m_pGrid->GetCursorCol_zb()
);
//обработка по умолчанию не требуется
return 0;
}
"Символ для сообщений от клавиатуры" имеет тип
const UINT , это всего лишь значение, полученное в гриде
CGridEdit1153::OnKeyUp(UINT nChar, ...))
При прокрутке поворотом колеса стоит обратить внимание на следующий момент: сообщения WM_MOUSEWHEEL посылаются гриду только тогда, когда фокус находится на гриде (например, до этого щёлкнули по нему мышью). Если требуется делать прокрутку независимо от фокуса, но, например, тогда, когда курсор мыши находится над окном таблицы, то в родительском окне в обработчике сообщения
WM_MOUSEWHEEL нужно применить метод
CGridEdit1153::InParent_OnMouseWheel_hook :
BOOL CGridTestDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
//для более приятного поведения колёсика мыши
//(для случая, когда грид не в фокусе, а курсор мыши находится над гридом
//этот крюк также произведёт прокрутку)
if(m_IDC_GRID.InParent_OnMouseWheel_hook(nFlags,zDelta,pt))return 1;
return CDialog::OnMouseWheel(nFlags, zDelta, pt);
}
Палитра цветов содержит набор цветов, используемых для отображения текста и фона ячеек.
Цель применения палитры - экономия ОЗУ. Таким образом, цвет ячейки задан всего одним байтом (или двумя в случае 65535 цветов) а не 4 байтами (как при использовании
COLORREF вместо индекса). Когда, к примеру, в таблице миллион строк и 20 колонок, то экономия ОЗУ весьма ощутима (на один только цвет - 40 мегабайт или 160, а ? ).
Поговорим о том, как выбрать размер палитры цветов.
В файле "
GridEdit1153.h" имеются строки:
//выбор размера палитры
typedef BYTE CColorIndex;enum {e_CColorIndex_colorsnum=255}; //2...(0xff)
//typedef WORD CColorIndex;enum {e_CColorIndex_colorsnum=65535}; //2...(0xffff)
//typedef DWORD CColorIndex;enum {e_CColorIndex_colorsnum=16777216}; //2...(0x01000000)
...
enum
{
e_tx_default_0=0,//индекс цвета по умолчанию для текста ячейки
e_bk_default_1=1,//индекс цвета по умолчанию для фона ячейки
...
e_tx_default_color = RGB(0,0,0),
e_bk_default_color = RGB(254,255,254),
};
В оригинальном варианте открыта строка, где индекс цвета (а каждая ячейка имеет два таких индекса - для фона и для текста)
CColorIndex - размером 1 байт. Такой индекс позволяет использовать для цвета текста и фона ячеек максимум 255 цветов одновременно. Для исключительного числа задач этого количества цветов достаточно. Если закрыть первую строчку и открыть вторую (где
typedef WORD..), то размер палитры увеличится до 65535 (но и ОЗУ цвет займёт в 2 раза больше, чем при
typedef BYTE). Ну, и вряд ли когда то понадобится третья строка , с
DWORD, где доступны все 16777216 RGB-цветов. Но вообще говоря, с
DWORD тут экономия пропадает, и рациональнее переписать класс
CGridElemColor (класс цвета) , а от типа
CColorIndex отказаться совсем.
Палитра реализована в виде класса
CElemzPalette. В конструкторе класса сразу создаются 2 цвета - которые используются по умолчанию для цвета фона и цвета текста ячеек.
CElemzPalette()
{
m_dwdPaletteLen=0;
::memset(m_a_palette,0,sizeof(m_a_palette));
//заполняем два цвета (которые по умолчанию)
m_a_palette[e_tx_default_0]=e_tx_default_color;
m_dwdPaletteLen++;
m_a_palette[e_bk_default_1]=e_bk_default_color;
m_dwdPaletteLen++;
}
Константы размеров и цветов
def_RowsMax=10000000, //максимальное количество строк //1...2147483647
def_ColsMax=2000, //максимальное количество колонок //1...2147483647
def_MinColWidth=12, //минимальная ширина колонки
init_ColWidth=20, //ширина колонки по умолчанию
def_MinRowHeight=2, //минимальная высота строки
def_MaxRowHeight=100, //максимальная высота строки
init_RowHeight=22, //высота строки по умолчанию
def_nHeaderHeightMin=0, //минимальная высота заголовка
def_nHeaderHeightMax=30,//максимальная высота заголовка
def_nHBarHeightMin=0, //минимальная высота горизонтальной полосы прокрутки
def_nHBarHeightMax=30, //максимальная высота горизонтальной полосы прокрутки
def_nVBarWidthMin=0, //минимальная ширина вертикальной полосы прокрутки
def_nVBarWidthMax=30, //максимальная ширина вертикальной полосы прокрутки
def_TextEscapeFromLeft=2,//отступ текста от левого края клетки (пикселы)
def_TextEscapeFromRight=2,//отступ текста от правого края клетки (пикселы)
def_TrackLineWid=1, //толщина трек-курсора (если виден) (пикселы)
init_TrackColor=RGB(0,200,0),//цвет трек-курсора по умолчанию
def_CursorLineWid=2, //толщина курсора-рамки (пикселы)
init_CursorColor=RGB(50,50,255),//цвет курсора-рамки по умолчанию
init_LeftInfoSpaceDef=20, //ширина информационной области слева от первой колонки
init_LeftInfoSpaceMax=100, //ширина информационной области слева от первой колонки
init_LeftInfoSpaceColor=RGB(220,255,220),////цвет информационной области по умолчанию
init_BkColor=RGB(253,255,253), //фон клеток по умолчанию
init_TxColor=RGB(0,0,0), //цвет текста клеток по умолчанию
init_LineColor=RGB(170,190,170),//цвет линий сетки по умолчанию
def_distWhenWheel_min=1, //величина сдвига таблицы при повороте колеса (строки)
def_distWhenWheel_max=100, //... при повороте колеса - максимум
def_distWhenH_Arr=2, //... при нажатии на стрелки гориз. полосы (пикселы)
def_distWhenH_Space=10, //... при нажатии сбоку от ползунка гориз. полосы (пикселы)
def_distWhenV_Arr=1, //... при нажатии на стрелки верт. полосы (строки)
def_distWhenV_Space=5, //... при нажатии сверху/снизу от ползунка гориз. полосы (строки)
Ширина информационной области слева от первой колонки хранится в переменной
m_LeftInfoSpace, начальное значение которой равно
init_LeftInfoSpaceDef. Ширину можно задавать в пределах 0...
init_LeftInfoSpaceMax методом
SetLeftInfoWidth().
Вся графика класса разделена на 3 области:
Элементы, рисуемые с двойным буфером (без мерцания при частой перерисовке). Область отрисовывается в методе
PaintGrid().
1)фон ячейки по умолчанию заливает всё пространство, ограниченное заголовком, полосами прокрутки и левым краем окна таблицы. Позже частично перекрывается следующими далее элементами:
2)информационная область слева
3)неиспользуемая область справа от ячеек таблицы
4)неиспользуемая область снизу от ячеек таблицы
Дочерние окна
1)заголовок
2)вертикальная полоса прокрутки
3)горизонтальная полоса прокрутки
Элементы, рисуемые без двойного буфера
1)"квадратик" с треугольником, используемый для прокрутки на начало таблицы (расположен над вертикальной полосой прокрутки)
2)правый нижний "квадратик" (под вертикальной полосой прокрутки)
3)левый верхний "квадратик" (слева от заголовка)
Теперь пройдёмся немного по коду файлов компонента.
Файл
EnterEditDialog.cpp, файл реализации вспомогательного диалога для прямого редактирования ячеек. Всё необходимое диалогу передаётся в конструктор. Диалог открывается методом
DoModal() , и пока не будет закрыт клавишей
Enter или
Esc , таблица недоступна для пользователя. Впрочем , как и главное окно проекта :) .
Файл
GridEdit1153.h , заголовочный файл таблицы. Содержим все константы компонента, содержит описание вспомогательных структур. В принципе, тут тоже всё подробно закомментировано.
Файл
GridEdit1153.cpp, файл реализации таблицы. Опишу моменты, на которые стоит обратить особое внимание.
В конструкторе , помимо инициализации переменных, задаётся будущий шрифт по умолчанию -
Arial, 15.
bool SetRowsCols(
DWORD rows,
DWORD cols,
bool bRedimIfAlreadyCreated=true,
bool bSaveContentIfRedim=true
);
Здесь создаётся динамический массив с ячейками. Если указать
bRedimIfAlreadyCreated==true, то функция в любом случае выполнит пересоздание массива с новыми параметрами. Если указать
false, то функция завершится, ничего не создавая нового и вернёт false. Флаг
bSaveContentIfRedim позволяет указать, нужно ли сохранить содержимое таблицы (тексты, цвета, ширина колонок) , естественно, которое останется после изменения размера.
void ClearTable(
DWORD begrow_zb=0,
DWORD endrow_zb=0xffffffff
);
Функция просто очищает ячейки от текста, не удаляя массив ячеек.
BOOL Create(
CWnd* pParent,
int nControlID_whenDinamicCreationOnly=0xffff
);
Тут создаём и инициализируем заголовок
m_Header и полосы прокрутки
m_HBar и
m_VBar.
void SendNotifyMessToParent(
ee_callbacksMessages eeID,
UINT nChar=0xffffffff
);
Отправка сообщения родительскому окну. Здесь вызывается
callback-функция, если назначена сообщению, либо выполняется встроенный обработчик, если он не выключен флагом
B_DontRunIfCallbackIsNull метода
SetMessageCallBack (смотри выше по тексту)
Обработчики событий по умолчанию выполняют следующее:
1)
mes_OnUpSqPressed - прокрутить таблицу в самый верх
2)
mes_OnVScroll_slidermove - прокрутка в соответствии с положением ползунка вертикальной полосы прокрутки (логическое количество строк == все строки).
3)
mes_OnMouseWheel - прокрутка вверх/вниз на 1 строку
4)
mes_OnVScroll_arrowup,
mes_OnVScroll_arrowdown - прокрутка вверх/вниз на 1 строку.
5)
mes_OnVScroll_placeup,
mes_OnVScroll_placedown - прокрутка вверх/вниз на 5 строк.
6)
mes_OnVirtualKeyDown - передвижение курсора по таблице при помощи стрелок клавиатуры.
bool SetUserCanChangeProperty(
bool b
);
Разрешает/запрещает пользователю вызвать меню настроек грида (щелчок правой кнопкой по нижнему правому квадратику грида). В обработчике сообщения
WM_RBUTTONUP таблицы.
afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
В
OnRButtonUp() создаётся контекстное меню. Сейчас там один пункт, да и то холостой. Если вам потребуется, создавайте своё меню и наполняйте его смыслом.
bool GetCellRowColRectOfPoint(
CPoint point,
DWORD* row,
DWORD* col
);
Определяет положение клетки по координатам точки на гриде (координаты точки - относительные для грида, то есть левый верхний угол таблицы - 0,0).
bool GetCellRect(
RECT& r,
DWORD row,
DWORD col,
bool bAbsolutCoord=true
);
Получить прямоугольник клетки (абсолютные/относит координаты , в зависимости от флага
bAbsolutCoord).
void EditCell(
DWORD row,
DWORD col
);
Открыть окошко для прямого редактирования текста ячейки.
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
Здесь реагируем на полосы прокрутки.
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
OnNotify() Обрабатывает сообщения от элемента заголовка.
Двойной щелчок по разделителю заголовка - выравниваем ширину колонки по ширине текста максимальной длины в видимой части колонки.
Двойной щелчок по заголовку колонки - уменьшаем ширину колонки до минимума.
Q: С какого числа начинаются индексы колонок и столбцов ?
A: С нуля.
Q: Что означает постфикс "
_not_saved" у некоторых методов?
A: Означает, что для ускорения работы в этих методах не производится проверка на существование колонок и столбцов с индексами (и их диапазонами), которые (индексы) передаются в метод. Поэтому, перед вызовом методов необходимо убедиться в существовании индексов методом
GetCellsAreBeing() . Например, это можно сделать один раз перед "долгим" циклом.
Q: Каково максимальное количество строк и столбцов ?
A: Это задаётся в константах
CGridEdit1153::def_RowsMax и
CGridEdit1153::def_ColsMax . Эти константы можно менять от 1 до
INT_MAX (ну, в разумных пределах :) ).
Q: В статье описано, как создать экземпляр грида при помощи
визарда студии, а можно ли это сделать полностью в динамике - через
new ?
A: Можно.
CGridEdit1153* m_pGrid;
...
...
m_pGrid = new CGridEdit1153;
m_pGrid->Create(указатель_на_родителя, идентификатор_грида_на_диалоге);
m_pGrid->MoveWindow(...);//размещаем, где нужно
m_pGrid->ModifyStyle(0 ,SS_SUNKEN | SS_NOTIFY, 0);
//SS_NOTIFY - обязательно. Хотя это свойство всё равно установиться в Create автоматически, но для порядку ставьте.
//SS_SUNKEN - если охота лёгкую рамочку вокруг грида
...
...
Q: Никак не могу рассчитать точное значение позиции ползунка, соответствующее нужному сдвигу таблицы.
A: На самом деле, положение горизонтального ползунка - это вторичная информация, которая лишь показывает примерное положение верхней видимой строки таблицы по отношению к строкам всей таблицы. Когда двигают ползунок - сообщение
mes_OnVScroll_slidermove "выдаёт" в процентах "откуда" и "куда" сдвинули (смотри методы структуры
CGridEditUP1153::CVBarInfo m_VBI). Если же таблицу сдвинули по горизонтали не ползунком, то для коррекции полосы нужно вызвать
CGridEditUP1153::CorrectVBarSliderPos().
Q: По умолчанию курсор можно передвигать стрелками. Мне это не нужно, как остановить передвижение стрелками, не определяя обработчик, возвращающий 0 ?
A: Можно установить для сообщения флаг, который при отсутствии переопределённого обработчика не будет вызывать встроенный обработчик:
//отключение встроенного обработчика
pGrid->SetMessageCallBack(CGridEdit1153::mes_OnVirtualKeyDown, 0, 1,false);
Q: Что за странное названия класса?
A: Символы "
Grid" говорят сами за себя - это таблица. Остальные символы ничего не говорят, но зато название вряд ли где повторится :) А вот так захотелось.
Q: Почему 4-я редакция ?
A: Совершенству нет предела, нет его и тут. Но серьёзно переделывать вроде больше не собираюсь. Если только глюко-косметические поправки.
Весь тестовый проект (вместе с файлами класса, они лежат в папке проекта) можно найти
здесь.
Обсуждение и замечания можно произвести в уже существующей
теме.