Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Subclassing ovládacích prvků.

21.1.2002

V tomto článku se opět vrátíme k dialogovým prvkům. Dost častým dotazem na diskusním fóru bývá téma, jak zabránit zavření dialogu například po stisknutí klávesy <Enter> v editačním poli na dialogu, nebo jak vůbec obecně reagovat po svém na libovolnou klávesu v edit-boxu nebo podobném dialogovém prvku. K tomuto účelu se musíme seznámit s takzvaným "subclassingem", což znamená "napíchnutí" se na proceduru okna libovolného okna (tedy i prvku na dialogu) v aplikaci. Když se nám toto podaří, pak z toho logicky vyplývá, že můžeme odchytit libovolnou zprávu tomuto prvku, a nejen ty oznamovací zprávy, které prvek posílá dialogu.

Jak tedy bude fungovat dnešní ukázka? Na dialogu jsou 2 běžné jednořádkové edit-boxy a jeden víceřádkový. Budeme chtít, aby po stisknutí klávesy <Enter> v některém ze 2 jednořádkových edit-boxů, se nám jeho obsah vložil jako nový řádek do víceřádkového editu, samozřejmě bez zavření dialogu. Současně si ukážeme, jak v aplikaci založené na dialogu zobrazit vlastní ikonu jako systémovou ikonu dialogu (což také bývá dotazem v diskusním fóru). Výsledek vidíte na obrázku:

win-api-21

Jak tedy na to. Podívejme se, jak vypadá handler zprávy WM_INITDIALOG:

case WM_INITDIALOG:
  SetClassLongPtr(hWnd, GCLP_HICONSM, 
    (LONG_PTR)LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_ICON_MAIN)));
  oldProc = (WNDPROC)SetWindowLongPtr(GetDlgItem(hWnd, IDC_EDIT1),
    GWLP_WNDPROC, (LONG_PTR)WindowProcEdit);
  oldProc = (WNDPROC)SetWindowLongPtr(GetDlgItem(hWnd, IDC_EDIT2),
    GWLP_WNDPROC, (LONG_PTR)WindowProcEdit);
	break;

Jako první je nastavení vlastní ikony dialogovému oknu. Jak vidíte, stačí použít již známou funkci SetClassLongPtr s parametrem GCLP_HICONSM, kde jejím třetím parametrem je handle ikony. K tomu ještě 2 poznámky. Je třeba nastavit malou ikonu, nikoli velkou (GCLP_HICON). Dále je třeba si uvědomit, že po zavolání této funkce budou všechny další dialogové boxy v naší aplikaci mít tuto ikonu jako systémovou, neboť ikonu lze nastavit třídě oken (tedy třídě dialogových oken) jako celku. Na to je třeba myslet v případě více dialogů, z nichž jen některé mají mít svoji ikonu. Pak je třeba si uložit hodnotu (handle ikony HICON) vrácené funkcí SetClassLongPtr a nejlépe před voláním EndDialog ji opět "vrátit zpět".

Dále vidíme použití funkce SetWindowLongPtr pro nastavení procedury okna na vlastní funkci. Jak hned uvidíme,je důležité uložit si původní hodnotu (vrácenou voláním této funkce) do proměnné typu WNDPROC:

	WNDPROC oldProc;

Funkce WindowProcEdit může být společná pro více prvků stejného typu (v našem případě oba edit-boxy). Podívejme se, jak vypadá v našem případě:

// wParam je kód klávesy, lParam je ID prvku
#define UM_KEY_IN_CONTROL ( WM_APP + 1 )
LRESULT CALLBACK WindowProcEdit(HWND hWnd, UINT uMsg,
  WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_GETDLGCODE:
      return DLGC_WANTALLKEYS;
    case WM_KEYDOWN:
      SendMessage(GetParent(hWnd), UM_KEY_IN_CONTROL, wParam /*kod lávesy*/,
        (LPARAM)GetMenu(hWnd));
      break;
  }
  return CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam);
}

Nejprve jeden důležitý rozdíl oproti "běžné" proceduře okna: jak návratovou hodnotu musíme použít hodnotu,kterou nám vrátí původní procedura okna (uložená v oldProc). To zajistíme použitím funkce CallWindowProc, které má jako 1. parametr právě hodnotu procedury okna, která se má volat. Dalšími parametry pak jsou předané (popřípadě i modifikované) parametry naší procedury.

