Статья
Версия для печати
Обсудить на форуме
Создание собственного графического элемента управления с использованием библиотеки MFC.
Часть 2.


Автор: Алексей1153.
Дата написания: 21.11.2009.
Права на статью принадлежат автору и Клубу программистов «Весельчак У».

Содержание.


Живое меню.

Теперь у нас всё готово для оживления меню. А пока пусть по-прежнему будут 3 безымянные кнопки с индексами 0, 1 и 2. Перечислим их:

Код:
class CMyControl:public CStatic
{
enum ee_buttons
{
e_but_noname0,
e_but_noname1,
e_but_noname2,
//
e_but_buttons_count,//количество кнопок
};

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

Оживляем кнопки.

Для демонстрации "живости" кнопок допишем в обработчик OnMouseMove() такой код:

Код:
void CMyControl::OnMouseMove(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

Invalidate(0); // перерисуется всё
}

Подчеркну: это для демонстрации, пока просто грубо перерисуем весь ЭУ при каждом сообщении движения мыши. Invalidate(0) говорит окну, что вся графика окна стала "невалидна", и помещает сообщение WM_PAINT в очередь сообщений. Таким образом, как только закончится обработка всех текущих сообщений, произойдёт вызов OnPaint().

Перерисовать окно, кстати, можно и немедленно, без очереди сообщений — достаточно сразу после Invalidate() вызвать метод UpdateWindow(), тогда сообщение WM_PAINT будет отправлено сразу в оконную процедуру без попадания в очередь сообщений. Такой способ иногда используется, а иногда является и единственно правильным.

Однако так, как сделано сейчас, оставлять нельзя — ведь на каждое сообщение мышедвижения (которых будет много) кроме меню в нашем ЭУ может рисоваться очень много всего, а нам сейчас нужно перерисовать лишь меню. Нам поможет уже сделанное разнесение графики по нескольким функциям. Создавать новый контекст не станем, получим указатель на существующий контекст и перерисуем только меню (PaintMenu_OnCurrentDC()) — тогда, когда курсор над областью меню. Но тут надо учесть ещё два чисто косметических момента:
  • Если курсор был над кнопкой, и она нарисовалась, а затем курсор резко передвинули на другую область ЭУ, то меню не перерисуется, и кнопка так и останется выпуклой. Для обхода этой ситуации используется вспомогательный флаг m_bCurcorWasOverMenu, который взводится, когда курсор мыши попадает в область меню. Если при последующем сообщении мышедвижения окажется, что курсор находится не над меню, но при предыдущем сообщении был над меню, то также перерисуем меню.
  • Аналогично, если курсор резко увели за пределы нашего ЭУ, то флаг m_bCurcorWasOverMenu окажется уже бессилен. Тут воспользуемся обработчиком OnMouseHover().

Код:
void CMyControl::PaintMenu(CDC& dc,const CRect& ClRect,const CRect& MenuRect,bool bUseCalmForAll /* =false */)
{
dc.FillSolidRect(&MenuRect,::GetSysColor(COLOR_3DFACE));
dc.FillSolidRect(MenuRect.left,MenuRect.bottom,MenuRect.Width(),1,::GetSysColor(COLOR_3DSHADOW));

ee_button_state st=(bUseCalmForAll?e_but_calm:e_but_auto);
PaintButton(dc,ClRect,e_but_noname0,st);
PaintButton(dc,ClRect,e_but_noname1,st);
PaintButton(dc,ClRect,e_but_noname2,st);
}

void CMyControl::PaintMenu_OnCurrentDC(bool bUseCalmForAll)
{
// прямоугольник клиентской области
CRect r_CL;
GetClientRect(&r_CL);
// прямоугольник меню
CRect r_Menu;
GetMenuRect(r_CL,r_Menu);

// получаем ссылку на существующий контекст устройства
CDC* pDC=GetDC();
if(pDC)
{
PaintMenu(*pDC,r_CL,r_Menu,bUseCalmForAll);
}
// освобождаем ресурсы, выделенные для ссылки на контекст
ReleaseDC(pDC);
pDC=0;
}

void CMyControl::OnMouseMove(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// Invalidate(0);

// прямоугольник клиентской области
CRect r_CL;
GetClientRect(&r_CL);
// прямоугольник меню
CRect r_Menu;
GetMenuRect(r_CL,r_Menu);

bool bCursorNowOverMenu=(r_Menu.PtInRect(point)?true:false);

if(bCursorNowOverMenu || m_bCurcorWasOverMenu)
{
m_bCurcorWasOverMenu=bCursorNowOverMenu;

PaintMenu_OnCurrentDC();
}
}

void CMyControl::OnMouseLeave()
{
m_TC.ProcessMessage(GetCurrentMessage());

PaintMenu_OnCurrentDC(true);
}

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

Определяем событие нажатия.

Для определения "нажатия на кнопку" отслеживаем такую последовательность действий:
  • Обрабатываем сообщение WM_LBUTTONDOWN (нажатие левой кнопкой мыши) — запоминаем относительные координаты точки щелчка и устанавливаем захват мыши (SetCapture()). Когда мышь захвачена, все сообщения от мыши в пределах приложения будут приходить захватившему её окну. Если фокус переместить на окно другого приложения, захват автоматически снимается.
  • Обрабатываем сообщение WM_LBUTTONUP (отпускание левой кнопкой мыши), сохраняем относительные координаты точки, где отпустили, и проверяем два условия:
    • условие 1 — сейчас имеется захват мыши (причём, нашим окном),
    • условие 2 — окно под курсором — наше окно.
    При невыполнении хоть одного условия ничего не происходит, а захват освобождаем в любом случае.
В функцию ProcessMessage() вспомогательного класса CTrackingControl добавим обработку сообщений WM_LBUTTONDOWN и WM_LBUTTONUP. Также добавим член-переменные для хранения координат и флага зафиксировнного события нажатия (m_pntWasDown, m_pntWasUp, m_bPushEventOccur):

Код:
class CTrackingControl
{
...
...

CPoint m_pntWasDown; // относительные координаты точки при нажатии левой кнопкой мыши
CPoint m_pntWasUp; // относительные координаты точки при отпускании
bool m_bPushEventOccur; // Зафиксировано событие нажатия, нажали в точке m_pntWasDown, отпустили в m_pntWasUp


public:
CTrackingControl(CWnd* pWin=0)
{
m_bPushEventOccur=false;
...
...
}

void ProcessMessage(const MSG* pM) // GetCurrentMessage()
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return;
if(!pM)return;

switch(pM->message)
{
...
...

case WM_LBUTTONDOWN:
{
// сброс флага зафиксированного события нажатия
m_bPushEventOccur=false;

// сохраняем точку нажатия (в относительных координатах)
m_pntWasDown=pM->pt;
::MapWindowPoints(0,m_pWin->m_hWnd,&m_pntWasDown,1);

// делаем захват мыши
m_pWin->SetCapture();
}
break;

case WM_LBUTTONUP:
{
// условие 1 — сейчас имеется захват мыши (причём, нашим окном)
if(m_pWin->m_hWnd==::GetCapture())
{
// охраняем точку отпускания (в относительных
// координатах)
m_pntWasUp=pM->pt;
::MapWindowPoints(0,m_pWin->m_hWnd,&m_pntWasUp,1);

// освобождаем захват мыши
::ReleaseCapture();

// условие 2 — окно под курсором — наше окно.
CPoint pnt=pM->pt;
HWND parent=::GetParent(m_pWin->m_hWnd);
::MapWindowPoints(0,parent,&pnt,1);
if(m_pWin->m_hWnd==::ChildWindowFromPoint(parent,pnt))
{
// зафиксировано событие нажатия
m_bPushEventOccur=true;
}
}
}
break;
}
}

...
...

// определение — захвачена ли мышь нашим окном сейчас
bool IsMouseCaptured()
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return false;
return ::GetCapture()==m_pWin->m_hWnd;
}

