Uživatelsky kreslené prvky Windows - ListView

Dnes si ukážeme, jak specificky kreslit prvek ListView.
Uživatelsky kreslené prvky Windows - ListView
Dnes si ukážeme, jak specificky kreslit prvek ListView. Konečný výsledek jednoduché ukázky vidíte na následujícím obrázku:

Klepněte pro větší obrázek

Trocha teorie na úvod

Jak už jsme poznali v článku věnovaném track-baru, ListView patří mezi ty prvky, které posílají svému vlastníku prostřednictví notifikační zprávy (WM_NOTIFY) zprávu NM_CUSTOMDRAW, kterou upozorňuje na „kreslící operace“. Vlastník (obvykle dialogové okno) má pak možnost některé (nebo samozřejmě všechny) tyto „kreslící operace“ provádět ve vlastní režii a jiné nechat zpracovat systém, tedy standardně. V parametru lParam této upozorňující zprávy je pak adresa příslušné struktury (o jakou strukturu jde konkrétně, závisí na druhu daného prvku). V případě ListView jde o strukturu:

typedef struct tagNMLVCUSTOMDRAW {
  NMCUSTOMDRAW nmcd;
  COLORREF clrText;
  COLORREF clrTextBk;
#if (_WIN32_IE >= 0x0400)
  int iSubItem;
#endif
} NMLVCUSTOMDRAW, *LPNMLVCUSTOMDRAW;

kde prvek nmcmd obsahuje strukturu

typedef struct tagNMCUSTOMDRAWINFO {
  NMHDR    hdr;
  DWORD    dwDrawStage;
  HDC      hdc;
  RECT      rc;
  DWORD    dwItemSpec;
  UINT      uItemState;
  LPARAM    lItemlParam;
} NMCUSTOMDRAW, FAR * LPNMCUSTOMDRAW;

Důležitou věcí je návratová hodnota zprávy NM_CUSTOMDRAW. Touto návratovou hodnotou v handleru této zprávy musíme systému určit, zda má provést výchozí kreslení, nebo zda nás má informovat o potřebě provést kreslení a „nechat to na aplikaci“. Výběr možných návratových hodnot závisí na hodnotě prvku dwDrawStage. Pokud dwDrawStage je roven CDDS_PREPAINT, máme „na výběr“ z následujících možností:

  • CDRF_DODEFAULT –CDRF_NOTIFYITEMDRAW – prvek bude oznamovat svému vlastníkovi všechny kreslící operace, tj. bude posílat zprávy NM_CUSTOMDRAW před a po kreslení položek.
  • CDRF_NOTIFYITEMDRAW – prvek oznámí svému vlastníku, když má být položka vykreslena, zprávu NM_CUSTOMDRAW bude posílat před i po vymazání položky.
  • CDRF_NOTIFYITEMERASE – prvek oznámí svému vlastníku, když má být položka vymazána, zprávu NM_CUSTOMDRAW bude posílat před i po vymazání položky.
  • CDRF_NOTIFYPOSTERASE – prvek bude informovat vlastníka po vymazání položky.
  • CDRF_NOTIFYPOSTPAINT – prvek bude informovat vlastníka po vykreslení položky.
Pokud dwDrawStage je rovno CDDS_ITEMPREPAINT, návratová hodnota může být:
  • CDRF_NEWFONT – aplikace chce specifikovat písmo položky
  • CDRF_NOTIFYSUBITEMDRAW – Aplikace obdrží zprávu NM_CUSTOMDRAW s parametrem dwDrawStage nastaveným na CDDS_ITEMPREPAINT | CDDS_SUBITEM před tím, než je vykreslena každá položka. V aplikaci pak můžeme určit písmo a barvu pro každou položku zvlášť nebo vrátit CDRF_DODEFAULT, pokud chceme „defaultní“ zpracování.
  • CDRF_SKIPDEFAULT – aplikace chce kreslit prvek sama, žádné „defaultní“ kreslení se nebude provádět.

Jak na to prakticky?

Ukázková aplikace je vytvořena jako MFC aplikaci typu dialog. Na dialog umístíme prvek nazvaný v editoru zdrojů „List Control“. Do třídy dialogu přidáme členskou proměnnou

HIMAGELIST m_hImgList;

Bude to handle image-listu, který přiřadíme jako „zdroj obrázků“ našemu ListView.

Zde poznamenám, že by bylo samozřejmě pro ty, kdo znají MFC, „jednodušší a méně psavé“ obalit si jak ListView, tak image-list příslušnou třídou MFC. Konkrétně by se jednalo o třídy CListCtrl (pozor – nikoliv ClistView, což je třída typu CView v aplikacích používajících techniku „dokument-pohled“) a CImageList. Tyto třídy mají příslušné členské funkce, zapouzdřující většinu z toho, co je třeba „v čistém API“ realizovat pro někoho možná ne příliš oblíbeným posíláním zpráv. Já v tomto případě zůstanu u „čistého API“. Někdo bude třeba chtít uvedený kód použít v jiném vývojovém prostředí, přesněji řečeno tam, kde nemá nebo nechce mít možnost používat MFC. A takto napsaný kód pak bude „chodit všude“ (samozřejmě pouze na platformě Win32).

