Uživatelsky kreslené prvky Windows VIII – vlastní dialogové okno

Dnes si ukážeme, jak můžete vytvořit, přesněji řečeno sami nakreslit dialogové okno.
Okno dialogu bude mít vlastní region definovaný bitmapou, která bude tvořit celé jeho pozadí. Také tlačítka na dialogu budou tvořena bitmapou a budou mít oválný region. Vzhledem k tomu, že okno nebude mít titulkový pruh pro tažení na obrazovce, ukážeme si, jak realizovat možnost tažení okna za klientskou oblast, která bude v našem případě totožná s celým oknem.

Jak na to?

Ukázková aplikace je vytvořena jako MFC aplikace typu dialog. Do zdrojů importujeme 2 bitmapy, pro celá okna a pro tlačítka.

V dialogu napíšeme vlastní handler zprávy WM_PAINT, kde budeme kreslit celé okno jednoduchým vykreslením bitmapy (pomocí BitBlt), a handler zprávy WM_DRAWITEM, ve kterém budeme kreslit tlačítka.

Do třídy dialogu si přidáme následující členské proměnné:

Cfont      m_fontBtn;  // písmo tlačítek
Cbitmap    m_bmpBtn;  // bitmapa pro zobrazení tlačítka
Cbitmap    m_bmpWnd;  // bitmapa pro vykreslení okna
SIZE      m_sizeBtn;  // rozměry bitmapy pro tlačítka
SIZE      m_size;      // rozměry bitmapy okna

Dále si napíšeme funkci, která vytvoří na základě zadané bitmapy region tím způsobem, že bílá barva v bitmapě bude z regionu vyloučena. Tento region pak nastavíme zadanému oknu. Použijeme to pro okno dialogu a tlačítka <OK> a <Cancel>.

HRGN CUkazkaDlg::CreateWindowRegion(HWND hwnd, HBITMAP hbitmap)
{
  if ( hbitmap == NULL )
    return NULL;
  HRGN hrgnRes = NULL;
  HRGN hrgn;
  COLORREF crColor;
  COLORREF crPixel;
  INT ConsecPix;
  INT x, y;
  HDC hdc;
  BITMAP bitmap;
  hdc = CreateCompatibleDC(::GetDC(::GetDesktopWindow()));
  GetObject(hbitmap, sizeof(BITMAP), &bitmap);
  SelectObject(hdc, hbitmap);
  hrgnRes = CreateRectRgn(0, 0, bitmap.bmWidth, bitmap.bmHeight);
  for ( y = 0; y < bitmap.bmHeight; y++ )
  {
    crColor = GetPixel(hdc, 0, y);
    ConsecPix = 1;
    for ( x = 0; x < bitmap.bmWidth; x++ )
    {
      crPixel = GetPixel(hdc, x, y);
      if ( crColor == crPixel )
        ConsecPix++;
      else
      {
        if ( crColor == 0x00FFFFFF )
        {
          hrgn = CreateRectRgn(x - ConsecPix, y, x, y + 1);
          CombineRgn(hrgnRes, hrgnRes, hrgn, RGN_DIFF);
          DeleteObject(hrgn);
        }
        crColor = crPixel;
        ConsecPix = 1;
      }
    }
    if ( (crColor == 0x00FFFFFF) && (ConsecPix > 0) )
    {
      hrgn = CreateRectRgn(x-ConsecPix, y, x, y+1);
      CombineRgn(hrgnRes, hrgnRes, hrgn, RGN_DIFF);
      DeleteObject(hrgn);
    }
  }
  if ( hrgnRes != NULL )
  {
    ::SetWindowRgn(hwnd, hrgnRes, TRUE);
    SetWindowLongPtr(hwnd, GWL_STYLE,
      GetWindowLongPtr(hwnd, GWL_STYLE) | WS_CLIPSIBLINGS);
  }
  DeleteObject(hdc);
  return hrgnRes;
}