// определение, находится ли курсор в прямоугольнике,
// который задан в относительных координатах окна
bool IsMouseInRect(const CRect& r_relat)
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return false;
CPoint pnt;
if(::GetCursorPos(&pnt))
{
::MapWindowPoints(0,m_pWin->m_hWnd,&pnt,1);
if(r_relat.PtInRect(pnt))
{
return true;
}
}

return false;
}

// определить, было ли зафиксировано событие нажатия и получить
// точки нажатия и отпускания кнопки мыши
bool GetPushEventPoints(CPoint& Down,CPoint& Up)
{
Down=m_pntWasDown;
Up=m_pntWasUp;
return m_bPushEventOccur;
}

};

В классе диалога добавляем обработчики LBUTTONDOWN и LBUTTONUP (OnLButtonDown и OnLButtonUp):

Код:
void CMyControl::OnLButtonDown(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// перерисовываем меню, иначе кнопка не утопится сразу
PaintMenu_OnCurrentDC();
}

void CMyControl::OnLButtonUp(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// перерисовываем меню, иначе кнопка останется выпуклой
PaintMenu_OnCurrentDC(true);
}

И функцию определения индекса кнопки по точке:

Код:
class CMyControl:public CStatic
{
...
...
// определить индекс и, если надо, прямоугольник кнопки по заданной координате
bool GetButtonIndxAndRectFromPoint(
const CPoint& pnt, const CRect& LayerRect,
int* pbutIndx_zb=0,CRect* pbutRect=0)
{
CRect r_Menu;
GetMenuRect(LayerRect,r_Menu);
if(!r_Menu.PtInRect(pnt))return false;

// кнопок немного, поэтому ищем просто перебором
CRect but;

for(int i=0; i<e_but_buttons_count; i++)
{
GetButtonRect(LayerRect,i,but);
if(but.PtInRect(pnt))
{
if(pbutRect)*pbutRect=but;
if(pbutIndx_zb)*pbutIndx_zb=i;
return true;
}
}

return false;
}

...
...


Делаем обработчик нажатия на кнопки меню.

Дело за малым: в обработчике же OnLButtonUp определяем, какому элементу управления было адресовано нажатие — его (элемента) прямоугольнику должны принадлежать обе точки m_pntWasDown и m_pntWasUp.

Код:
void CMyControl::OnLButtonUp(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// определяем, было ли зафиксировано событие нажатия
CPoint Down;
CPoint Up;
if(m_TC.GetPushEventPoints(Down,Up))
{
// проверки точек на принадлежность
// ...
}
}

Если обе точки лежат на поверхности одной и той же рисованной кнопки — это событие нажатия этой кнопки. А также эти две точки можно считать началом и окончанием перетаскивания, когда это нужно. Вот такой код покажет окно с сообщением, в котором будет написано, кнопку с каким индексом нажали

Код:
void CMyControl::OnLButtonDown(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// перерисовываем меню, иначе кнопка не утопится сразу
PaintMenu_OnCurrentDC();
}

void CMyControl::OnLButtonUp(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// перерисовываем меню, иначе кнопка останется выпуклой
PaintMenu_OnCurrentDC(true);

// определяем, было ли зафиксировано событие нажатия
CPoint Down;
CPoint Up;
if(m_TC.GetPushEventPoints(Down,Up))
{
// проверка точек на принадлежность
int indx;
CRect but;

// прямоугольник клиентской области
CRect r_CL;
GetClientRect(&r_CL);

// индекс кнопки из точки Down
if(GetButtonIndxAndRectFromPoint(Down,r_CL,&indx,&but))
{
// также проверяем, что точка Up - на этой же кнопке
if(but.PtInRect(Up))
{
ProcessMenuCommand(indx);
}
}
}
}

void CMyControl::ProcessMenuCommand(int Index)
{
CString txt;
txt.Format("Нажата кнопка с индексом %d",Index);
MessageBox(txt,0);
}
Кстати, традиционно кнопки меню работают немного иначе: для определения индекса кнопки учитывается только точка отпускания. Ну, ничто не мешает вам переделать так же. ;)

