Доброе время суток.
В этой статье представлен класс CGridEdit1153 (далее - просто компонент), написанный в среде VC++6 с использованием библиотеки MFC , предназначенный для вывода на экран простых табличных данных. Здесь нет таких специальных примочек, как объединение ячеек, различная высота строк, отдельное расцвечивание символов или вставка картинок. Класс специально создавался для проекта, где необходимо выводить на экран МНОГО строк (1...5 миллионов строк - суровая реальность) и затачивался под быстрое добавление новых данных и перерисовку без мерцания (в том проекте эта таблица - быстро заполняющийся лог сообщений от сотен охранных приборов, пользователь-оператор мониторит таблицу и принимает решения, и, соответственно, моргание утомляло бы глаза, а тормоза - вообще недопустимы). Поддерживается задание шрифта, задание фона для каждой ячейки, задание цвета текста для каждой ячейки, прямое редактирование текста ячейки, etc. Таблица не универсальна, конечно, но все минусы решаемы, потому что код открыт и прокомментирован :) . А кроме того, эта статья является неким мануалом по вставке в проект и использованию компонента.
Возникает вопрос по поводу странного названия класса. Символы "Grid" говорят сами за себя. Остальные символы ничего не говорят, зато название вряд ли где не повторится :) .

Исходный код компонента представлен в 5 файлах (их можно взять из файлов проекта, который находится по ссылке в конце статьи):

GridEdit1153.h заголовочный файл класса компонента.
GridEdit1153.cpp файл реализации класса компонента.
GridEdit1153_notify.h предоставляет обработчики сообщений для родительского окна (описание - ниже по течению).
EnterEditDialog.h заголовочный файл диалога CEnterEditDialog
EnterEditDialog.cpp файл реализации диалога CEnterEditDialog

Вспомогательный класс CEnterEditDialog предназначен для для прямого редактирования текста ячеек. Он является классом, производным от класса CDialog. Для него нужен диалог-ресурс, который сделать очень просто при помощи редактора ресурсов:
1) вставляем в ресурсы новый диалог, ID == IDD_ENTER_EDIT_DIALOG.
2) удаляем всё с поверхности диалога.
3) у диалога в свойствах -> окно Border -> устанавливаем None.
4) кладём на диалог элемент CEdit , ID == IDC_edTHEEDIT.
5) у CEdit в свойствах -> убираем галку Border , ставим галку Auto HScroll.


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

Итак, создаём тестовый проект GridTest (MFC , Dialog-based). Копируем в папку с проектом 5 файлов, описанных выше. Добавляем файлы также в дерево файлов проекта. Добавляем ресурс для диалога, как это описано выше. После этого рекомендуется удалить из папки проекта файл "имя_проекта.clw" (можно даже не закрывая студию). Затем в студии нажать Ctrl+W для того, чтобы обновилось дерево классов.
Описываемый класс является производным от MFC-класса CStatic. Поэтому, чтобы поместить компонент на диалоговое окно в режиме редактирования ресурсов, кладём сначала на диалог CStatic из стандартной палитры конторолов. Зададим идентификатор IDC_GRID. Поставьте в свойствах статика следующие галочки:
1) Notify (обязательно. Если не поставить галочку, то контрол не будет реагировать на щелчки мышью.)
2) Sunken (это по желанию. Будет модная каёмочка :) )
3) Устанавливаем размер и положение будущего контрола. (Потом, конечно, можно будет поменять программно при помощи CWnd::MoveWindow() )

Добавляем для статика связанный с ним член-переменную: удерживая 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))
{
//задание шрифта 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->SetLogFont(&lf);

//высота ячеек
pGR->SetRowHeight(20);

//включить отсылку извещений к родителю
pGR->SetEnableNotifyParent(true);

//установка количества строк и столбцов
pGR->DeleteTable();
enum
{
e_rows = 10,
e_cols = 3,
};

CString colsarray[e_cols]={"1","2","3"};
int widsarray[e_cols]={20,20,100};

pGR->SetRowsCols(e_rows,e_cols,
widsarray,colsarray,100);
}
//////////////////////////////////
//....
//....