Při inicializaci dialogu si naloadujeme bitmapy, podle jejich zjištěných rozměrů upravíme rozměr okna dialogu a rozměr tlačítek a vytvoříme si font pro zobrazení textu tlačítka. Část funkce InitDialog vypadá takto:

  // TODO: Add extra initialization here
  m_bmpWnd.LoadBitmap(IDB_WINDOW);
  m_bmpBtn.LoadBitmap(IDB_BUTTON);
  BITMAP bitmap;
  GetObject(m_bmpWnd.m_hObject, sizeof(BITMAP), &bitmap);
  m_size.cx = bitmap.bmWidth;
  m_size.cy = bitmap.bmHeight;
  GetObject(m_bmpBtn.m_hObject, sizeof(BITMAP), &bitmap);
  m_sizeBtn.cx = bitmap.bmWidth;
  m_sizeBtn.cy = bitmap.bmHeight;
  SetWindowPos(NULL, 100, 100, m_size.cx, m_size.cy, SWP_NOZORDER);
  CreateWindowRegion(m_hWnd, m_bmpWnd);
  CreateWindowRegion(::GetDlgItem(m_hWnd, IDOK), m_bmpBtn);
  CreateWindowRegion(::GetDlgItem(m_hWnd, IDCANCEL), m_bmpBtn);
  GetDlgItem(IDOK)->SetWindowPos(NULL, 0, 0, m_sizeBtn.cx, m_sizeBtn.cy, SWP_NOZORDER | SWP_NOMOVE);
  GetDlgItem(IDCANCEL)->SetWindowPos(NULL, 0, 0, m_sizeBtn.cx, m_sizeBtn.cy, SWP_NOZORDER | SWP_NOMOVE);
  SetClassLong(::GetDlgItem(m_hWnd, IDCANCEL), GCL_HCURSOR,
    (LONG)LoadCursor(NULL, IDC_HAND));
  m_fontBtn.CreateFont(
    18,
    0,
    0,
    0,
    FW_BOLD,
    FALSE,
    FALSE,
    0,
    ANSI_CHARSET,
    OUT_DEFAULT_PRECIS,
    CLIP_DEFAULT_PRECIS,
    DEFAULT_QUALITY,
    DEFAULT_PITCH | FF_SWISS,
    "Arial"); 

Vykreslení plochy okna budeme provádět v handleru zprávy WM_PAINT. Vytvoříme si kompatibilní device-context, vybereme do něj načtenou bitmapu a pomocí funkce BitBlt ji vykreslíme. Zde je kód přidaný do handleru OnPaint:

void CUkazkaDlg::OnPaint()
{
  CRect rect;
  CPaintDC dc(this); // device context for painting
  if (IsIconic())
  {
  // ….
  // ….
  }
  else
  {
    CDC cdc;
    cdc.CreateCompatibleDC(&dc);
    cdc.SelectObject(m_bmpWnd);
    dc.BitBlt(0,0,
      m_size.cx, m_size.cy,
      &cdc,0,0, SRCCOPY);
  }
}

Bitmapu bychom mohli načítat vždy až ve funkci OnPaint. Opakované volání LoadBitmap by v případě větší true-color bitmapy nebylo asi to nejlepší. V případě opakovaného načítání by bylo lepší použít funkci

HANDLE LoadImage(
  HINSTANCE hinst,    // handle instance
  LPCTSTR lpszName,  // jméno nebo identifikátor
  UINT uType,        // typ grafiky
  int cxDesired,        // požadovaná délka
  int cyDesired,        // požadovaná výška
  UINT fuLoad        // parametry
);

Tato funkce není zapouzdřená v MFC, je třeba volat API, ale má tu výhodu, že lze specifikovat volbu LR_SHARED, což znamená, že načtený grafický objekt je při opakovaném loadování sdílený. Je ale třeba mít handle bitmapy jako globální proměnnou, neboť v opačném případě se systém postará o automatické zrušení objektu a budeme ve stejné situaci jako při použití LoadBitmap.

Dále si ukažme, jak budeme realizovat tlačítka. Opět použijeme naši funkci CreateWindowRegion pro vytvoření „neobdélníkového“ regionu tlačítek <OK> a <Cancel>, která pak budeme kreslit v handleru zprávy WM_DRAWITEM:

void CUkazkaDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
  if ( lpDrawItemStruct->CtlType != ODT_BUTTON )
    return;
  CDC cdc;
  TCHAR chText[10];
  ::GetWindowText(lpDrawItemStruct->hwndItem,
    chText, 10);
  cdc.CreateCompatibleDC(GetDC());
  cdc.SelectObject(m_bmpBtn);
  BitBlt(lpDrawItemStruct->hDC,
    0,0,
    m_size.cx, m_size.cy,
    cdc.m_hDC,0,0,SRCCOPY);
  SetBkMode(lpDrawItemStruct->hDC, TRANSPARENT);
  if ( lpDrawItemStruct->CtlID == IDOK )
    SetTextColor(lpDrawItemStruct->hDC, 0x0000A000);
  if ( lpDrawItemStruct->CtlID == IDCANCEL )
    SetTextColor(lpDrawItemStruct->hDC, 0x000000A0);
  SelectObject(lpDrawItemStruct->hDC, m_fontBtn.m_hObject);
  DrawText(lpDrawItemStruct->hDC, chText, -1, &lpDrawItemStruct->rcItem,
    DT_SINGLELINE | DT_VCENTER | DT_CENTER);
  CDialog::OnDrawItem(nIDCtl, lpDrawItemStruct);
}

Nyní zbývá realizovat možnost tažení (přesouvání) okna po obrazovce pomocí „chycení“ za libovolnou část okna. Při jakékoli „myší události“ je oknu poslána zpráva WM_NCHITTEST. Návratová hodnota volání DefWindowProc určuje polohu kurzoru v okamžiku události. Přesně řečeno polohu nikoli ve smyslu souřadnic, ale v které části okna se kurzor nalézá, tj. například v titulkovém pruhu, na minimalizačním tlačítku, některém z okrajů (lze rozlišit ve kterém, zda levém, pravém atd.) a podobně. V dokumentaci ke zprávě WM_NCHITTEST naleznete úplný seznam možných návratových hodnot a jejich význam.

Pro realizaci výše uvedeného tažení (za klientskou oblast) stačí odchytávat zprávu WM_NCHITTEST. V případě, že zjistíme, že kurzor je v klientské oblasti a současně je stlačeno levé tlačítko myši, musíme „nalhat“ systému, že je kurzor v titulkovém pruhu, tím, že v proceduře okna prostě vrátíme HTCAPTION (hodnota pro titulkový pruh). Celý handler vypadá takto:

LRESULT CUkazkaDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  LRESULT lResult;
  switch ( message )
  {
    case WM_NCHITTEST:
      lResult = DefWindowProc(message, wParam, lParam);
      if ( (lResult == HTCLIENT) && ( GetAsyncKeyState(MK_LBUTTON) < 0 ) )
        return HTCAPTION;
      break;
    case WM_ERASEBKGND:
      return 0;
      break;
  } 
  return CDialog::WindowProc(message, wParam, lParam);
}

Ke kódu procedury okna ještě doplním, že zadržení zprávy WM_ERASEBKGND, které zde vidíte, je z důvodu urychlení překreslování okna. Je to zbytečné a vedlo by to i k jistému zpomalení a popřípadě i k nežádoucímu blikání, nechat Windows „vymazat“ pozadí, přesněji řečeno vyplnit je definovaným štětcem, když poté celé okno kreslíme sami jeho vyplněním bitmapou.

Tento způsob realizace vytvoření „vlastního“ okna je pouze jeden z možných. Bylo by také možné definovat oknu štětec pozadí (člen hbrBackground struktury WNDCLASSEX), který lze nastavit i později pomocí funkce SetClassLongPtr (s parametrem GCLP_HBRBACKGROUND – viz dokumentace). Tento štětec by byl vytvořen z naší bitmapy pomocí funkce CreatePatternBrush. Zde by byl problém ve Windows 95, která podporují tyty štětce pouze z bitmapy maximálního rozměru 8x8 pixelů, což by nám samozřejmě nestačilo.

Podobnou tématikou je kreslení vlastního titulku a okrajů okna, tedy neklientské oblasti. To je však už trochu jiné téma (musíme pracovat s handlery „neklientských zpráv – tedy WM_NCPAINT atd.) a někdy příště se mu na těchto stránkách budeme věnovat.

Ukázkový příklad si můžete stáhnout zde VlastniDialogOkno.zip.

Diskuze (2) Další článek: Následník Windows XP Blackcomb odložen

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