Снова кнопки.

Кнопки одинаковой ширины — это неудобно. Ведь на них может находиться текст, который обычно разной длины. Казалось бы: столько всего переделывать! Но на самом деле, мы это уже предусмотрели заранее: прямоугольник кнопки с заданным индексом мы всегда получаем одной единственной функцией — GetButtonRect().
Добавим в класс ЭУ структуру описания кнопки s_button_handle и объявим вектор этих структур для хранения наших кнопок:

Код:
#include <vector>

class CTrackingControl
{
...
...
struct s_button_handle
{
private:
enum
{
e_edgestep=3, // отступ текста от краёв
e_maxtextlen=200, // максимальная длина текста в пикселах
};

CString m_Text; // текст. Для простоты — всегда однострочный
int m_Wid_ifnm1; // ширина текста. Если -1, то не ещё посчитана (_ifnm1 — "if not minus 1" )
int m_TextHig; // высота текста.

public:
s_button_handle()
{
InvalidateWid();
}

void InvalidateWid()
{
m_Wid_ifnm1=-1;
m_TextHig=0;
}

void SetText(const char* pText)
{
InvalidateWid();
m_Text=(pText?pText:"");
}

const CString& GetText()
{
return m_Text;
}

int GetEdgeStep()
{
return e_edgestep;
}

int GetWid(CWnd* pWndForRecalcWid)
{
if(m_Wid_ifnm1<0)
{
// ширину надо рассчитать
if(pWndForRecalcWid)
{
if(::IsWindow(pWndForRecalcWid->GetSafeHwnd()))
{
// расчёт ширины
m_Wid_ifnm1=30;
}
}
}

return min(0,m_Wid_ifnm1);
}
};

std::vector<s_button_handle> m_v_buttons;
typedef std::vector<s_button_handle>::iterator it_v_buttons;
...
...
};

