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)

Diskuze (3) Další článek: Globe Internet převzala české zákazníky Carambole

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


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

Kdyby měli železničáři tento superpočítač za 99 dolarů, nepotřebovali by lasery

Kdyby měli železničáři tento superpočítač za 99 dolarů, nepotřebovali by lasery

** Nejmodernější český železniční tunel je prošpikovaný technologiemi ** Za tři tisíce koupíte počítač, který je překoná ** Seznamte se s Nvidia Jetson Nano

Jakub Čížek | 50

Co zabírá nejvíce místa na disku? Těchto 10 nástrojů odhalí největší žrouty dat

Co zabírá nejvíce místa na disku? Těchto 10 nástrojů odhalí největší žrouty dat

** Je vhodné jednou za čas zanalyzovat, co vám leží na disku ** Poradíme vám nástroje, kterými zjistíte, jaká data uchováváte ** Podle výsledků můžete optimalizovat svá data či úložiště

Karel Kilián | 49

Pojďme programovat elektroniku: Postavíme bezpečnostní systém za 30 Kč

Pojďme programovat elektroniku: Postavíme bezpečnostní systém za 30 Kč

** Před pár týdny jste si mohli v akci koupit Wi-Fi desku za jeden dolar ** Nám už TTGO T-Display dorazila do redakce ** Připojíme k ní jazýčkový kontakt a vyrobíme bezpečnostní systém

Jakub Čížek | 30



Aktuální číslo časopisu Computer

Speciál o přechodu na DVB-T2

Velký test herních myší

Super fotky i z levného mobilu

Jak snadno upravit PDF