Každý, kdo někdy programoval uživatelské rozhraní se někdy setkal s problémem jak změnit velikost dětských oken společně se změnou okna rodičovského. Tato vlastnost umožní uživateli roztáhnout dialog podle svého přání. Příklad implementace takovéhoto dialogu je vyhledávací dialog v programu Total Commander (Alt+F7) nebo okno programu Outlook Express "Nová zpráva". Vyřešení tohoto problému vyžaduje reagovat na zprávu WM_SIZE hlavního okna. Obsluha této zprávy musí zajistit změnu velikosti všech dětských oken.
Každý, kdo někdy naprogramoval změnu velikosti dětských oken společně se změnou okna rodičovského, zjistil, že změnu velikosti okna doprovází nepříjemné blikání celého obsahu okna. K tomuto jevu dochází na systémech, které maji nastaveno překreslování okna během změny jeho velikosti:
HKEY_CURRENT_USER\Control Panel\Desktop DragFullWindows=dword:1
Blikání vzniká v důsledku několikanásobného překreslování plochy okna pozadím okna, následně např. pozadím listboxu a pak ještě položkami listboxu. Rychlejší grafická karta v tomto případě nic nevyřeší, pouze zvýší "frekvenci blikání". Blikání je nutné odstranit programově.
V dokumentaci MSDN je uvedeno, že při použití příznaku WS_CLIPCHILDREN u rodičovského okna zabrání kreslení do oblasti, kterou zabírají dětská okna. Kromě toho je dobré nastavit každému dětskému oknu příznak WS_CLIPSIBLINGS, který zabrání případnému překreslování dětských oken v případě, pokud se překrývají.
for(/* Pro všechna dětská okna */) { // Vypočítáme novou pozici okna a přesuneme jej. if(GetNewWindowRect(hParent, hWndChild, NewRect)) { SetWindowPos(hWndChild, NULL, NewRect.left, NewRect.top, NewRect.right, NewRect.bottom, SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOOWNERZORDER); InvalidateRect(hWndChild, NULL, TRUE); } }
Pokusil jsem se takovou metodu naprogramovat, ale výsledky stále nebyly tak dobré, jak bych očekával. Např. okno zmiňovaného programu Outlook Express bylo změny své velikosti s mnohem větší "elegancí", tedy bez blikání. Zdrojový text funkce GetNewWindowRect najdete v souborech k článku.
Jako druhý pokus jsem se inspiroval u knihovny VCL z Delphi (C++ Builderu). Zjistil jsem, že překreslování oken je kompletně v režii knihovny VCL, která používá tzv. double buffering - nejdříve vytvoří paměťové DC, pak do něj nakreslí formulář (dialog), následně všechna dětská okna. Takto připravené DC je následně překopírováno do DC dialogu. Blikání žádné.
// Zakážeme překreslování SendMessage(hParent, WM_SETREDRAW, FALSE, 0); for(/* Pro všechna dětská okna */) { if(GetNewWindowRect(hParent, hWndChild, NewRect)) SetWindowPos(hWndChild, NULL, NewRect.left, NewRect.top, NewRect.right, NewRect.bottom, SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOOWNERZORDER); } // Povolíme překreslování a zavoláme funkci, která provede překreslení okna // s použitím double-bufferingu SendMessage(hParent, WM_SETREDRAW, TRUE, 0); PaintWindow(hParent, NULL);
(Kompletní zdrojové texty překreslovacích funkcí zde uvádět nebudu, najdete je v doprovodném materiálu k článku)
Při testování této metody jsem ale zjistil, že je problém s překreslováním neklientských oblastí okna (okraje, scrollbary). Také groupboxy dělají problémy - při změně jejich velikosti vytvářejí nepěkné efekty. Knihovna VCL problém řeší tak, že okraje oken překresluje vlastními silami. To metoda sice funkční, ale podle mého názoru nesystematická. Kdo má vědět, jaké okraje oken budou v konkrétním skinu Windows XP ? Nebo v pozdějších verzích Windows ?
Windows umožňují změnit pozici několika oken v jediném překreslovacím cyklu, a tedy bez blikání. Za použití funkcí BeginDeferWindowPos, DeferWindowPos a EndDeferWindowPos je možné dosáhnout tohoto efektu bez větších problémů:
HDWP hDwp = NULL; int nWindows = 0; // Count the anchored windows and begin the window positioning for(/* Pro všechna dětská okna */) nWindows++; hDwp = BeginDeferWindowPos(nWindows); for(/* Pro všechna dětská okna */) { if(GetNewWindowRect(hParent, hWndChild, NewRect)) hDwp = DeferWindowPos(hDwp, hWndChild, NULL, NewRect.left, NewRect.top, NewRect.right, NewRect.bottom, SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOOWNERZORDER); } EndDeferWindowPos(hDwp);
Tato metoda funguje k mé spokojenosti, okýnka mění svoji velikost bez ošklivého blikání.
Jakmile je problém teoreticky vyřešen, bylo by dobré napsat univerzální kód, který beze změn bude fungovat i po začlenění do jiné aplikace. K řešení mě opět inspirovala knihovna VCL z Delphi, která tento problém řeší použitím tzv. kotev (Anchors). Pri každý objekt je definováno, zda je ukotven k některému z okrajů rodičovského okna. Rychlý a snadný návrh pomocí několika kliků myši budeme muset ve Visual C++ oželet, ale přesto je použití kotev s předprogramovanou třídou lepší než doprogramovávat kotvy do každého dialogu zvlášť.
Napsal jsem třídu TAnchors. Pro její použití je nutné provést tyto kroky:
#include "TAnchors.h"
class CMyWindow { ... protected: TAnchors m_Anchors; // Window anchors };
// Initialize anchors m_Anchors.AddAnchor(m_hWnd, IDC_TITLE, akLeft | akTop | akRight); m_Anchors.AddAnchor(m_hWnd, IDC_EDIT, akLeft | akTop | akRight); m_Anchors.AddAnchor(m_hWnd, IDOK, akRight | akBottom); m_Anchors.AddAnchor(m_hWnd, IDCANCEL, akRight | akBottom);
Parametry funkce AddAnchor jsou handle rodičovského okna, ID dětského okna (třída TAnchors obsahuje i verzi funkce s parametrem handle dětského okna) a kotvy, složené ze symbolických konstant.
void CMyWindow::OnSize(UINT nType, int cx, int cy) { // Make ancestor handle the event CWnd::OnSize(nType, cx, cy); m_Anchors.AnchorAllObjects(); }
Toť vše - zbývá stáhnout zdrojový text třídy nebo příkladový program a vyzkoušet jej. Ve zdrojovém textu třídy je přibalena i verze pro Delphi, kterou převedl Zdeněk Dárk.
Copyright Ladislav Zezula 23.08.2003