Рассчёт ширины в методе GetWid() ещё не сделан, пока просто поставим "заглушку" m_Wid_ifnm1=30. Обратите внимание на следующую деталь: при инициализации и при смене текста объявляем ширину не посчитанной: присваиваем -1. Ширина посчитается при очередном вызове GetWid(), причём, один раз. Если текст не будет меняться, то и ширину не нужно снова пересчитывать.
Заполнять наш вектор кнопок можно в любое время — сделаем это в конструкторе:

Код:
CMyControl::CMyControl()
{
m_TC=CTrackingControl(this);

m_bIsCreated=false;

// заполнение вектора кнопок
m_v_buttons.resize(e_but_buttons_count);
m_v_buttons[e_but_noname0].SetText("первая кнопка с очень длинным текстом");
m_v_buttons[e_but_noname1].SetText("вторая");
m_v_buttons[e_but_noname2].SetText("третья");
}

Перепишем теперь функцию GetButtonRect() (в частности, определение ширины кнопки) с учётом нашей задумки. Кстати, константа e_Menu_but_wid_percent более не нужна, её вообще удаляем.

Код:
void GetButtonRect(const CRect& LayerRect,int butIndx_zb,CRect& butRect)
{
int H=0; // высота кнопки
int W=0; // ширина кнопки


// определение высоты кнопки
GetMenuH(LayerRect,H);
H=H-e_Menu_but_vert_spasing*2;
// верх и низ прямоугольника
butRect.top=LayerRect.top+e_Menu_but_vert_spasing;
butRect.bottom=butRect.top+H-1;


// определение левой и правой координаты кнопки
butRect.left=LayerRect.left+e_Menu_but_horis_spasing; // начало
butRect.right=butRect.left;
if(butIndx_zb<0 || m_v_buttons.size()<=(DWORD)butIndx_zb)
{
return;
}
else
{
it_v_buttons it=m_v_buttons.begin();
for(DWORD dwd=0; it!=m_v_buttons.end(); dwd++,it++)
{
if(dwd==butIndx_zb)
{
// определение правой координаты кнопки
butRect.right=butRect.left+it->GetWid(this)-1;
break;
}

// добавляем ширину очередной предыдущей кнопки +
// расстояние между кнопками
butRect.left+= it->GetWid(this)+e_Menu_but_horis_spasing;
}
}
}

