Uživatelsky kreslené prvky Windows VII - Tab Control

Dnes si ukážeme, jak vytvořit, přesněji řečeno upravit podle svého uživatelsky kreslený prvek tab-control, který budeme kreslit s vlastním pozadím, tvořeným bitmapovým rastrem, jiným pozadím budeme vyplňovat aktuálně vybranou „záložku“.

Dnes si ukážeme, jak vytvořit, přesněji řečeno upravit podle svého uživatelsky kreslený prvek tab-control, který budeme kreslit s vlastním pozadím, tvořeným bitmapovým rastrem, jiným pozadím budeme vyplňovat aktuálně vybranou „záložku“. Dále budeme umět na každé záložce zobrazit vlastní obrázek, v tomto konkrétním příkladě z ikony uložené v image-listu. Vytvoříme si také vlastní font pro zobrazení textu záložek (s ukázkou možnosti sklonu písma u vybrané záložky), jak vidíte na následujícím obrázku:

Jak na to?

Ukázková aplikace je vytvořena jako MFC aplikace typu dialog. Do zdrojů je importováno 5 bitmap, které použijeme pro pozadí tab-control a pozadí vybrané záložky, dále bitmapy, které budeme přepínat podle aktuálně vybrané záložky. Na dialog tedy umístíme prvek „Tab Control“ a dále prvek „Picture“, který bude zobrazovat vždy jednu ze 3 bitmap.

Nastavení vlastnosti owner-draw

Abychom mohli tab-control kreslit uživatelsky, nastavíme mu v editoru zdrojů v „Tab Control Properties“ na záložce „Styles“ vlastnost „Owner draw fixed“.

Vše realizujeme v kódu třídy dialogu

Podobně jako u vícesloupcového list-boxu není v MFC (alespoň v mém případě – používám Visual C++ 6 Professional, Service Pack 4) zpráva WM_DRAWITEM korektně posílána třídě CTabCtrl (resp. jejímu potomku, který bychom si odvodili). Bylo by možné z dialogu tuto zprávu třídě posílat „ručně“, ale jednoduší a efektivnější bude zpracovat vše v kódu třídy dialogu, aniž bychom vůbec vytvářeli pro Tab Control členskou proměnnou.

Vytvořme si tedy následující členské proměnné třídy dialogu:

// bitmapy zobrazované na jednotlivých „stránkách“:
HBITMAP m_hb0;
HBITMAP m_hb1;
HBITMAP m_hb2;
// výška záložky:
int m_tabHeight;
// struktura pro vytvoření písma záložky:
LOGFONT m_logfont;
// štětce pro výplň pozadí a pozadí vybrané záložky:
HBRUSH m_SelectedTab;
HBRUSH m_BackTab;
// obrázky zobrazované na záložkách:
CImageList m_ImageList;

Inicializaci provedeme ve funkci InitDialog. Ukažme si nyní postupně části jejího kódu:

Nejdříve běžným způsobem načteme ze zdrojů bitmapy, první z nich hned zobrazíme pomocí prvku „Picture“, naplníme si výchozí vlastnosti struktury m_logfont vlastnostmi získanými z jednoho ze „systémových“ písem, které zvětšíme a nastavíme jako tučné, a nakonec vytvoříme dva štětce pro uvedené výplně pozadí:

m_hb0 = LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_HONZA));
m_hb1 = LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_FOTO1));
m_hb2 = LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_FOTO2));
m_Picture.SetBitmap(m_hb0);
m_tabHeight = 48;
::GetObject(GetStockObject(DEFAULT_GUI_FONT), sizeof(LOGFONT),&m_logfont);
m_logfont.lfHeight =20;
m_logfont.lfWeight = FW_BOLD;
m_BackTab = CreatePatternBrush(
  LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BITMAP1)));
m_SelectedTab = CreatePatternBrush(
  LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BITMAP2)));

Dalším krokem je vytvoření image-listu:

m_ImageList.Create(32,32,ILC_COLOR, 0, 1);
m_ImageList.Add(AfxGetApp()->LoadIcon(IDR_MAINFRAME));
m_ImageList.Add(LoadIcon(NULL, IDI_ERROR));
m_ImageList.Add(LoadIcon(NULL, IDI_INFORMATION));

Nakonec přidáme do Tab Controlu 3 záložky pomocí zprávy TCM_INSERTITEM, nastavíme zvolenou výšku záložky a podle celkové šířky Tab Controlu nastavíme délku záložek tak, aby jejich celková délka byla „zarovnaná“ k délce Tab Controlu; nakonec nastavíme pozadí Tab Controlu jako vlastnost „background“ třídy jeho okna.