Vytvoříme si tedy image-list o rozměrech 64 x 64 pixelů, tomuto rozměru se pak „přizpůsobí“ rozměry položky našeho ListView.

m_hImgList = ImageList_Create(64, 64, ILC_COLOR | ILC_MASK, 4, 4);

Když se podíváte do dokumentace, zjistíte, že máme k dispozici rozsáhlou nabídku zpráv (začínajících na LVM_), kterými můžeme nastavovat různé vlastnosti prvku ListView. V ukázkovém příkladě jsou 3 z nich. Již zmíněné přiřazení image-listu provedeme pomocí zprávy LVM_SETIMAGELIST

SendDlgItemMessage(IDC_LIST, LVM_SETIMAGELIST,
  LVSIL_NORMAL, (LPARAM)m_hImgList);

Parametr wParam určuje typ image-listu, v našem případě použijeme hodnotu LVSIL_NORMAL, tedy velké ikony.

Dále nastavíme barvu pozadí ListView pomocí zprávy LVM_SETBKCOLOR, jejíž lParam určuje hodnotu barvy typu COLORREF.

SendDlgItemMessage(IDC_LIST, LVM_SETBKCOLOR, 0, (LPARAM)0x00A0FFFF);

Ještě nastavíme rozteč položek pomocí zprávy LVM_SETICONSPACING, v jejímž parametru lParam jsou hodnoty určující rozteč v obou souřadnicových osách. Hodnoty jsou uloženy jako dolní a horní WORD, pro vytvoření lParam použijeme makro MAKELONG:

SendDlgItemMessage(IDC_LIST, LVM_SETICONSPACING,
  0, (LPARAM)MAKELONG(95, 95));

Nyní je samozřejmě třeba naplnit ListView nějakými položkami. V této ukázce použijeme 5 systémových ikon, tedy těch, které můžeme získat pomocí funkce LoadIcon, kde parametr hInstance bude NULL a lpIconName bude některá z předdefinovaných hodnot. Získáme tak například známé ikony „chyba“, „otázka“ apod.

Pro přidání položky do ListView se používá zpráva LVM_INSERTITEM, jejíž parametr lParam je adresa struktury

typedef struct _LVITEM {
  UINT      mask;
  int        iItem;
  int        iSubItem;
  UINT      state;
  UINT      stateMask;
  LPTSTR    pszText;
  int        cchTextMax;
  int        iImage;
  LPARAM    lParam;
#if (_WIN32_IE >= 0x0300)
  int        iIndent;
#endif
} LVITEM, FAR *LPLVITEM;

Pokud chceme, jako v našem jednoduchém příkladě vložit text a použít obrázek z připojeného image-listu, stačí nastavit položky

  • iImage- index obrázku (ikony) v připojeném image-listu
  • pszText – text zobrazovaný u položky
  • iItem – index, který bude mít přidaná položka (počítaný od nuly, jak je obvyklé v C i WinAPI)
  • mask – zde je třeba uvést pomocí příznaků, které ze členů struktury jsou „významné“ – v našem případě uvedeme hodnotu LVIF_IMAGE | LVIF_TEXT znamenající, že jsme zadali text a index ikony.
Vše provedeme ve funkci InitDialog. Index ikony nám vrátí funkce

int ImageList_AddIcon(
  HIMAGELIST himl,
  HICON hicon);

kterou přidáme do připojeného image-listu požadovanou ikonu. Celý kód pro přidání zmíněných systémových ikon vypadá takto:

LVITEM lvi;
ZeroMemory(&lvi, sizeof(LVITEM));
lvi.mask = LVIF_IMAGE | LVIF_TEXT;
lvi.iImage = ImageList_AddIcon(m_hImgList, LoadIcon(NULL, IDI_HAND));
lvi.pszText = "Chyba";
lvi.iItem = lvi.iImage;
SendDlgItemMessage(IDC_LIST, LVM_INSERTITEM, 0,(LPARAM)&lvi);
lvi.iImage = ImageList_AddIcon(m_hImgList, LoadIcon(NULL, IDI_WARNING));
lvi.pszText = "!!!!";
lvi.iItem = lvi.iImage;
SendDlgItemMessage(IDC_LIST, LVM_INSERTITEM, 0,(LPARAM)&lvi);
lvi.iImage = ImageList_AddIcon(m_hImgList, LoadIcon(NULL, IDI_WINLOGO));
lvi.pszText = "Logo";
lvi.iItem = lvi.iImage;
SendDlgItemMessage(IDC_LIST, LVM_INSERTITEM, 0,(LPARAM)&lvi);
lvi.iImage = ImageList_AddIcon(m_hImgList, LoadIcon(NULL, IDI_QUESTION));
lvi.pszText = "Otázka";
lvi.iItem = lvi.iImage;
SendDlgItemMessage(IDC_LIST, LVM_INSERTITEM, 0,(LPARAM)&lvi);
lvi.iImage = ImageList_AddIcon(m_hImgList, LoadIcon(NULL, IDI_INFORMATION));
lvi.pszText = "Info";
lvi.iItem = lvi.iImage;
SendDlgItemMessage(IDC_LIST, LVM_INSERTITEM, 0,(LPARAM)&lvi);

