Pár tipů pro okna v C++

V tomto článku si ukážeme pár snad užitečných tipů pro práci s okny. Ukážeme si jak táhnout okno za klientskou oblast, zpracovávat a modifikovat systémové zprávy (zavření, minimalizace,..), omezit minimální a maximální velikost okna, nechat okno vždy na vrchu, nastavit polohu a rozměry maximalizovaného okna a zobrazit okno přes celou obrazovku.
Jak na to

Vytvořme si aplikaci s použitím MFC. Odstraníme třídy dokumentu a pohledu. Do projektu jsem dále přidal dvě třídy odvozené od CWnd, nazvané CMinMaxWnd a CMyWindow, ke kterým se dostaneme později. Ukažme si nejprve pár věcí přímo s hlavním oknem:

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

Tažení okna za klientskou oblast

Pokud chceme umožnit uživateli přesouvat okno po obrazovce tažením za jeho klientskou oblast, což je užitečné zejména u okna bez titulkového pruhu, můžeme použít jednoduchý trik využívající změny návratové hodnoty zprávy. Když se podíváte do dokumentace na popis zprávy WM_NCHITTEST, zjistíte, že jde o zprávu, která je posílána při všech "myších" zprávách. Její návratová hodnota nám určuje, ve které oblasti okna k příslušné zprávě došlo. Tedy například zda jde o titulkový pruh, okraje, lze i rozlišit, o který okraj okna se jedná, nebo zda jde o jednu ze systémových "ikonek" (maximalizace, zavření...). Výčet je opravdu bohatý a naleznete ho v dokumentaci. V našem případě budeme zachytávat zprávy v klientské oblasti, tedy kdy návratová hodnota zprávy je HTCLIENT. V tom případě v proceduře okna "ručně" vrátíme hodnotu HTCAPTION, tedy nalžeme systému, že jsme v titulkovém pruhu. Pro větší "čistotu" ještě otestujeme, zda je současně stačeno levé tlačítko myši, a budeme se takto chovat jen v tomto případě. Část procedury okna (přepsané metody WindowProc) vypadá takto:

LRESULT CMainFrame::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;
  }
  return CFrameWnd::WindowProc(message, wParam, lParam);
}

Jak vidíte, pro zjištění návratové hodnoty zavoláme funkci DefWindowProc. Ta provede výchozí zpracování zprávy a vrátí nám její návratovou hodnotu. My pak prostě v proceduře okna vrátíme vlastní hodnotu HTCAPTION a systém "nic nepozná". Pro zjištění, zda je v daném okamžiku stlačena některá klávesa či tlačítko myši, používáme funkci GetAsyncKeyState, její podrobnější popis naleznete v dokumentaci.

Jak si pohrát se systémovými zprávami?

Systémovými zprávami zde mám na mysli zprávu WM_SYSCOMMAND. Nejprve si ukažme, jak zachytit a popřípadě zadržet požadavek na zavření okna: V parametru wParam zprávy WM_SYSCOMMAND je hodnota určující, k jaké systémové zprávě došlo (jejich výčet naleznete v dokumentaci). V případě požadavku na zavření okna má wParam hodnotu SC_CLOSE. Chceme-li tedy ovládat tento prvek, stačí přidat svůj kód jako v tomto příkladě (Pozn.: V dalších ukázkách jde vždy o část „switch“ příkazu v proceduře okna, budu tedy uvádět ve výpisu pouze příslušný „case“.):

case WM_SYSCOMMAND:
  switch ( wParam )
  {
    case SC_CLOSE:
      if ( MessageBox("Opravdu zavřít okno?", "Potvrdit", MB_YESNO | MB_ICONQUESTION) != IDYES )
        return 0;
    break;
  }
  break;

Ale pozor! Pokud je někde v kódu aplikace volána funkce DestroyWindow, už je pozdě, neboť zprávu WM_DESTROY dostane okno až poté, co je již odstraněno z obrazovky. V případě MFC aplikace proto ještě musíme vytvořit handler identifikátoru ID_APP_EXIT a reagovat v něm:

void CMainFrame::OnAppExit()
{
  if ( MessageBox("Opravdu ukončit aplikaci?", "Potvrdit", MB_YESNO | MB_ICONQUESTION) == IDYES )
    DestroyWindow();
}

Nyní si ještě ukažme, jak lze například při stisknutí minimalizačního "tlačítka" místo minimalizování okno třeba maximalizovat.

case WM_SYSCOMMAND:
  switch ( wParam )
  {
    case SC_MINIMIZE:
      MessageBox("Pozor změna !", "Demo", MB_ICONEXCLAMATION);
      SendMessage(WM_SYSCOMMAND, SC_MAXIMIZE, lParam);
      return 0;
  }
  break;

Jak vidíte, nejde o nic jiného, než tuto zprávu zadržet a místo ní poslat zprávu se změněným parametrem wParam.

Podobného efektu můžeme dosáhnout ještě jiným způsobem. Ukažme si ho na opačném případě – při kliknutí na maximalizační tlačítko okno minimalizujeme. Opět použijeme již zmíněnou zprávu WM_NCHITTEST; myslím, že po předchozím výkladu již nepotřebuje další komentář:

case WM_NCHITTEST:
  lResult = DefWindowProc(message, wParam, lParam);
  if ( (lResult == HTMAXBUTTON)  && ( GetAsyncKeyState(MK_LBUTTON) < 0 ) )
  {
    MessageBox("To okno je dnes nějaké divné ....", "Hmmmm", MB_ICONEXCLAMATION);
    ShowWindow(SW_MINIMIZE);
    return 0;
  }
  break;

Jako poslední "legrácku" s tímto oknem si například ukažme, jak při dvojkliku v titulku, který normálně okno maximalizuje (popř. vrací zpět z maximalizace), okno zavřeme:

case WM_NCLBUTTONDBLCLK:
  MessageBox("Už toho mám opravdu dost! Bye!", "Grrrr", MB_ICONEXCLAMATION);
  DestroyWindow();
  return 0;

Pozice vždy na vrchu

Na tomto okně si ještě ukažme, jak oknu nastavit pozici "vždy nahoře" a jak je opět vrátit do "normálního režimu". Použijeme funkci SetWindowPos, v níž bude významný parametr hWndInsertAfter. Následující funkce nastaví okno do pozice "vždy nahoře":

void CMainFrame::OnStayTop()
{
  ::SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}

a takto ho zase přepneme do "normálního chování":

void CMainFrame::OnStayNormal()
{
::SetWindowPos(m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}

Omezení velikosti okna

Nechme již na pokoji toto hlavní okno aplikace a ukažme si na jiném okně, jak omezit minimální a maximální "roztažitelnou" velikost okna a jak nastavit pozici a velikost maximalizovaného okna. V projektu máme pro tento účel okno CMinMaxWnd. Zmíněné úpravy provedeme v handleru zprávy WM_GETMINMAXINFO. Parametr lParam této zprávy ukazuje na strukturu

typedef struct tagMINMAXINFO {
  POINT ptReserved;
  POINT ptMaxSize;
  POINT ptMaxPosition;
  POINT ptMinTrackSize;
  POINT ptMaxTrackSize;
} MINMAXINFO;

Prvek ptMinTrackSize určuje minimální "roztažitelné" rozměry okna a prvek ptMaxTrackSize naopak maximální rozměry, do kterých může uživatel(ka) okno roztáhnout.

Prvek ptMaxSize pak určuje rozměry, které bude mít okno, když bude maximalizováno. Z toho vidíme, že maximalizovat okno nemusí vždy znamenat jeho roztažení na celou maximalizační plochu obrazovky. S tím souvisí prvek ptMaxPosition, který určuje polohu levého horního rohu takto omezeného maximalizovaného okna. Pro praktické vyzkoušení uvedených efektů přidáme do procedury okna handler zprávy WM_GETMINMAXINFO:

LRESULT CMinMaxWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  LPMINMAXINFO minmaxinfo;
  switch ( message )
  {
    case WM_GETMINMAXINFO:
      minmaxinfo = (LPMINMAXINFO)lParam;
      minmaxinfo->ptMaxSize.x = 700;
      minmaxinfo->ptMaxSize.y = 600;
      minmaxinfo->ptMaxPosition.x = 50;
      minmaxinfo->ptMaxPosition.y = 50;
      minmaxinfo->ptMaxTrackSize.x = 550;
      minmaxinfo->ptMaxTrackSize.y = 450;
      minmaxinfo->ptMinTrackSize.x = 180;
      minmaxinfo->ptMinTrackSize.y = 150;
      break;
  }
  return CWnd::WindowProc(message, wParam, lParam);
}