TCITEM ti;
ti.mask = TCIF_TEXT;
ti.pszText = "První";
SendDlgItemMessage(IDC_TAB, TCM_INSERTITEM, 0, (LPARAM)&ti);
ti.pszText = "Druhý";
SendDlgItemMessage(IDC_TAB, TCM_INSERTITEM, 1, (LPARAM)&ti);
ti.pszText = "Třetí";
SendDlgItemMessage(IDC_TAB, TCM_INSERTITEM, 2, (LPARAM)&ti);
RECT rect;
GetDlgItem(IDC_TAB)->GetWindowRect(&rect);
SendDlgItemMessage(IDC_TAB, TCM_SETITEMSIZE, 0,
  (LPARAM)MAKELPARAM(m_tabHeight, m_tabHeight));
SendDlgItemMessage(IDC_TAB, TCM_SETMINTABWIDTH, 0,
  (LPARAM)(rect.right - rect.left)/3 - 1);
SetClassLong(::GetDlgItem(m_hWnd, IDC_TAB), GCL_HBRBACKGROUND,
  (LONG)m_BackTab);

Vykreslení záložek Tab Controlu realizujeme v handleru zprávy WM_DRAWITEM, kterou obdrží vlastník uživatelsky kresleného Tab Controlu, tedy náš dialog. V kódu handleru vidíte, jak nastavit sklon písma pomocí prvků lfOrientation a lfEscapement struktury LOGFONT, a dále jak získat text přiřazený příslušné záložce pomocí zprávy TCM_GETITEM. K dalším částem kódu handleru myslím netřeba dalšího komentáře:

void CUkazkaDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
  HFONT hfont;
  if ( lpDrawItemStruct->itemState & ODS_SELECTED )
  {
    FillRect(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem,
      (HBRUSH)m_SelectedTab);
    SetTextColor(lpDrawItemStruct->hDC, 0x00A00000);
    // vybraná položka bude mít sklon písma
    m_logfont.lfOrientation = 350;
    m_logfont.lfEscapement = 350;
  }
  else
  {
    FillRect(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem,
      (HBRUSH)m_BackTab);
    SetTextColor(lpDrawItemStruct->hDC, 0x00A0FFFF);
    m_logfont.lfOrientation = 0;
    m_logfont.lfEscapement = 0;
  }
  hfont = CreateFontIndirect(&m_logfont);
  SelectObject(lpDrawItemStruct->hDC, hfont);
  SetBkMode(lpDrawItemStruct->hDC, TRANSPARENT);
  TCITEM ti;
  TCHAR chText[20];
  ZeroMemory(&ti, sizeof(TCITEM));
  ti.mask = TCIF_TEXT;
  ti.cchTextMax = 20;
  ti.pszText = chText;
  // získáme text položky
  SendDlgItemMessage(IDC_TAB, TCM_GETITEM,
    lpDrawItemStruct->itemID,
    (LPARAM)(LPTCITEM)&ti);
  // nakreslíme obrázek
  ImageList_Draw(m_ImageList.m_hImageList,
    lpDrawItemStruct->itemID,
    lpDrawItemStruct->hDC,
    lpDrawItemStruct->rcItem.left,
    (m_tabHeight - 32)/2,
    ILD_TRANSPARENT);
  // výpis textu
  if ( lpDrawItemStruct->itemState & ODS_SELECTED )
    TextOut(lpDrawItemStruct->hDC,
      lpDrawItemStruct->rcItem.left + 34,
      lpDrawItemStruct->rcItem.bottom - 20,
      chText, strlen(chText));
  else
    DrawText(lpDrawItemStruct->hDC,
      ti.pszText, -1,
      &lpDrawItemStruct->rcItem,
      DT_SINGLELINE | DT_CENTER | DT_VCENTER);
  DeleteObject(hfont);
}

Nyní nám zbývá jediné: zachytit změnu výběru Tab Controlu a podle toho zobrazit příslušnou bitmapu, což je v ukázkovém příkladě realizováno velice „primitivním“ způsobem, neboť tématem článku je uživatelsky kreslený Tab Control a nikoli „pokročilé“ zobrazování bitmapy (jako centrování, zoom apod.). Při změně výběru tedy Tab Control posílá svému vlastníku notifikaci TCN_SELCHANGE ve zprávě WM_NOTIFY. Její zpracování vidíte v kódu procedury okna dialogu:

LRESULT CUkazkaDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  LPNMHDR lpnmhdr = (LPNMHDR)lParam;
  switch ( message )
  {
    case WM_NOTIFY:
      if ( (wParam == IDC_TAB) && (lpnmhdr->code == TCN_SELCHANGE))
      {
        switch  ( SendDlgItemMessage(IDC_TAB, TCM_GETCURSEL, 0, 0) )
        {
          case 0:
            m_Picture.SetBitmap(m_hb0);
            break;
          case 1:
            m_Picture.SetBitmap(m_hb1);
            break;
          case 2:
            m_Picture.SetBitmap(m_hb2);
            break;
        }
      }
      break;
  } 
  return CDialog::WindowProc(message, wParam, lParam);
}

Na závěr

Uvedený příklad je velice jednoduchý. Cílem je ukázat princip a vlastní kreativní činnost je pak věcí programátora. Ukázkový příklad si můžete stáhnout zde TabControl.zip (668 kB)
Váš názor Další článek: Oprava článku o trendech v připojení k internetu

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