Uživatelsky kreslené prvky Windows - Check-Box

Dnes se trochu blíže podíváme na check-box. Ti zkušenější asi vědí, že z pohledu programátorského modelu Windows není check-box zvláštní třída, ale je to ve skutečnosti jeden z typů buttonu, tedy třída „BUTTON“.

Proč se ještě vracím k buttonu, si hned řekneme; nejdříve – jako obvykle – obrázek z ukázkové aplikace:

Problémy s check-boxem v MFC

Při použití check-boxu v aplikaci používající knihovnu MFC check-box zapouzdřuje třída CButton. Avšak v editoru zdrojů máme check-box jako samostatný prvek vedle tlačítka („push-button“) a radio-buttonu. A zde narazíme na první problém v případě, že chceme mít check-box uživatelsky kreslený. Editor zdrojů nám ve vlastnostech check-boxu nenabízí zvolit vlastnost „owner-draw“, jako je tomu u tlačítka. Dobře, řeknete si, nastavíme tuto vlastnost za běhu, pomocí SetWindowLongPtr, kde přidáme do stylu vlastnost BS_OWNERDRAW. Když to však zkusíte v praxi, program skončí chybou. Zkusil jsem tedy nedávat check-box na dialog v editoru zdrojů, ale vytvořit ho za běhu jako proměnnou typu třídy CButton (nebo nějaké vlastní třídy odvozené od CButton) funkcí Create, kde jsem do stylu uvedl BS_OWNERDRAW. Výsledkem byl opět pád programu. Nebudeme tedy dále rozebírat příčiny tohoto chování a vytvoříme si check-box bez pomoci třídy CButton a editoru zdrojů. V ukázkové aplikaci jsem tedy vytvořil vlastní třídu CCheckBox odvozenou přímo od CWnd, což je obecné okno, tedy předek všech „okenních“ tříd v MFC. Tuto třídu jsem vytvořil víceméně z důvodu opakování kódu, neboť na dialogu je 5 check-boxů, jejichž „proces vytváření“ je stejný. Navíc je tím dán lepší prostor k vlastnímu rozvíjení univerzální funkčnosti tohoto check-boxu, což již nechám na čtenáři.

Vlastní třída CCheckBox

Třída bude mít 2 členské proměnné typu HFONT, které budou představovat písma pro text položky (budeme používat jiné písmo pro „zaškrtnuté“ a „nezaškrtnuté“ položky), dále handle image-listu, ve kterém budou 2 obrázky (v našem případě ikony), představující grafickou reprezentaci dvou stavů check-boxu. Dále zde bude public funkce Create, kterou budeme volat „zvenku“ pro vytvoření check-boxu. Jako poslední přidáme public funkci pro zjištění stavu check-boxu „zvenku“. V hlavičce tedy bude kromě základní kostry nové třídy toto:

public:
  HIMAGELIST m_ImageList;
  HFONT m_hfont2;
  HFONT m_hfont;
  BOOL Create(int x, int y, int nWidth, int nHeight,
    LPCTSTR lpText, HWND hwndParent);
  BOOL GetChecked();

Celé vytvoření okna a inicializaci uvedených členských prvků provedeme ve funkci Create:

BOOL CCheckBox::Create(int x, int y, int nWidth, int nHeight, LPCTSTR lpText, HWND hwndParent)
{
  if ( !CreateEx(0, "BUTTON", lpText,
    WS_CHILD | WS_VISIBLE | BS_CHECKBOX | BS_OWNERDRAW,
    x, y, nWidth, nHeight, hwndParent, (HMENU)NULL, NULL) )
      return FALSE;
  LOGFONT lf;
  ZeroMemory(&lf, sizeof(LOGFONT));
  lf.lfHeight = 24;
  strcpy(lf.lfFaceName, "Comic Sans MS");
  m_hfont = CreateFontIndirect(&lf);
  lf.lfItalic = TRUE;
  m_hfont2 = CreateFontIndirect(&lf);
  return TRUE;
}

Jak jsem již napsal, handle na image-list přiřadíme „zvenku“. Při větším množství check-boxů budeme pravděpodobně chtít jednotný grafický vzhled, proto nebudeme vytvářet image-list pro každý check-box zvlášť.

Nyní k vlastnímu vykreslení check-boxu. Protože jde o uživatelsky kreslený button, vlastník check-boxu bude dostávat zprávy WM_DRAWITEM (podrobněji v úvodních dílech tohoto seriálu, kde jsem rozebíral uživatelsky kreslený button). V tomto případě jsem kreslící funkci umístil do třídy CCheckBox, proto budeme muset zajistit, aby dialog „přeposlal“ tuto zprávu příslušnému check-boxu. To si ukážeme za chvíli, nyní se ještě podíváme na handler zprávy WM_DRAWITEM (vygenerovaný pomocí ClassWizarda ve třídě CComboBox).