Toto okno je vytvořeno z hlavního okna způsobem, který většina z vás jistě zná:

void CMainFrame::OnMinMaxWnd()
{
  m_MinMaxWnd.CreateEx(WS_EX_TOPMOST,
    AfxRegisterWndClass(0, NULL, (HBRUSH)(COLOR_WINDOW + 1)),
    "Omezení velikosti",
    WS_OVERLAPPEDWINDOW,
    120,100, // počáteční poloha
    350, 250, // počáteční rozměry
    m_hWnd, // rodičovské okno
    (HMENU)NULL,
    NULL);
  m_MinMaxWnd.ShowWindow(SW_SHOW);
}

Okno přes celou obrazovku

Jako poslední si v tomto článku ukážeme, jak vytvořit okno, které bude překrývat celou obrazovku, včetně pruhu úloh, a bude vyplněno štětcem tvořeným z bitmapy. V projektu máme okno CMyWindow, na kterém si ukážeme postup. Full-screen okno vytvoříme například takto:

void CMainFrame::OnFullScreen()
{
m_MyWindow.m_NullBrush = FALSE;
m_MyWindow.CreateEx(WS_EX_TOPMOST,
AfxRegisterWndClass(0, NULL,
(HBRUSH)CreatePatternBrush(
LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BACKGROUND)))),
"",
WS_POPUP,
0,0,
GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN),
m_hWnd,
(HMENU)NULL,
NULL);
m_MyWindow.ShowWindow(SW_SHOW);
m_MyWindow.RedrawWindow();
}

Jak je vidět, stačí pouze použít styl WS_POPUP, tedy okno bez jakýchkoli dalších okrajů a titulku a nastavit ho na velikost obrazovky, kterou zjistíme ze systémové metriky funkcí GetSystemMetrics. Pro "posílení pozice" okna mu ještě nastavíme jako rozšířený styl vlastnost WS_EX_TOPMOST, tedy nahoře. V této souvislosti si zde ukažme jeden žertík. Pokud toto full-screen okno vytvoříme s nulovým štětcem, jeho třída tedy bude mít hbrBackground (HBRUSH) rovný NULL, okno bude při svém vytvoření "průhledné", což znamená, že na obrazovce se nijak neprojeví, avšak méně ostřílený uživatel(ka) bude mít silný pocit, "že mu ta okna zamrzla". Bude klikat myší, mačkat klávesnici a nic. Samozřejmě že stačí například Alt-Tab nebo Alt-F4, aby se přepnul do jiné aplikace nebo okno zavřel. Nastavení nulového štětce v případě použití MFC je velice jednoduché, při vytvoření okna bude funkce AfxRegisterWndClass mít parametr hbrBackgroud roven 0, takže můžeme nechat defaultní parametr. Vytvoření okna pak vypadá takto:

void CMainFrame::OnNullBrush()
{
  m_MyWindow.m_NullBrush = TRUE;
  m_MyWindow.CreateEx(WS_EX_TOPMOST,
    AfxRegisterWndClass(0), "",
    WS_POPUP,
    0,0,
    GetSystemMetrics(SM_CXSCREEN),
    GetSystemMetrics(SM_CYSCREEN),
    m_hWnd,
    (HMENU)NULL,
    NULL);
  m_MyWindow.ShowWindow(SW_SHOW);
  m_MyWindow.RedrawWindow();
}

