Uživatelsky kreslené prvky Windows III - ListBox

V předchozím článku jsme se seznámili se základním principem uživatelsky kreslených prvků (owner-draw). Ukázali jsme si jejich použití za pomoci tříd knihovny MFC na příkladě tlačítka. Dnes si řekneme něco o uživatelsky kresleném list-boxu.
Cíl

Stanovme opět si nejprve cíl: Vytvoříme třídu MFC odvozenou od standardní třídy CListBox, která bude implementovat uživatelsky kreslený list-box. Jako ukázkový příklad bude tento list-box umět:

  • Zobrazovat vedle textu jednotlivých položek ikony. Položky list-boxu budeme moci zadávat pomocí vlastní funkce, s tím, že tato funkce bude mít 2 varianty: jednu pro zadání ikony ve formě handle (HICON), druhou pro zadání identifikátoru ikony ve zdrojích (resources) programu.
  • Třída bude mít veřejné (public) proměnné pro volbu 2 barev textu - normální a vybrané položky.
  • Budeme moci nastavit pozadí položek ve formě handle na štětec (HBRUSH), opět 2 hodnoty, pro normální a vybrané položky.
  • Možnost nastavit vlastní rozměry ikon. Možná víte, že pomocí funkce LoadImage lze ikonu načíst do libovolných výsledných rozměrů (samozřejmě s rozlišením běžné ikony, obvykle 32 x 32 - obraz tedy bude uměle natáhnut nebo zmenšen)
Jak na to?

Vytvoříme si aplikaci založenou na dialogu. Přidáme novou třídu odvozenou od MFC třídy CListBox. V ukázce jsem třídu nazval CMyListBox. Do této třídy přidáme následující členské proměnné a funkce:

….
public:
  COLORREF m_crTextSelected;
  COLORREF m_crText;
  int m_IconWidth;
  int m_IconHeight;
  BOOL AddItem(LPCTSTR lpText, UINT uiIconID);
  BOOL AddItem(LPCTSTR lpText, HICON hIcon);
  void SetSelectBrush(HBRUSH hbrush);
  void SetBackBrush(HBRUSH hbrush);
….
private:
  HBRUSH m_SelectBrush;
  HBRUSH m_BackBrush;
….

Do („veřejných“) proměnných typu COLORREF - m_crText a m_crTextSelected budeme moci kdekoli v programu nastavit barvy textu položek (nevybraných a vybraných). Typ COLORREF je ve skutečnosti DWORD, kde v jednotlivých BYTEch jsou barevné složky určující požadovanou barvu a to takto:

0x00BBGGRR

kde BB je modrá složka, GG zelená a RR červená, v rozsahu 0 - 255 (jak plyne z „rozměru“, který je BYTE).

Pro konstrukci hodnoty typu COLORREF můžeme použít například makro:

COLORREF RGB(
  BYTE byRed,    // červená složka barvy
  BYTE byGreen,  // zelená složka barvy
  BYTE byBlue    // modrá složka barvy
);

Proměnné m_IconWidth a m_IconHeight budou určovat již zmíněné požadované rozměry zobrazovaných ikon.

Samozřejmě všechny tyto proměnné (tj. barvy textu a rozměry ikon) budeme na začátku, přesněji řečeno v konstruktoru třídy CMyListBox inicializovat na výchozí hodnoty podle nastavení systému (systémové metriky). Konstruktor tedy vypadá takto:

CMyListBox::CMyListBox()
{
  m_BackBrush = NULL;
  m_SelectBrush = NULL;
  m_IconWidth = GetSystemMetrics(SM_CXICON);
  m_IconHeight = GetSystemMetrics(SM_CYICON);
  m_crText = GetSysColor(COLOR_WINDOWTEXT);
  m_crTextSelected = GetSysColor(COLOR_HIGHLIGHTTEXT);
}

Jak vidíte, vlastní štětce pro výplň pozadí „vynulujeme“. Znamená to, jak uvidíme dále, že v kreslící funkci tyto štětce použijeme pouze, pokud jsou zadány, jinak použijeme opět výplně podle systémové metriky.

Nyní trocha nutné teorie: Každá položka list-boxu (stejně je to i u combo-boxu) může kromě vlastního textu obsahovat tzv. „data položky“, což je libovolné 32-ti bitové číslo, do kterého si můžeme uložit cokoli. V našem případě ho využijeme pro uložení handle ikony (typ HICON), kterou budeme chtít v dané položce zobrazit. K uložení „dat položky“ lze využít zděděnou členskou funkci třídy CListBox