Ещё подправим код функции GetButtonIndxAndRectFromPoint, чтобы не зависеть там от константы e_but_buttons_count, ведь теперь количество кнопок нужно получать из вектора:

Код:
// определить индекс и, если надо, прямоугольник кнопки по заданной координате
bool GetButtonIndxAndRectFromPoint(
const CPoint& pnt, const CRect& LayerRect,
int* pbutIndx_zb=0,CRect* pbutRect=0)
{
...
...

// for(int i=0; i<e_but_buttons_count; i++)
for(DWORD dwdi=0; dwdi<m_v_buttons.size(); dwdi++)
{
GetButtonRect(LayerRect,(int)dwdi,but);
if(but.PtInRect(pnt))
{
if(pbutRect)*pbutRect=but;
if(pbutIndx_zb)*pbutIndx_zb=(int)dwdi;
return true;
}
}

...
...
}

Также избавляем от перечисления констант функцию PaintMenu():

Код:
void CMyControl::PaintMenu(CDC& dc,const CRect& ClRect,const CRect& MenuRect,bool bUseCalmForAll)
{
dc.FillSolidRect(&MenuRect,::GetSysColor(COLOR_3DFACE));
dc.FillSolidRect(MenuRect.left,MenuRect.bottom,MenuRect.Width(),1,
::GetSysColor(COLOR_3DSHADOW));

ee_button_state st=(bUseCalmForAll?e_but_calm:e_but_auto);


it_v_buttons it=m_v_buttons.begin();
for(DWORD dwd=0; it!=m_v_buttons.end(); dwd++,it++)
{
PaintButton(dc,ClRect,dwd,st);
}
}

Запускаем проект и убеждаемся, что визуально ничего не изменилось (стабильность — это хорошо!). Однако мы-то знаем теперь, что легко уже можем делать ширину кнопки произвольной. Допишем функцию s_button_handle::GetWid(). Также добавим возможность получать размеры прямоугольника текста — это пригодится при его выводе на кнопку.

Код:
// pWndForRecalcWid — указатель на окно, где будет рисоваться текст
int GetWid(CWnd* pWndForRecalcWid,CSize* pTextSize=0)
{
if(pTextSize)
{
pTextSize->cx=0;
pTextSize->cy=0;
}

if(m_Wid_ifnm1<0)
{
// ширину надо рассчитать
if(pWndForRecalcWid)
{
if(::IsWindow(pWndForRecalcWid->GetSafeHwnd()))
{
// расчёт ширины
// m_Wid_ifnm1=30;
CDC* pDC=pWndForRecalcWid->GetDC();
if(pDC && pDC->m_hDC)
{
CSize sz=pDC->GetTextExtent(m_Text);
m_Wid_ifnm1=min(sz.cx,e_maxtextlen);
m_TextHig=sz.cy;
}
pWndForRecalcWid->ReleaseDC(pDC);
pDC=0;
}
}
}

if(pTextSize)
{
pTextSize->cx=((m_Wid_ifnm1<0)?0:m_Wid_ifnm1);
pTextSize->cy=m_TextHig;
}

if(m_Wid_ifnm1<0)return 0;
return m_Wid_ifnm1+2*e_edgestep;
}

Готово! Теперь ширина кнопок переменная, можно запустить проект и убедиться в этом. Но для полной и окончательной красоты выведем ещё сразу и текст. Недолго думая, переписываем функцию CMyControl::PaintButton():