void CCheckBox::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
  TCHAR chText[50];
  if ( GetWindowLongPtr(lpDrawItemStruct->hwndItem, GWLP_USERDATA) == 0 )
  {
    ImageList_Draw(m_ImageList, 1, lpDrawItemStruct->hDC,
      lpDrawItemStruct->rcItem.left,
      lpDrawItemStruct->rcItem.top,
      ILD_NORMAL);
    SetTextColor(lpDrawItemStruct->hDC, 0x000000FF);
    SelectObject(lpDrawItemStruct->hDC, m_hfont);
  }
  else
  {
    ImageList_Draw(m_ImageList, 0, lpDrawItemStruct->hDC,
      lpDrawItemStruct->rcItem.left,
      lpDrawItemStruct->rcItem.top,
      ILD_NORMAL);
    SetTextColor(lpDrawItemStruct->hDC, 0x0000D000);
    SelectObject(lpDrawItemStruct->hDC, m_hfont2);
  }
  ::GetWindowText(lpDrawItemStruct->hwndItem, chText, 50);
  lpDrawItemStruct->rcItem.left += 36;
  SetBkMode(lpDrawItemStruct->hDC, TRANSPARENT);
  DrawText(lpDrawItemStruct->hDC, chText, -1, &lpDrawItemStruct->rcItem,
    DT_SINGLELINE | DT_VCENTER | DT_LEFT);
}

V kódu této funkce vidíte, že informaci o stavu check-boxu, tj. zda je zaškrtnutý, získáváme z uživatelsky definovaných dat okna. Tím se dostáváme k dalšímu „problému“ s uživatelsky kresleným check-boxem. V tomto případě nelze použít styl BS_AUTOCHECKBOX, který se automaticky přepíná v reakci na standardní akce uživatele, tedy kliknutí nebo stisknutí mezerníku, které má check-box „překlopit“ do opačného stavu. Toto překlopení proto musíme realizovat sami. Znamená to reagovat na notifikační zprávu BN_CLICKED, která (i když z názvu to hned nevyplývá) je posílána právě i třeba na stisknutí mezerníku, zpráva tedy obecně určuje, že má dojít k překlopení check-boxu. U owner-draw check-boxu je ještě jeden problém: jeho stav nelze nastavit posláním zprávy BM_SETCHECK. U běžného check-boxu bychom totiž mohli v reakci na BN_CLICKED zjistit stav pomocí zprávy BM_GETCHECK a nastavit opačnou hodnotu právě pomocí BM_SETCHECK. Řešení tohoto „problému“ je samozřejmě více. Jde o to si „někam uložit“ stav. Já jsem pro to využil tzv. „uživatelská data“, která má každé okno. Jde o hodnotu typu LONG_PTR, resp. LONG, do které si můžeme uložit cokoli zcela podle libosti. Tato data jsem tedy použil pro uložení stavu check-boxu. Pokud je zaškrtnut, je v nich hodnota 1, v opačném případě 0. Nastavení těchto hodnot realizujeme již v kódu dialogu, tedy vlastníka check-boxu(-ů). Takto tedy bude vypadat kód funkce GetChecked:

BOOL CCheckBox::GetChecked()
{
  return ( GetWindowLongPtr(m_hWnd, GWLP_USERDATA) > 0 );
}

Kód realizovaný v dialogu

Podívejme se nejprve na obsluhu notifikační zprávy BN_CLICKED, která je posílána ve zprávě WM_COMMAND, jde o část kódu procedury okna dialogu:

……
case WM_COMMAND:
  if ( (HIWORD(wParam) == BN_CLICKED ) )
  {
    if ( GetWindowLongPtr((HWND)lParam, GWLP_USERDATA) == 0 )
      SetWindowLongPtr((HWND)lParam, GWLP_USERDATA, 1);
    else
      SetWindowLongPtr((HWND)lParam, GWLP_USERDATA, 0);
    ::RedrawWindow((HWND)lParam, NULL, NULL,
      RDW_ERASE | RDW_INVALIDATE | RDW_ERASENOW );
  }
  break;
……

Jak vidíte, handle příslušného check-boxu máme v parametru lParam zprávy WM_COMMAND. Při změně stavu pak ještě musíme zajistit překreslení, které vyvoláme funkcí RedrawWindow.

V dialogu tedy máme 5 proměnných typu naší třídy CCheckBox nazvaných m_CheckBox až m_CheckBox5, dále image-list pro uložená zobrazovaných ikon u check-boxů a navíc bitmapu pro pozadí celého dialogu (což už přímo nesouvisí s check-boxem, ale nedávno byl na builder.cz nějaký dotaz na použití vlastního pozadí dialogu, takže to zde uvádím jako radu nebo inspiraci pro někoho). Nyní se podívejme na vlastní inicializační kód přidaný do funkce InitDialog:

// Vlastní inicializační kód
m_bitmapBack = LoadBitmap(AfxGetInstanceHandle(), AKEINTRESOURCE(IDB_BACK));
m_ImageList = ImageList_Create(32, 32, ILC_COLOR | ILC_MASK, 4, 4); 
ImageList_AddIcon(m_ImageList, LoadIcon(AfxGetInstanceHandle(),
  MAKEINTRESOURCE(IDR_MAINFRAME)));
ImageList_AddIcon(m_ImageList, LoadIcon(NULL, IDI_ERROR));
int yPos = 40;
#define _cb_width 150
m_CheckBox.Create(10, yPos, _cb_width, 32, "check-box 1",  m_hWnd);
yPos += 42;
m_CheckBox2.Create(10, yPos, _cb_width, 32, "check-box 2",  m_hWnd);
yPos += 42;
m_CheckBox3.Create(10, yPos, _cb_width, 32, "check-box 3",  m_hWnd);
yPos += 42;
m_CheckBox4.Create(10, yPos, _cb_width, 32, "check-box 4",  m_hWnd);
yPos += 42;
m_CheckBox5.Create(10, yPos, _cb_width, 32, "check-box 5",  m_hWnd);
m_CheckBox.m_ImageList = m_ImageList;
m_CheckBox2.m_ImageList = m_ImageList;
m_CheckBox3.m_ImageList = m_ImageList;
m_CheckBox4.m_ImageList = m_ImageList;
m_CheckBox5.m_ImageList = m_ImageList;

Ukažme si zmíněný kód pro použití vlastního pozadí dialogu a pak se hned vrátíme k tématu. Bitmapu máme načtenou v InitDialog a jediné, co musíme udělat, je doplnit handler zprávy WM_PAINT, tedy funkci OnPaint. Tučně je zvýrazněn kód přidaný ke kódu vygenerovaným wizardem při vytvoření aplikace:

void CUkazkaDlg::OnPaint()
{
  HBRUSH hbrush;
  TCHAR chLabel[] = "Ukázka použití check-boxu";
  CPaintDC dc(this); // device context for painting
  RECT rect;
  if (IsIconic())
  {
    SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
    // Center icon in client rectangle
    int cxIcon = GetSystemMetrics(SM_CXICON);
    int cyIcon = GetSystemMetrics(SM_CYICON);
    CRect rect;
    GetClientRect(&rect);
    int x = (rect.Width() - cxIcon + 1) / 2;
    int y = (rect.Height() - cyIcon + 1) / 2;
    // Draw the icon
    dc.DrawIcon(x, y, m_hIcon);
  }
  else
  {
    GetClientRect(&rect);
    hbrush = CreatePatternBrush(m_bitmapBack);
    FillRect(dc.m_hDC, &rect, hbrush);
    DeleteObject(hbrush);
    dc.SetBkMode(TRANSPARENT);
    dc.SetTextColor(0x00FF0000);
    SelectObject(dc.m_hDC, m_CheckBox.m_hfont);
    dc.TextOut(10, 5, chLabel, strlen(chLabel));
  }
}

Protože handler zprávy WM_DRAWITEM máme v třídě CCheckBox, musíme z dialogu tuto zprávu přeposílat příslušnému check-boxu. Ve třídě dialogu tedy vytvoříme handler zprávy WM_DRAWITEM, ve kterém to budeme velice jednoduše realizovat:

void CUkazkaDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
  ::SendMessage(lpDrawItemStruct->hwndItem, WM_DRAWITEM,
    lpDrawItemStruct->CtlID,
    (LPARAM)lpDrawItemStruct);
}

Nyní již zbývá ukázat si, jak zjistit a aplikovat stav check-boxu. V ukázkové aplikaci jsem poslední dva check-boxy použil pro povolení či zakázání tlačítek OK a Cancel, přesněji řečeno obsluhy identifikátorů IDOK a IDCANCEL, což znamená, že současně zakážeme nebo povolíme také odpovídající „horké klávesy“, tj, <Enter> a <Cancel>. Navíc když je zakázán Cancel, dialog nelze zavřít ani třeba pomocí Alt-F4 nebo systémovou ikonkou (křížek v titulku). Vytvořme tedy hadlery těchto identifikátorů, do kterých doplníme následující kód:

void CUkazkaDlg::OnCancel()
{
  if ( m_CheckBox5.GetChecked() ) 
    CDialog::OnCancel();
}

void CUkazkaDlg::OnOK()
{
  if ( m_CheckBox4.GetChecked() ) 
    CDialog::OnOK();
}

Ukázkový příklad si můžete stáhnout zde CheckBox.zip (120 kB)

Diskuze (5) Další článek: Nvidia uvedla Quadro 2 Go

Témata článku: , , , , , , , , , , , , , , , , , , ,