Теперь надо обработать сообщения от контрола. Имеется следующий набор сообщений (его вы , конечно же, можете расширить):
IDmess_OnLButtonDown Нажатие левой кнопкой мыши на клетку
IDmess_OnRButtonDown Нажатие правой кнопкой мыши на клетку
IDmess_OnMButtonDown Нажатие средней кнопкой мыши на клетку
IDmess_OnLButtonDblClk Двойной щелчок левой кнопкой мыши по клетке
IDmess_OnRButtonDblClk Двойной щелчок правой кнопкой мыши по клетке
IDmess_OnRandLButtonDown Одновременное нажатие левой и правой кнопкой мыши на клетку
IDmess_OnUpSqPressed Нажатие кнопки грида "наверх" (верхний правый угол контрола)
IDmess_OnCellChange Сменилось положение курсора-рамки
IDmess_OnVScroll_slidermove Передвинут ползунок горизонтальной полосы прокрутки
IDmess_OnVScroll_arrowup Нажата стрелка "вверх" горизонтальной полосы прокрутки
IDmess_OnVScroll_arrowdown Нажата стрелка "вниз" горизонтальной полосы прокрутки
IDmess_OnVScroll_placeup Нажата область выше ползунка на горизонтальной полосе прокрутки
IDmess_OnVScroll_placedown Нажата область ниже ползунка на горизонтальной полосе прокрутки
IDmess_OnVirtualKeyUp Нажата кнопка клавиатуры
IDmess_OnVirtualKeyDown Отпущена кнопка клавиатуры
IDmess_OnTRACKING Идет изменение ширины столбца мышью (за разделитель в заголовке)
IDmess_OnHScroll_MOVED Перемещён ползунок горизонтальной полосы прокрутки
IDmess_OnMouseWheel Было повёрнуто колесо мыши


Для реализации обработки сообщений добавляем в главный диалог виртуальную функцию OnNotify(...).
В конце файла GridEdit1153_notify.h приведён пример, как обрабатывать сообщения от этого контрола. Код вставляется в OnNotify(...) :

Код:
//обработка сообщений от грида CGridEdit1153
{{{
//ЗДЕСЬ УКАЗЫВАЕТСЯ Идентификатор_Грида (ID контрола на окне)
#define MSG_CGRIDEDIT1153_1 (((Идентификатор_Грида)))
//--------------------------------------------------

//ЗДЕСЬ ЗАДАЁМ Логическое_Количество_Строк ДЛЯ
//ВСТРОЕННОЙ ПРОКРУТКИ
//(0- максимальное кол-во строк)

#define MSG_CGRIDEDIT1153_2 (((Логическое_Количество_Строк)))
#include "GridEdit1153_notify.h"


//тут доступны для чтения переменные:

//pGR
// указатель на грид

//info->hwndFrom;
// хендл окна грида

//info->idFrom;
// ID грида, как контрола

//info->code;
// ID уведомляющего сообщения от грида

//info->pVBI->...
// Информация о положении вертикального
// ползунка (см. определение CVBarInfo)

//info->dwdLogicNumberOfRows
// Логическое количество строк в таблице
// (по умолчанию == pGR->GetRows())



//ОБРАБОТЧИКИ СОБЫТИЙ

// Если в блоке case после своего кода нужно
//вызвать ещё и обработчик по умолчанию,
//то выполнить макрос MSG_CGRIDEDIT1153_dflt.
// Также обработка по умолчанию произойдёт
//автоматом для сообщений, которые здесь
//не указаны

//(использовать переменные в info->pVBI->... ТОЛЬКО для чтения!!!)
// case pGR->IDmess_OnUpSqPressed: { }break;
// case pGR->IDmess_OnVScroll_slidermove: { }break;
// case pGR->IDmess_OnTRACKING: { }break;
// case pGR->IDmess_OnHScroll_MOVED: { }break;
// case pGR->IDmess_OnLButtonDblClk: { }break;
// case pGR->IDmess_OnLButtonDown: { }break;
// case pGR->IDmess_OnRButtonDblClk: { }break;
// case pGR->IDmess_OnRandLButtonDown: { }break;
// case pGR->IDmess_OnCellChange: { }break;
// case pGR->IDmess_OnMButtonDown: { }break;
// case pGR->IDmess_OnRButtonDown: { }break;
// case pGR->IDmess_OnVirtualKeyUp: { }break;
// case pGR->IDmess_OnVirtualKeyDown: { }break;
// case pGR->IDmess_OnVScroll_arrowup: { }break;
// case pGR->IDmess_OnVScroll_arrowdown: { }break;
// case pGR->IDmess_OnVScroll_placeup: { }break;
// case pGR->IDmess_OnVScroll_placedown: { }break;
// case pGR->IDmess_OnMouseWheel: { }break;


#define MSG_CGRIDEDIT1153_4
#include "GridEdit1153_notify.h"

//если сообщение было левое (не для этого
//контрола) то станет выполнятся код за тремя
//скобками. Если же сообщение обработано,
//то тут произошёл выход из процедуры
}}}