Код:
void CMyControl::PaintButton(CDC& dc,const CRect& ClRect,int butIndx_zb,ee_button_state state)
{
CRect butRect;
GetButtonRect(ClRect,butIndx_zb,butRect);
LONG& L=butRect.left;
LONG& T=butRect.top;
LONG  W=butRect.Width();
LONG  H=butRect.Height();

if(state==e_but_auto)
{
...
...
}

switch(state)
{
...
...
}

// выводим текст
if(butIndx_zb<0 || m_v_buttons.size()<=(DWORD)butIndx_zb)
{
// а нет такой кнопки
}
else
{
s_button_handle& Button=m_v_buttons[butIndx_zb];

CSize TextSize;
if(Button.GetWid(this,&TextSize))
{
// рассчитываем, куда будем выводить текст
CRect TextRect;

// по вертикали — делаем по середине кнопки
TextRect.top=butRect.top+butRect.Height()/2-TextSize.cy/2;
TextRect.bottom=TextRect.top+TextSize.cy;

// по горизонтали — от левого края, учитывая отступ
// от края — GetEdgeStep()
TextRect.left=butRect.left+Button.GetEdgeStep();
TextRect.right=butRect.right-Button.GetEdgeStep();

// если курсор двигается над кнопкой, "выпучиваем" текст
if(state==e_but_over)
{
TextRect+=CPoint(-1,-1);
}

// если кнопка сейчас нажата, "утапливаем" и текст
if(state==e_but_push)
{
TextRect+=CPoint(1,1);
}

// обеспечиваем невыход за пределы кнопки
TextRect.top=max(TextRect.top,butRect.top);
TextRect.bottom=min(TextRect.bottom,butRect.bottom);

dc.SetBkMode(TRANSPARENT);//прозрачность фона текста
dc.SetTextColor(RGB(0,128,0));//цвет текста
dc.DrawText(Button.GetText(),&TextRect,
DT_NOPREFIX| DT_LEFT| DT_SINGLELINE|DT_VCENTER);
}
}
}

Вот что у нас получилось:


рисунок 5

При проведении курсора над кнопкой текст тоже слегка приподнимается, а при нажатии — утапливается. Длинный текст первой кнопки обрезан согласно константе s_button_handle::e_maxtextlen. Если текст не помещается, то можно, например, пририсовать троеточие в конце или нарисовать разрыв на правом краю кнопки. А лучше всего разумно выбрать длину текста для кнопки.
Также посмотрим на правую часть рисунка. Если сильно уменьшить окно, наше меню тоже уменьшится. Текст по-прежнему пытается печататься посредине кнопок, но сверху и снизу обрезается их размерами. Тут можно либо регулировать размер шрифта (о шрифте будет разговор дальше), либо просто не выводить текст, когда он не помещается по вертикали.
Справа текст вылез из ЭУ и попал на главный диалог. И не только текст, но и рамка кнопки себя так же ведёт. Как с этим бороться, будет рассказано также позднее, когда будем рассматривать перерисовку без мерцания. А мерцать сейчас пока нечему, поэтому и отложим на потом, а то сейчас поборем мерцание и так и не увидим его.

Выбираем шрифт.

Шрифт, которым был напечатан текст на кнопках, это шрифт контекста устройства по умолчанию (наверное, сам БГ выбирал :) ). При выводе текста можно использовать произвольные шрифты и даже не один, а сколько угодно. Выберем для меню нашего ЭУ свой шрифт. Добавим член-переменную для объекта шрифта — m_MenuFont. Создадим его в методе CreateMe() и инициализируем при помощи структуры LOGFONT. А при рисовании меню и при расчёте размеров текста применим. Обратите внимание на ещё один новый параметр функции s_button_handle::GetWid() — pFont. Всё правильно, ведь шрифт нужен не только при выводе текста, но и при расчёте его размеров.

Код:
class CMyControl:public CStatic
{
...
...
CFont m_MenuFont;
...
...
};