Takto máme ListView naplněn položkami, a nyní je třeba zajistit jeho „uživatelské kreslení“. Dialog jako vlastník ListView dostává od ListView zprávu NM_CUSTOMDRAW prostřednictvím notifikační zprávy WM_NOTIFY, kterou nás ListView informuje o kreslících operacích. Tento mechanismus jsem popsal v úvodní teoretické části, takže nyní si ukažme praktickou realizaci. V handleru zprávy WM_NOTIFY (což provedeme přímo v proceduře okna – WindowProc) budeme po otestování, zda se jedná o náš ListView, volat vlastní funkci MyListDraw. Kód vypadá takto:

LRESULT CUkazkaDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  LPNMHDR lpnmhdr;
  switch ( message )
  {
    case WM_NOTIFY:
      lpnmhdr = (LPNMHDR)lParam;
      if ( lpnmhdr->idFrom == IDC_LIST )
        if ( lpnmhdr->code == NM_CUSTOMDRAW )
          return MyListDraw((LPNMLVCUSTOMDRAW)lParam);
      break;
  }
  return CDialog::WindowProc(message, wParam, lParam);
}

LRESULT CUkazkaDlg::MyListDraw(LPNMLVCUSTOMDRAW lpnmcd)
{
  RECT rect;
  LVITEM lvi;
  TCHAR chText[30];
  HBRUSH hbrush;
  LOGFONT lf;
  memset(&lf, 0, sizeof(LOGFONT));
  lf.lfHeight = 26;
  lf.lfWeight = FW_BOLD;
  strcpy(lf.lfFaceName, "Times New Roman");       
  HFONT hfont;
  if ( lpnmcd->nmcd.dwDrawStage == CDDS_PREPAINT )
    return CDRF_NOTIFYITEMDRAW;
  if ( lpnmcd->nmcd.dwDrawStage == CDDS_ITEMPREPAINT )
  {
    if ( lpnmcd->nmcd.uItemState & CDIS_FOCUS )
    {
      hbrush = CreatePatternBrush(LoadBitmap(
        AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BACKSEL)));
      lf.lfItalic = TRUE;
      lf.lfUnderline = TRUE;
      hfont = CreateFontIndirect(&lf);
      SetTextColor(lpnmcd->nmcd.hdc, 0x0080FFFF);
    }
    else
    {
      hbrush = CreatePatternBrush(LoadBitmap(
        AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BACK)));
      hfont = CreateFontIndirect(&lf);
      SetTextColor(lpnmcd->nmcd.hdc, 0x00800000);
    }
    rect.left = LVIR_BOUNDS;
    SendDlgItemMessage(IDC_LIST, LVM_GETITEMRECT,
      lpnmcd->nmcd.dwItemSpec, (LPARAM)&rect);
    SelectObject(lpnmcd->nmcd.hdc, hbrush);
    SelectObject(lpnmcd->nmcd.hdc, hfont);
    RoundRect(lpnmcd->nmcd.hdc,
      rect.left, rect.top,
      rect.right, rect.bottom, 20,20);
    DrawIcon(lpnmcd->nmcd.hdc,
      rect.left + ((rect.right - rect.left - GetSystemMetrics(SM_CXICON))/2),
      rect.top + 2,
      ImageList_GetIcon(m_hImgList, lpnmcd->nmcd.dwItemSpec, ILD_NORMAL));
    rect.top += GetSystemMetrics(SM_CYICON);
    ZeroMemory(&lvi, sizeof(LVITEM));
    lvi.pszText = chText;
    lvi.cchTextMax = 30;
    SendDlgItemMessage(IDC_LIST, LVM_GETITEMTEXT,
      lpnmcd->nmcd.dwItemSpec, (LPARAM)&lvi);
    DrawText(lpnmcd->nmcd.hdc, chText, -1, &rect,
      DT_SINGLELINE | DT_VCENTER | DT_CENTER);
    DeleteObject(hbrush);
    DeleteObject(hfont);
    return CDRF_SKIPDEFAULT;
  }
  return 0;
}