Dále vidíte handler zprávy WM_GETDLGCODE. Tímto musíme říci systému, že chceme, aby do dialogového prvku byly posílané některé další zprávy, především jde o zprávy klávesnice. Hodnotou DLGC_WANTALLKEYS si jednoduše řekneme o všechny zprávy, které jsou nabízeny. Když se podíváte do dokumentace, zjistíte že máme na výběr další hodnoty, určující separátně pouze některé klávesy. Klíčem k vlastnímu zpracování klávesy <Enter> je zachycení zprávy WM_KEYDOWN, po kterém pošleme dialogu (obecně vlastníku editu) uživatelskou zprávu UM_KEY_IN_CONTROL, kde jako parametr lParam zadáme kód klávesy a do lParam vložíme identifikátor prvku pro případné rozlišení konkrétního prvku v proceduře dialogu. Tento parametr získáme pomocí funkce GetMenu, neboť když se podíváte na význam parametru hMenu funkce CreateWindowEx, zjistíte, že tento parametr určuje buď handle hlavního menu okna, nebo identifikátor dětského okna, který právě takto zjišťujeme.  Toto řešení je pro náš jednoduchý případ možná zbytečně obecné, ale uvádím ho proto, abych naznačil možné řešení i pro rozsáhlejší případy. Například, pokud jde o program s MFC, můžete si vytvořit třídu odvozenou od CEdit, kde samozřejmě předem neznáte handle okna vlastníka, a musíte použít funkci pro jeho zjištění (nejlépe GetParent()). Reakci na zprávu pak obecně provádíme v dialogu, i když zde bychom opět mohli jednodušeji (s použitím globálních proměnných) vše vyřešit bez dalšího posílání vlastní zprávy přímo v proceduře edit-boxů.

Nyní se podívejme, jak vypadá zpracování naší uživatelské zprávy v proceduře okna dialogu:

case UM_KEY_IN_CONTROL:
  switch (lParam)
  {
    case IDC_EDIT1:
    case IDC_EDIT2:
      if (wParam == VK_RETURN)
      {
        GetDlgItemText(hWnd, lParam, chText, MAX_LINE);
        lstrcat(chText, newLine);
        SendDlgItemMessage(hWnd, IDC_EDIT_MULTI, 	EM_REPLACESEL,
          FALSE, (LPARAM)chText);
      }
      break;
  }
  break;

Nejdříve otestujeme kód virtuální klávesy, uložený v parametry wParam (zprávy WM_KEYDOWN a předaný do stejného parametru naší uživatelské zprávy). V případě VK_RETURN (klávesa <Enter>) načteme již známým způsobem (GetDlgItemText) text edit-boxu určeného parametrem lParam, do kterého jsme si uložili identifikátor prvku pro rozlišení edit.boxů navzájem. Dále přidáme na konec textu znaky ukončující řádek, definované v tomto řetězci:

	TCHAR newLine[] = {0x0D, 0x0A, 0};

Pozor, zde nelze použít znak "nové řádky", tedy "\n"!

Přidání textu do edit-boxu

Dále použijeme zprávu EM_REPLACESEL, která obecně nahradí aktuálně vybraný text v edit-boxu textem zadaným v parametru lParam. Pokud není vybrán žádný text, je tento text vložen na pozici karetu, který po přidání textu zůstává nastaven na konec textu. Pro programové nastavení výběru znaků v edit boxu (samozřejmě i víceřádkovém) slouží zpráva EM_SETSEL, v jejímž parametru wParam je pozice znaku určujícího počátek výběru a v parametru lParam pak znak ukončující výběr. Pokud chceme jednoduše vybrat celý text, zadáme wParam = 0 a lParam = -1. Pokud chceme zrušit aktuální výběr, zadáme parametr wParam = -1. Nastavení pozice karetu (bez nastavení výběru textu) na libovolné místo provedeme tak, že oba parametry této zprávy nastavíme na hodnotu této pozice. Na konec textu se tady můžeme nastavit tak, že zjistíme délku textu v editu pomocí funkce GetWindowTextLength a máme tak hodnotu, na kterou nastavíme pozici karetu pro tento požadovaný případ.

Doprovodný projekt je ke stažení zde: win_api_21.zip