Pokud se ptáte, proč je nastaven (vlastní - vytvořený) prvek třídy CMyWindow m_NullBrush, je to proto, abych při kreslení okna mohl rozlišit, který typ štětce právě okno má. Handler WM_PAINT, ve kterém zabráníme zmatení uživatele, pak vypadá takto (v případě nulového štětce nakreslíme "přes obrazovku" červený kříž):

void CMyWindow::OnPaint()
{
  CPaintDC dc(this);
  RECT clientRect;
  HPEN hPen;
  GetClientRect(&clientRect);
  if ( !m_NullBrush )
  {
    dc.SetBkMode(TRANSPARENT);
    DrawText(dc.m_hDC, "Zavřete nejlépe pomocí Alt-F4", -1,
      &clientRect, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
  }
  else
  {
    hPen = CreatePen(PS_SOLID, 20, 0x000000A0);
    SelectObject(dc.m_hDC, hPen);
    dc.MoveTo(clientRect.left + 100, clientRect.top + 100);
    dc.LineTo(clientRect.right - 100, clientRect.bottom - 100);
    dc.MoveTo(clientRect.left + 100, clientRect.bottom - 100);
    dc.LineTo(clientRect.right - 100, clientRect.top + 100);
  }
}

Zde je ukázkový příklad ke stažení okna.zip (86 kB)

Témata článku: Software, Programování, Okna, Bohatý výčet, Switch, Code, Pár, Message, Case, Okno

3 komentáře

Nejnovější komentáře

  • Screamer 16. 2. 2006 19:02:34
    no ten ghost window frame stejne neni zaruceny, protoze to si de v systemu...
  • ML 31. 10. 2004 23:05:57
    Pan autor a co tak definicia funkcie MessageBox(HWND...) vam nic nehovori?...
  • peko 4. 10. 2001 9:36:02
    Este raz vdaka za ten 1. tip ohladom tahania okna za klientsku oblast....
Určitě si přečtěte

Země se jenom o vlásek vyhnula věčnému zmrznutí

Země se jenom o vlásek vyhnula věčnému zmrznutí

** Země po většinu doby své existence zmrzlá rozhodně nebyla ** Podle nového výzkumu tomu však unikla jen o vlásek ** Kdyby totiž byla jenom o 15 procent dále od Slunce, tak by prý kompletně celá zamrzla

20.  9.  2017 | Stanislav Mihulka | 9

CCleaner obsahuje softwarovou havěť! Tvůrcům se do kódu dostali hackeři

CCleaner obsahuje softwarovou havěť! Tvůrcům se do kódu dostali hackeři

** Masově oblíbený program pro softwarovou očistu Windows ovládli hackeři ** Narušení se podařilo zavčas odhalit, unikla jen data o počítačích uživatelů ** Je paradoxní, že CCleaner byl slabě zabezpečen, když jej letos koupil Avast

18.  9.  2017 | David Polesný | 46

Hacknutý CCleaner je mnohem zákeřnější, než se zdálo. Update na novou verzi nestačí, proveďte obnovu systému

Hacknutý CCleaner je mnohem zákeřnější, než se zdálo. Update na novou verzi nestačí, proveďte obnovu systému

** Chyba v CCleaneru je závažnější, než se zdálo ** Update na novou verzi nemusí stačit ** Přinášíme detaily

21.  9.  2017 | Stanislav Janů | 61

Noční strana Venuše vydala další tajemství

Noční strana Venuše vydala další tajemství

18.  9.  2017 | Jiří Černý

Americká armáda si pořizuje nové švédské pancéřovky Carl Gustav

Americká armáda si pořizuje nové švédské pancéřovky Carl Gustav

** Pancéřovky Carl Gustav jsou přenosné bezzákluzové protipancéřové zbraně ráže 84 mm se sofistikovanou optikou ** Jejich první verze byla vyrobena již v roce 1946 ** Mj. si je oblíbili i v americké armadě

21.  9.  2017 | Stanislav Mihulka


Aktuální číslo časopisu Computer

Vyplatí se ještě těžit kryptoměny?

Velký test studentských notebooků

Test pěti levných soundbarů

Nejlepší chytré hodinky