int SetItemData( int nIndex, DWORD dwItemData );

V našem případě jsem použil (mimo jiné ze „studijních důvodů“) přímo zprávy list-boxu na úrovni WinAPI. Stejný postup pak můžete použít u list-boxu, který nebude odvozen od nějaké hotové třídy MFC, pokud třeba budete celou aplikaci psát v API, tj. ve Visual C++ označenou jako „Win32 Application“. Funkce pro přidání položky s ikonou tedy vypadají takto:

BOOL CMyListBox::AddItem(LPCTSTR lpText, HICON hIcon)
{
  int nItem = ::SendMessage(m_hWnd, LB_ADDSTRING, 0, (LPARAM)lpText);
  return ( SendMessage(LB_SETITEMDATA, nItem, (LPARAM)hIcon) != LB_ERR );
}

BOOL CMyListBox::AddItem(LPCTSTR lpText, UINT uiIconID)
{
  HICON hicon = (HICON)LoadImage(AfxGetInstanceHandle(),
    MAKEINTRESOURCE(uiIconID),
    IMAGE_ICON,
    m_IconWidth, m_IconHeight,
    LR_SHARED);
  if ( hicon == NULL )
    return FALSE;
  return AddItem(lpText, hicon);
}

Jak je vidět v kódu, zpráva LB_ADDSTRING nám vrátí index položky, kterou touto zprávou přidáme. Tento index pak použijeme jako parametr zprávy LB_SETITEMDATA, kterou vložíme handle ikony do dat položky.

Druhá varianta funkce používá funkci

HANDLE LoadImage(
  HINSTANCE hinst,    // handle instance
  LPCTSTR lpszName,  // jméno nebo identifikátor grafického prvku
  UINT uType,        // typ obrázku
  int cxDesired,        // požadovaná délka (šířka) obrázku
  int cyDesired,        // požadovaná výška obrázku
  UINT fuLoad        // volby načítání
);

Právě díky této funkci můžeme načíst ikonu do libovolné vlastní velikosti, určené parametry cxDesired a cyDesired. K volbám načítání (parametr fuLoad), jejichž úplný popis najdete v dokumentaci (nejlépe MSDN) bych upozornil na hodnotu LR_SHARED. Znamená to, že pokud je tato hodnota v parametru fuLoad uvedena, opakované volání této funkce (samozřejmě se stejnou hodnotou identifikátoru zdroje) nám vrátí stejný handle, tedy načtený obrázek je sdílený. Zde je rozdíl oproti funkci LoadIcon, která nám vždy vrátí sdílený handle. Pokud tedy hodnotu LR_SHARED neuvedeme, měli bychom se postarat o smazání takto získaného obrázku, tj. použít funkci DeleteIcon, resp. DeleteObject či DeleteCursor - v závislosti na tom, jaký typ grafického objektu jsme touto funkcí předtím načetli.

Nyní tedy máme položky naplněné, a zbývá si ukázat, jak je „uživatelsky kreslit“. Opět nejdříve trochu nutné teorie: Pokud je list-box uživatelsky kreslený, jeho vlastník, obvykle dialogové okno, na kterém je umístěn obdrží jednak zprávu WM_MEASUREITEM, jejíž lParam ukazuje na strukturu

typedef struct tagMEASUREITEMSTRUCT {
  UINT      CtlType;
  UINT      CtlID;
  UINT      itemID;
  UINT      itemWidth;
  UINT      itemHeight;
  ULONG_PTR itemData;
} MEASUREITEMSTRUCT;

a od aplikace musí před vrácením zprávy naplnit položku itemHeight požadovanou hodnotou výšky položky (v našem případě) list-boxu. Ještě poznámku k dokumentaci (MSDN), kde se tvrdí, že aplikace musí specifikovat také šířku položky (itemWidth). Myslím, že to není vždy nutné, a v ukázkovém příkladě „to chodí“ i bez určení délky, která je již naplněna podle šířky list-boxu. Jiná situace je u více sloupcového list-boxu, ale to už je mimo rámec tohoto článku. Dále dialog obdrží zprávu WM_DRAWITEM vždy, když některá položka list-boxu má být překreslena. S touto zprávou jsme se již seznámili v prvních 2 částech tohoto „miniseriálu“. Nyní opět k praxi. V dialogu si vytvoříme list-box, kterému pomocí editoru zdrojů nastavíme vlastnosti Owner Draw - Variable, dále zaškrtneme „Has strings“ označující, že položky list-boxu budou obsahovat taká texty. Vytvoříme si proměnnou dialogu typu naší třídy (CMyLstBox). V této třídě je třeba ještě vytvořit handlery zpráv WM_DRAWITEM a WM_MEASUREITEM. Zde se možná někteří pozastaví nad tím, co jsem říkal o zprávě WM_DRAWITEM (a podobně o WM_MEASUREITEM). Tuto zprávu obdrží vlastník list-boxu, tedy dialog, a nikoli list box. Avšak v systému tříd knihovny MFC je tato zpráva posílána navíc také příslušnému prvku (list-boxu). Toto zaslání zprávy však realizuje MFC, nikoli systém Windows. Pokud tedy budete psát aplikaci bez použití MFC, musíte tyto 2 zprávy zachytávat v proceduře okna vlastníka list-boxu. Podívejme se nyní již na jejich konkrétní implementaci v naší ukázce:

void CMyListBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
  lpMeasureItemStruct->itemHeight = m_IconHeight;
}

void CMyListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
  HICON iconData;
  RECT rect;
  TCHAR chText[120];
  ZeroMemory(chText, sizeof(chText));
  switch ( lpDrawItemStruct->itemAction )
  {
    case ODA_SELECT:
    case ODA_DRAWENTIRE:
      iconData = (HICON)SendMessage(LB_GETITEMDATA,
        lpDrawItemStruct->itemID, (LPARAM)0);
      SendMessage(LB_GETTEXT, lpDrawItemStruct->itemID, (LPARAM)chText);
      rect = lpDrawItemStruct->rcItem;
      if ( lpDrawItemStruct->itemState & ODS_SELECTED )
      {
        if ( m_SelectBrush == NULL )
          FillRect(lpDrawItemStruct->hDC, &rect,
            (HBRUSH)(COLOR_HIGHLIGHT + 1));
        else
          FillRect(lpDrawItemStruct->hDC, &rect, (HBRUSH)m_SelectBrush);
      }
      else
      {
        if ( m_BackBrush == NULL )
          FillRect(lpDrawItemStruct->hDC, &rect,
            (HBRUSH)(COLOR_WINDOW + 1));
        else
          FillRect(lpDrawItemStruct->hDC, &rect, (HBRUSH)m_BackBrush);
      }
      DrawIconEx(lpDrawItemStruct->hDC,
        lpDrawItemStruct->rcItem.left, lpDrawItemStruct->rcItem.top, iconData,
        m_IconWidth, m_IconHeight, 0, NULL, DI_NORMAL);

      rect.left += m_IconWidth + 6;
      SetBkMode(lpDrawItemStruct->hDC, TRANSPARENT);
      if ( lpDrawItemStruct->itemState & ODS_SELECTED )
        SetTextColor(lpDrawItemStruct->hDC, m_crTextSelected);
      else
        SetTextColor(lpDrawItemStruct->hDC, m_crText);
      DrawText(lpDrawItemStruct->hDC, chText, strlen(chText), &rect,
        DT_SINGLELINE | DT_VCENTER);
      break;
  }
}

Jak vidíte, v tomto případě opravdu stačí v MeasureItem specifikovat výšku položky.

Pokud jde o funkci DrawItem, testujeme nejprve prvek itemAction. Ten může nabývat jedné z hodnot:

ODA_DRAWENTIRE - je nutné překreslit celý prvek (v našem případě list-box)

ODA_SELECT - změnil se výběr položky v list-boxu

ODA_FOCUS - prvek (list-box) získal nebo ztratil klávesnicový fokus

V našem případě nemusíme nic překreslovat při změně fokusu, neboť vizuální podobu list-boxu se v tomto případě nijak neměníme. Jinak je to třeba u tlačítka, kde kreslíme známý čárkovaný obdélník, označující „fokusované“ tlačítko.

Další postup ve funkci DrawItem je již zřejmý. Vidíte, že v případě, že není zadán příslušný štětec pro výplň pozadí, použijme systémový štětec podle nastavení Windows. Tyto štětce, (ve formě handle (HBRUSH)) lze získat tímto zápisem:

HBRUSH hSysBrush = (HBRUSH)(COLOR_xxxxx + 1),

kde COLOR_xxxxx je konstanta příslušné barvy, jejich seznam lze najít třeba v popisu funkce GetSysColor v dokumentaci.

Zde si můžete stáhnou ukázkový projekt UserListBox.zip

Váš názor Další článek: Intel bude nadále pokračovat v cenové válce

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