bool CMyControl::CreateMe(CWnd* pParent,UINT ID,CRect* pRect)
{
...
...
...

if(m_hWnd)
{
LOGFONT lf;
::memset(&lf,0,sizeof(lf));

lf.lfQuality=NONANTIALIASED_QUALITY; // не люблю размазывание

// кегль хитро высчитывается по формуле из MSDN
lf.lfHeight=-MulDiv(10, GetDeviceCaps(::GetDC(0), LOGPIXELSY), 72);

lf.lfCharSet=RUSSIAN_CHARSET; // а какой же ещё? :)

lf.lfWidth=0;
lf.lfEscapement=0;
lf.lfOrientation=0;
lf.lfWeight=FW_NORMAL;
lf.lfItalic=0;
lf.lfUnderline=0;
lf.lfStrikeOut=0;
lf.lfOutPrecision=OUT_CHARACTER_PRECIS;
lf.lfClipPrecision=CLIP_CHARACTER_PRECIS;
lf.lfPitchAndFamily=FF_ROMAN;


const char* FaceName="Times New Roman";
::memmove(lf.lfFaceName,FaceName
,min(::strlen(FaceName),sizeof(lf.lfFaceName)));
lf.lfFaceName[sizeof(lf.lfFaceName)-1]=0;

m_MenuFont.CreateFontIndirect(&lf);
}

return(m_hWnd!=0 && m_bIsCreated);
}

void CMyControl::PaintMenu(CDC& dc,const CRect& ClRect,const CRect& MenuRect,bool bUseCalmForAll)
{
// выбираем шрифт для текста кнопок
CFont* oldFont=dc.SelectObject(&m_DefaultFont);

...
...
...

// возвращаем старый шрифт в контекст
dc.SelectObject(oldFont);
}

struct s_button_handle
{
...
...
// pWndForRecalcWid — указатель на окно, где будет рисоваться текст
int GetWid(CWnd* pWndForRecalcWid,CFont* pFont,CSize* pTextSize=0)
{
if(!pFont || !pFont->m_hObject)
{
InvalidateWid();
return 0;
}

if(pTextSize)
{
pTextSize->cx=0;
pTextSize->cy=0;
}

if(m_Wid_ifnm1<0)
{
// ширину надо рассчитать
if(pWndForRecalcWid)
{
if(::IsWindow(pWndForRecalcWid->GetSafeHwnd()))
{
// расчёт ширины
CDC* pDC=pWndForRecalcWid->GetDC();
if(pDC && pDC->m_hDC)
{
// выбираем шрифт для текста кнопок
CFont* oldFont=0;
pDC->SelectObject(pFont);

CSize sz=pDC->GetTextExtent(m_Text);
m_Wid_ifnm1=min(sz.cx,e_maxtextlen);
m_TextHig=sz.cy;

// возвращаем старый шрифт в контекст
pDC->SelectObject(oldFont);
}
pWndForRecalcWid->ReleaseDC(pDC);
pDC=0;
}
}
}

if(pTextSize)
{
pTextSize->cx=((m_Wid_ifnm1<0)?0:m_Wid_ifnm1);
pTextSize->cy=m_TextHig;
}

if(m_Wid_ifnm1<0)return 0;
return m_Wid_ifnm1+2*e_edgestep;
}
...
...
};

Долго смеялся над одним моментом: когда вытаскиваем дефолтные настройки LOGFONT из окна, поле lf.lfCharSet имеет значение 0xCC, а этим значением заполняются в режиме отладки все неинициализированные ячейки озу, отведённые под переменные. Это показалось особо странно на фоне нормально заполненных остальных полей. А объяснялось всё очень просто — Билл Гейтс опять отличился ))) :

Код:
// файл "WinGDI.h"
#define RUSSIAN_CHARSET         204

Следующая часть статьи немного задерживается по техническим и временнЫм причинам. Код проекта, уже созданный на данной стадии, пока не прикладываю, полагаю, что вам будет интересно всё самим повторить и довести до совершенства. ))

(Продолжение: часть 3.)
Версия для печати
Обсудить на форуме