Тут, в принципе, всё расписано в комментариях. Пояснения:
1) Идентификатор_Грида -
идентификатор контрола в ресурсах.
2) Логическое_Количество_Строк -
к примеру, создана таблица на 100 строк. Но заполнены ещё только первые 60. В данном случае , 60 - это и есть логическое количество строк, только они будут участвовать в прокрутке. Остальные 40 строк в конце - они не будут видны на экране. Можно, конечно, указать и всё количество строк - 100. Также, можно указать 0 , это будет равносильно указанию максимального количества строк.

Добавьте свой код блоках case обработчиков. Работает следующее правило:
1) Если для сообщения IDmess_xxxxx не указан блок case , то произойдёт обработка по умолчанию.
2) если блок case указан , то произойдёт выполнение кода в блоке.
3) если в блоке встречается макрос MSG_CGRIDEDIT1153_dflt (а это совсем не обязательно), то выполнится весь код до макроса, затем выполнится обработчик по умолчанию, затем произойдёт выход из блока. Если есть код в блоке case, расположенный после этого макроса , то этот код проигнорируется.

Например, вот несколько обработчиков:

Код:
//нажата кнопка "показать первую строку"
case pGR->IDmess_OnUpSqPressed:
{
//что то сделали
//....

//выполняем обработчик по умолчанию, если надо
//MSG_CGRIDEDIT1153_dflt

//если тут написать ещё код, он игнорится
//...
}
break;

//повёрнуто колесо
//делаем прокрутку 3 строки вместо 1
case pGR->IDmess_OnMouseWheel:
{
if(info->pVBI->tomovevalue>0)
{
//вниз
info->pVBI->tomovevalue=3;
}
else
{
//вверх
info->pVBI->tomovevalue=-3;
}

//продолжаем обработку по умолчанию
MSG_CGRIDEDIT1153_dflt
}
break;

//редактирование ячейки по двойному щелчку
case pGR->IDmess_OnLButtonDblClk:
{
pGR->EditCell(pGR->GetCursorRow_zb(),
pGR->GetCursorCol_zb());
}
break;