Důležité je v případě že prvek dwDrawStage má hodnotu CDDS_PREPAINT vrátit CDRF_NOTIFYITEMDRAW, čímž právě řekneme systému, že chceme dostávat notifikační kreslící zprávy, na které pak reagujeme vlastním algoritmem kreslení položky. Toto kreslení realizujeme jako reakci na tuto zprávu v případě, že dwDrawStage má hodnotu CDDS_ITEMPREPAINT, jak je vidět ve výše uvedeném výpisu kódu. Které informace nás zajímají? Zda má právě kreslená položka fokus zjistíme pomocí prvku uItemState struktury NMCUSTOMDRAW. Položka má fokus, pokud uItemState obsahuje hodnotu CDIS_FOCUS. V ukázkovém příkladě pak reagujeme nastavením různého pozadí, tvořeného štětcem vytvořeným z bitmapy, a různou barvou a stylem písma. Kontext zařízení (device context) máme v prvku hdc struktury NMCUSTOMDRAW. Dále budeme potřebovat obdélník (RECT) právě kreslené položky. K jeho získání máme k dispozici zprávu LVM_GETITEMRECT, jejímž parametrem wParam je index požadované položky a jako lParam uvedeme adresu struktury typu RECT, kterou musíme ovšem „předvyplnit“ tím způsobem, že do jejího prvku left uvedeme jednu z definovaných hodnot, které specifikují o jakou část položky se nám jedná konkrétně může nabývat hodnot:

  • LVIR_BOUNDS – vrátí ohraničující obdélník celé položky, tj. ikony i textového pole.
  • LVIR_ICON – vrátí oblast ikony (velké nebo malé – podle aktuálního zobrazení). LVIR_LABEL – vrátí oblast textu položky.
  • LVIR_SELECTBOUNDS – vrátí spojení obdélníků získaných parametry LVIR_ICON a LVIR_LABEL, ale s vyloučením sloupců ve zobrazení „podrobnosti“ (report).
Takto tedy máme vše co potřebujeme k vykreslení položky, a zbývající část kódu již je pouze o použití příslušných GDI funkcí.

Ukázkový příklad si můžete stáhnout zde: ListView.zip (velikost 136 kB)

Témata článku: Software, Windows, Programování, Lvi, Message, Hicon

11 komentářů

Nejnovější komentáře

  • Franta 13. 8. 2001 10:21:23
    Moc tomu nerozumim, muze mi to nekdo vysvetlit???
  • TomasF 10. 8. 2001 10:17:50

    Tak si to radeji stahnete a neunavujte nikoho takovyma blbostma....

  • TomasF 10. 8. 2001 10:16:31

    Tak si to radeji stahnete a neunavujte nikoho takovyma blbostma....

Určitě si přečtěte

Pojďme programovat elektroniku: Postavíme si titěrnou Wi-Fi meteostanici s lepším teploměrem než Netatmo

Pojďme programovat elektroniku: Postavíme si titěrnou Wi-Fi meteostanici s lepším teploměrem než Netatmo

** Dnes se podíváme na maličkou Wi-Fi destičku Wemos D1 mini ** A připojíme k ní barometrický a teplotní shield ** Poběží na ní web a nabídne i JSON API

18.  6.  2017 | Jakub Čížek | 28

Jak vybrat monitor k počítači: nenechte se zlákat nepodstatnými parametry

Jak vybrat monitor k počítači: nenechte se zlákat nepodstatnými parametry

** Na jaké parametry se zaměřit a kde vás výrobci chtějí nachytat ** Monitory se stále více specifikují pro konkrétní určení ** Náročný hráč nebo profesionální grafik mají různé požadavky

20.  6.  2017 | Tomáš Holčík | 31

Dlouhodobý test HTC Vive: co vám recenze o virtuální realitě neřeknou

Dlouhodobý test HTC Vive: co vám recenze o virtuální realitě neřeknou

** Ani hry se sebelepší grafikou vás nevtáhnou tolik, jako ve virtuální realitě ** Pro sledování filmů není VR ani zdaleka ideální ** I první generace je skvělá, stále však působí jako prototyp

20.  6.  2017 | Stanislav Janů | 22

Jak unikají informace o nových iPhonech? Třeba podprsenkami čínských pracovnic

Jak unikají informace o nových iPhonech? Třeba podprsenkami čínských pracovnic

** Na černém trhu mohou zaměstnanci továren za kradené součástky inkasovat částku ve výši ročního platu ** Velké množství informací je vyneseno i z centrály Applu ** Díly jsou pašovány v botách, podprsenkách i odpadem

21.  6.  2017 | Stanislav Janů | 22


Aktuální číslo časopisu Computer

Bojujeme proti Fake News

Dva velké testy: fotoaparáty a NASy

Co musíte vědět o změně evropského roamingu

Radíme s výběrem základní desky