//нажатие клавиш
case pGR->IDmess_OnVirtualKeyDown:
{
DWORD col,row;
col=pGR->GetCursorCol_zb();
row=pGR->GetCursorRow_zb();
//Здесь: info->nChar... находится код.
switch(info->nChar)
{
case VK_RETURN://ентер
{
//редактирование ячейки
pGR->EditCell(pGR->GetCursorRow_zb(),
pGR->GetCursorCol_zb());
}
break;

case VK_DOWN://клавиша вниз
{
//двигаем курсор
pGR->SetCursorRow_zb(row+1);
pGR->Invalidate(0);
}
break;

case VK_UP://клавиша верх
{
if(row>0)
{
//двигаем курсор
pGR->SetCursorRow_zb(row-1);
pGR->Invalidate(0);
}
}
break;

case VK_LEFT://клавиша влево
{
if(col>0)
{
//двигаем курсор
pGR->SetCursorCol_zb(col-1);
pGR->Invalidate(0);
}
}
break;

case VK_RIGHT://клавиша вправо
{
//двигаем курсор
pGR->SetCursorCol_zb(col+1);
pGR->Invalidate(0);
}
break;
}
}
break;


Как сказано в комментарии, если сообщение не было предназначено для контрола, идентификатор которого указан в MSG_CGRIDEDIT1153_1, то станет выполнятся код функции OnNotify(), расположенный далее "}}}". Если же сообщение обработано, то произойдёт выход из OnNotify() со значением 1 ("сообщение обработано").


В блоках case обработчиков доступны следующие переменные :
CGridEdit1153 *pGR; Указатель на объект контрола
const CGridEdit1153::CGridEdit1153_NMHDR *info; указатель на структуру CGridEdit1153_NMHDR, содержащую информацию, нужную для обработчиков сообщений (см. чуть ниже)


(*info) содержит следующую информацию (актуальность переменных зависит от обработчика):
Код:
//границы для вертикальной полосы прокрутки
//(тип переменных - const int)
info->pVBI->minpos //минимальная позиция ползунка
info->pVBI->maxpos //максимальная позиция ползунка

//при перетаскивании ползунка вертикальной полосы прокрутки
//(тип переменных - const int)
info->pVBI->posfrom //позиция, откуда начато перемещение
info->pVBI->poswhere //позиция, куда ползунок переместили

//для нажатия стрелок и пространства снизу или сверху от ползунка
//вертикальной полосы прокрутки
//(тип переменных - const int)
info->pVBI->tomovevalue //на сколько требуется подвинуть ползунок (со знаком)

//логическое количество строк в таблице (тип const DWORD)
info->dwdLogicNumberOfRows

//символ для сообщений от клавиатуры (тип const UINT)
//(значение из CWnd::OnKeyUp(UINT nChar, ...))
info->nChar


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

Код:
BOOL CGridTestDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
//определяем ситуацию, когда контрол-таблица
//не в фокусе, но курсор мыши находится сейчас
//над таблицей.
{
RECT r;
POINT pnt;

CGridEdit1153* pGR=&m_IDC_GRID;

//текущий прямоугольник грида (абсолютные координаты)
pGR->GetWindowRect(&r);

//текущее положение курсора мыши (абсолютные координаты)
::GetCursorPos(&pnt);

//проверяем , находится ли курсор мыши над гридом

if(pnt.x>=r.left && pnt.x<=r.right &&
pnt.y>=r.top && pnt.y<=r.bottom)
{
//делаем имитацию прокрутки для грида
pGR-> Imitate_CtrlSendsMouseWheelNotifyForParent(zDelta);

//и не будем обрабатывать прокрутку для *this окна
return 0;
}
}

//...
//...

return CDialog::OnMouseWheel(nFlags, zDelta, pt);
}


Итак, мы уже посмотрели на компонент, так сказать снаружи, выяснили, как обрабатывать сообщения от него. Теперь пройдёмся немного по коду файлов компонента.
Файл EnterEditDialog.cpp, файл реализации вспомогательного диалога для прямого редактирования ячеек. Всё необходимое диалогу передаётся в конструктор. Диалог открывается методом DoModal , и пока не будет закрыт клавишей Enter или Esc , таблица недоступна для пользователя. Впрочем , как и главное окно проекта :) . Модальный диалог-с. Код диалога небольшой , в комментариях в файле всё расписано.
Файл GridEdit1153.h , заголовочный файл таблицы. Содержим все константы компонента, содержит описание вспомогательных структур. В принципе, тут тоже всё подробно закомментировано.
Файл GridEdit1153.cpp, файл реализации таблицы. Опишу моменты, на которые стоит обратить особое внимание.


В конструкторе , помимо инициализации переменных, задаётся будущий шрифт по умолчанию - Arial, 15.


Тут создаётся динамический массив с ячейками. Если до вызова процедуры не вызвать DeleteTable(), то функция зразу же завершится, ничего не создавая нового.
Код:
bool CGridEdit1153::SetRowsCols(DWORD rows,
DWORD cols,int* widsarray,CString* colsarray,
int headtextlenmax)


Функция просто очищает ячейки от текста, не удаляя массив ячеек.
Код:
void ClearTable(DWORD begrow_zb=0,DWORD endrow_zb=0xffffffff);



Тут создаём и инициализируем заголовок m_Header и полосы прокрутки m_HBar и m_VBar.
Код:
BOOL CGridEdit1153::Create(CWnd* pParent)


Отправка сообщения родительскому окну.
Код:
SendNotifyMessToParent(int nID,UINT nChar)
Если m_pParent->SendMessage(...) возвращает значение pResult_RunDef, то выполняется обработчик события по умолчанию. А это:
1) IDmess_OnUpSqPressed - прокрутить таблицу в самый верх
2) IDmess_OnVScroll_slidermove - прокрутка в соответствии с положением ползунка вертикальной полосы прокрутки (логическое количество строк == все строки).
3) IDmess_OnMouseWheel - прокрутка вверх/вниз на 1 строку
4) IDmess_OnVScroll_arrowup,IDmess_OnVScroll_arrowdown - прокрутка вверх/вниз на 1 строку.
5) IDmess_OnVScroll_placeup , IDmess_OnVScroll_placedown: - прокрутка вверх/вниз на 5 строк.



Разрешает/запрещает пользователю вызвать меню настроек грида (щелчок правой кнопкой по нижнему правому квадратику грида). В обработчике сообщения WM_RBUTTONUP контрола
Код:
bool CGridEdit1153::SetUserCanChangeProperty(bool b)



В OnRButtonUp() создаётся контекстное меню. Сейчас там один пункт, да и то холостой. Если вам потребуется, создавайте своё меню и наполняйте его смыслом.
Код:
void CGridEdit1153::OnRButtonUp(UINT nFlags, CPoint point);



Определяет положение клетки по координатам точки на гриде (координаты точки - относительные для грида, то есть левый верхний угол контрола - 0,0).
Код:
bool CGridEdit1153::GetCellRowColRectOfPoint(
CPoint point, DWORD *row, DWORD *col)



Получить прямоугольник клетки (абсолютные/относит координаты , в зависимости от флага bAbsolutCoord).
Код:
bool CGridEdit1153::GetCellRect(
RECT &r,DWORD row,DWORD col,bool bAbsolutCoord)


Сымитировать сообщение о повороте колеса мыши (всё отработает, как и при реальном повороте, единственно, что реально сообщение WM_MOUSEWHEEL не будет послано в грид)
Код:
void Imitate_CtrlSendsMouseWheelNotifyForParent(short zDelta)


Открыть окошко для прямого редактирования текста ячейки
Код:
void CGridEdit1153::EditCell(DWORD row,DWORD col)


Здесь реагируем на полосы прокрутки
Код:
void CGridEdit1153::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
void CGridEdit1153::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)


OnNotify Обрабатывает сообщения от элемента заголовка.
Код:
BOOL CGridEdit1153::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
Двойной щелчок по разделителю заголовка - выравниваем ширину колонки по ширине текста максимальной длины в видимой части колонки.
Двойной щелчок по заголовку колонки - уменьшаем ширину колонки до минимума.


Ну вот вроде бы и всё. Надеюсь, мой компонент окажется полезен :)


Автор: Алексей1153
Information
  • Posted on 31.01.2010 21:35
  • Просмотры: 342