Rozdział XXII - Przejęcie klasy okna


Na tej lekcji zapoznamy się z przejmowaniem klasy okna (window superclassing). Dowiemy się co to jest i do czego ono służy. Również nauczysz się dodawania nawigacji klawiszem TAB do kontrolek w swoim własnym oknie.

 

 

Kod źródłowy możesz pobrać {z tego archiwum}.

 

Teoria

W swojej karierze programisty z pewnością spotkasz sytuacje, gdy musisz umieścić w oknie kilka kontrolek o "nieco" innym działaniu niż standardowe. Na przykład możesz potrzebować 10 kontrolek edycyjnych, które akceptują jedynie liczby. Istnieje kilka sposobów osiągnięcia tego celu:

Pierwsza metoda jest zbyt męcząca. Musiałbyś samemu zaimplementować całe działanie kontrolki edycyjnej. Trudne zadanie do podjęcia się go. Druga metoda jest już lepsza od pierwszej, lecz wciąż wymaga dużo pracy. Można jej użyć przy kilku kontrolkach, lecz zmienia się w koszmar nocny, jeśli musimy przejąć procedury okna tuzina lub więcej kontrolek. W takiej sytuacji najbardziej odpowiednią techniką jest przejęcie klasy okna (superclassing).

Przejęcie klasy okna jest metodą używaną do "przejęcia kontroli" nad określoną klasą okna. Przez "przejęcie kontroli" mam na myśli, iż możesz zmodyfikować własności klasy okna, aby przystosować ją do własnych celów, a następnie utworzenie na jej podstawie wiązanki kontrolek.

Poniżej wymieniłem kroki przy przejmowaniu klasy okna:

Przejęcie klasy okna (superclassing) jest lepsze od przejmowania procedury okna (subclassing), jeśli planujesz utworzenie wielu kontrolek o tych samych charakterystykach.

Przykład

Plik SUPERCLASS.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\user32.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDELIB \masm32\lib\user32.lib
INCLUDELIB \masm32\lib\kernel32.lib

WinMain     PROTO :DWORD, :DWORD, :DWORD, :DWORD
EditWndProc PROTO :DWORD, :DWORD, :DWORD, :DWORD

.CONST

WM_SUPERCLASS EQU WM_USER+5

.DATA

ClassName DB "SuperclassWinClass", 0
AppName   DB "Przejmowanie klasy okna", 0
EditClass DB "EDIT", 0
OurClass  DB "SUPEREDITCLASS", 0
Message   DB "Nacisnąłeś klawisz Enter w polu tekstowym!", 0

.DATA?

hInstance  DD ?
hwndEdit   DD 6 DUP(?)
OldWndProc DD ?

.CODE

start:
    INVOKE GetModuleHandle,  NULL
    mov    hInstance, eax
    INVOKE WinMain,  hInstance, NULL, NULL,  SW_SHOWDEFAULT
    INVOKE ExitProcess, eax

WinMain PROC hInst:     HINSTANCE,\
             hPrevInst: HINSTANCE,\
             CmdLine:   LPSTR,\
             CmdShow:   DWORD

LOCAL wc:   WNDCLASSEX
LOCAL msg:  MSG
LOCAL hwnd: HWND

    mov    wc.cbSize, SIZEOF WNDCLASSEX
    mov    wc.style,  CS_HREDRAW OR CS_VREDRAW
    mov    wc.lpfnWndProc,  OFFSET WndProc
    mov    wc.cbClsExtra, NULL
    mov    wc.cbWndExtra, NULL
    push   hInst
    pop    wc.hInstance
    mov    wc.hbrBackground, COLOR_APPWORKSPACE
    mov    wc.lpszMenuName, NULL
    mov    wc.lpszClassName, OFFSET ClassName
    INVOKE LoadIcon, NULL, IDI_APPLICATION
    mov    wc.hIcon, eax
    mov    wc.hIconSm, eax
    INVOKE LoadCursor, NULL, IDC_ARROW
    mov    wc.hCursor, eax
    INVOKE RegisterClassEx,  ADDR wc
    INVOKE CreateWindowEx, WS_EX_CLIENTEDGE + WS_EX_CONTROLPARENT,\
                           ADDR ClassName, ADDR AppName,\
                           WS_OVERLAPPED + WS_CAPTION + WS_SYSMENU + \
                           WS_MINIMIZEBOX + WS_MAXIMIZEBOX + WS_VISIBLE,\
                           CW_USEDEFAULT, CW_USEDEFAULT, 350, 220, NULL, NULL,\
                           hInst, NULL
    mov hwnd, eax

    .WHILE TRUE
        INVOKE GetMessage,  ADDR msg, NULL, 0, 0
        .BREAK .IF (!eax)
        INVOKE TranslateMessage,  ADDR msg
        INVOKE DispatchMessage,  ADDR msg
    .ENDW

    mov eax, msg.wParam
    ret

WinMain ENDP

WndProc PROC USES ebx edi hWnd:   HWND,\
                          uMsg:   UINT,\
                          wParam: WPARAM,\
                          lParam: LPARAM

LOCAL wc: WNDCLASSEX	

    .IF uMsg==WM_CREATE
        mov    wc.cbSize, SIZEOF WNDCLASSEX
        INVOKE GetClassInfoEx, NULL, ADDR EditClass, ADDR wc
        push   wc.lpfnWndProc
        pop    OldWndProc
        mov    wc.lpfnWndProc,  OFFSET EditWndProc
        push   hInstance
        pop    wc.hInstance
        mov    wc.lpszClassName, OFFSET OurClass
        INVOKE RegisterClassEx,  ADDR wc
        xor    ebx, ebx
        mov    edi, 20

        .WHILE ebx<6
            INVOKE CreateWindowEx, WS_EX_CLIENTEDGE,\
                                   ADDR OurClass, NULL,\
                                   WS_CHILD + WS_VISIBLE + WS_BORDER, 20,\
                                   edi, 300, 25, hWnd, ebx,\
                                   hInstance, NULL		
            mov    DWORD PTR [hwndEdit+4*ebx], eax
            add    edi, 25
            inc    ebx
        .ENDW

        INVOKE SetFocus, hwndEdit
    .ELSEIF uMsg==WM_DESTROY
        INVOKE PostQuitMessage, NULL
    .ELSE
        INVOKE DefWindowProc, hWnd, uMsg, wParam, lParam		
        ret
    .ENDIF

    xor eax, eax
    ret

WndProc ENDP

EditWndProc PROC hEdit:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD

    .IF uMsg==WM_CHAR
        mov eax, wParam
        .IF (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
            .IF al>="a" && al<="f"
                sub al, 20h
            .ENDIF
            INVOKE CallWindowProc, OldWndProc, hEdit, uMsg, eax, lParam
            ret
        .ENDIF
    .ELSEIF uMsg==WM_KEYDOWN
        mov eax, wParam
        .IF al==VK_RETURN
            INVOKE MessageBox, hEdit,\
                               ADDR Message, ADDR AppName,\
                               MB_OK + MB_ICONINFORMATION
            INVOKE SetFocus, hEdit
        .ELSEIF al==VK_TAB
            INVOKE GetKeyState, VK_SHIFT
            test   eax, 80000000H
            .IF ZERO?
                INVOKE GetWindow, hEdit, GW_HWNDNEXT
                .IF eax==NULL
                    INVOKE GetWindow, hEdit, GW_HWNDFIRST
                .ENDIF
            .ELSE
                INVOKE GetWindow, hEdit, GW_HWNDPREV
                .IF eax==NULL
                    INVOKE GetWindow, hEdit, GW_HWNDLAST
                .ENDIF
            .ENDIF
            INVOKE SetFocus, eax
            xor    eax, eax
            ret
        .ELSE
            INVOKE CallWindowProc, OldWndProc, hEdit, uMsg, wParam, lParam
            ret
        .ENDIF
    .ELSE
        INVOKE CallWindowProc, OldWndProc, hEdit, uMsg, wParam, lParam
        ret
    .ENDIF

    xor eax, eax
    ret

EditWndProc ENDP

END start

Analiza

Program utworzy proste okno z 6 "zmodyfikowanymi" kontrolkami edycji w jego obszarze roboczym. Kontrolki edycyjne będą akceptowały tylko cyfry szesnastkowe. Właściwie to zmodyfikowałem przykład z przejmowaniem procedury okna, aby zastosować go w przejmowaniu klasy, stąd duże podobieństwo kodu. Program uruchamia się normalnie, a interesujący fragment występuje w sekcji tworzenia okna:

 

    .IF uMsg==WM_CREATE
        mov    wc.cbSize, SIZEOF WNDCLASSEX
        INVOKE GetClassInfoEx, NULL, ADDR EditClass, ADDR wc

 

Najpierw musimy wypełnić strukturę WNDCLASSEX danymi z klasy, którą chcemy przejąć, czyli klasy EDIT. Pamiętaj o ustawieniu pola cbSize w strukturze WNDCLASSEX przed wywołaniem GetClassInfoEx, w przeciwnym razie struktura WNDCLASSEX nie zostanie wypełniona prawidłowo. Po powrocie z GetClassInfoEx struktura wc wypełniona jest całą potrzebną nam informacją do utworzenia nowej klasy okna.

 

    push   wc.lpfnWndProc
    pop    OldWndProc
    mov    wc.lpfnWndProc,  OFFSET EditWndProc
    push   hInstance
    pop    wc.hInstance
    mov    wc.lpszClassName, OFFSET OurClass

 

Teraz musimy zmodyfikować kilka pól struktury wc. Pierwszym jest wskazanie do procedury okna. Ponieważ będziemy musieli połączyć naszą własną procedurę okna z oryginalna procedurą, niezbędne jest zapamiętanie zawartości tego pola w zmiennej OldWndProc, aby później nie było problemów z wywołaniem za pomocą funkcji CallWindowProc. Ta technika jest identyczna jak w przejmowaniu procedury okna, z wyjątkiem tego, iż modyfikuje się strukturę WNDCLASSEX bezpośrednio a nie za pomocą wywołania SetWindowLong. Następne dwa pola, hInstance oraz lpsClassName, muszą być zmienione, w przeciwnym razie system nie zarejestruje twojej nowej klasy okna. Musisz wymienić oryginalną zawartość pola hInstance z wartością hInstance twojego własnego programu. Również należy wybrać nową nazwę dla nowej klasy.

 

    INVOKE RegisterClassEx,  ADDR wc

 

Gdy wszystko będzie gotowe, rejestrujemy nową klasę. Będzie ona posiadała niektóre własności starej klasy.

 

    xor    ebx, ebx
    mov    edi, 20
    .WHILE ebx<6
        INVOKE CreateWindowEx, WS_EX_CLIENTEDGE,\
                               ADDR OurClass, NULL,\
                               WS_CHILD + WS_VISIBLE + WS_BORDER, 20,\
                               edi, 300, 25, hWnd, ebx,\
                               hInstance, NULL		
        mov    DWORD PTR [hwndEdit+4*ebx], eax
        add    edi, 25
        inc    ebx
    .ENDW
    INVOKE SetFocus, hwndEdit

 

Po zarejestrowaniu tej klasy można na jej podstawie tworzyć okna. W powyższym fragmencie kodu używam rejestru ebx jako licznika ilości utworzonych okien. Rejestr edi używany jest jako współrzędna y lewego górnego narożnika okna. Gdy okno jest tworzone, jego uchwyt zostaje umieszczony w tablicy podwójnych słów (DWORD). Po utworzeniu wszystkich okien ustawiamy skupienie na pierwszym oknie.

W tym momencie otrzymujemy 6 kontrolek edycyjnych, które akceptują tylko cyfry szesnastkowe. Zastąpiona procedura okna obsługuje ten filtr. Właściwie jest ona identyczna z procedurą okna w przykładzie przejmowania procedury okna. Jak widzisz, nie musisz wykonywać dodatkowej pracy.

Dorzuciłem fragment kodu obsługujący nawigację klawiszem TAB, aby nadać temu przykładowi więcej treści. Zwykle, gdy umieszczasz kontrolki w oknie dialogowym, menadżer okna dialogowego obsługuje dla ciebie klawisze nawigacyjne, zatem możesz przemieszczać się do następnej kontrolki klawiszem TAB, a do poprzedniej za pomocą SHIFT-TAB. Niestety taka usługa nie jest dla ciebie dostępna, jeśli umieścisz swoje kontrolki w prostym oknie. Musisz przejąć ich procedury okien, aby umożliwić obsługę klawisza TAB. W naszym przykładzie nie musimy przejmować procedur okien kontrolek jednej za drugą, ponieważ już przejęliśmy ich klasę, zatem możemy utworzyć dla nich "centralnego zarządcę nawigacji".

 

    .ELSEIF al==VK_TAB
        INVOKE GetKeyState, VK_SHIFT
        test   eax, 80000000
        .IF ZERO?
            INVOKE GetWindow, hEdit, GW_HWNDNEXT
            .IF eax==NULL
                INVOKE GetWindow, hEdit, GW_HWNDFIRST
            .ENDIF
        .ELSE
            INVOKE GetWindow, hEdit, GW_HWNDPREV
            .IF eax==NULL
                INVOKE GetWindow, hEdit, GW_HWNDLAST
            .ENDIF
        .ENDIF
        INVOKE SetFocus, eax
        xor    eax, eax
        ret

 

Powyższy fragment kodu pochodzi z procedury EditWndClass. Sprawdza on, czy użytkownik nacisnął klawisz TAB, a jeśli tak, to wywołuje funkcję GetKeyState w celu sprawdzenia, czy jest naciśnięty również klawisz SHIFT. Funkcja GetKeyState zwraca wartość w rejestrze eax, która określa, czy zadany klawisz jest naciśnięty, czy nie. Jeśli klawisz jest naciśnięty, to najstarszy bit rejestru eax zostaje ustawiony na 1. Jeśli nie, najstarszy bit ma wartość 0. Zatem testujemy najstarszy bit eax (test bitowy z 80000000h). Ustawienie najstarszego bitu na 1 oznacza, iż użytkownik nacisnął kombinację klawiszy SHIFT+TAB, co musimy obsłużyć oddzielnie.

Jeśli użytkownik naciśnie sam klawisz TAB, wywołujemy GetWindow, aby pobrać uchwyt następnej kontrolki. Stosujemy znacznik GW_HWNDNEXT, aby poinformować funkcję GetWindow, iż powinna pobrać uchwyt do okna następnego po bieżącym hEdit. Jeśli funkcja ta zwróci NULL, to potraktujemy to jako koniec uchwytów do pobrania, zatem obecny uchwyt hEdit wskazuje ostatnią kontrolkę w szeregu. Przewiniemy skupienie do pierwszej kontrolki wywołując GetWindow ze znacznikiem GW_HWNDFIRST. W podobny sposób obsługujemy kombinację klawiszy SHIFT-TAB pamiętając, iż powinna ona dawać efekt odwrotny.

 

Dodatek w Pascalu

Ta sama aplikacja w Pascalu:

 

{********************************
**  I Liceum Ogólnokształcące  **
**           w Tarnowie        **
**       mgr Jerzy Wałaszek    **
********************************}

program Superclass;

uses Windows;

const
  WM_SUPERCLASS = WM_USER + 5;
  ClassName     = 'SuperclassWinClass';
  AppName       = 'Przejmowanie klasy okna';
  EditClass     = 'EDIT';
  OurClass      = 'SUPEREDITCLASS';
  Messagetx     = 'Nacisnąłeś klawisz Enter w polu tekstowym!';

type

  TOldWndProc = function (a,b,c,d:longword) : longint;

var

  hInstance  : HINST;
  hwndEdit   : array[0..5] of HANDLE;
  OldWndProc : TOldWndProc;

function EditWndProc(hEdit,uMsg,wParam,lParam:longword) : longint;
var
  c : char;
  h : HANDLE;

begin
  Result := 0;
  case uMsg of
    WM_CHAR:
    begin
      c := UpCase(char(wParam and $ff));
      if (c in ['0'..'9']) or (c in ['A'..'F']) or (ord(c) = VK_BACK) then
        Result := CallWindowProc(OldWndProc,hEdit,uMsg,ord(c),lParam);
    end;
    WM_KEYDOWN:
    begin
      case (wParam and $ff) of
        VK_RETURN:
        begin
          MessageBox(hEdit,Messagetx,AppName,MB_OK + MB_ICONINFORMATION);
          SetFocus(hEdit);
        end;
        VK_TAB:
        begin
          if (GetKeyState(VK_SHIFT) and $80000000) = 0 then
          begin
            h := GetWindow(hEdit,GW_HWNDNEXT);
            if h = 0 then h := GetWindow(hEdit,GW_HWNDFIRST);
          end
          else
          begin
            h := GetWindow(hEdit,GW_HWNDPREV);
            if h = 0 then h := GetWindow(hEdit,GW_HWNDLAST);
          end;
          SetFocus(h);
        end;
        else  Result := CallWindowProc(OldWndProc,hEdit,uMsg,wParam,lParam);
      end;
    end;
    else Result := CallWindowProc(OldWndProc,hEdit,uMsg,wParam,lParam);
  end;
end;

function WndProc(hWnd:HWND;uMsg:UINT;wParam:WPARAM;lParam:LPARAM) : longint;
var
  wc  : WNDCLASSEX;
  i,y : integer;

begin
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
      wc.cbSize := sizeof(WNDCLASSEX);
      GetClassInfoEx(0,EditClass,wc);
      OldWndProc       := wc.lpfnWndProc;
      wc.lpfnWndProc   := @EditWndProc;
      wc.hInstance     := hInstance;
      wc.lpszClassName := OurClass;
      RegisterClassEx(wc);
      y := 20;
      for i := 0 to 5 do
      begin
        hwndEdit[i] := CreateWindowEx(WS_EX_CLIENTEDGE,OurClass,0,
                       WS_CHILD + WS_VISIBLE + WS_BORDER,20,y,300,25,
                       hWnd,i,hInstance,0);
        y := y + 25;
      end;
      SetFocus(hwndEdit[0]);
    end;
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(hWnd,uMsg,wParam,lParam);
  end;
end;

function WinMain(hInst,hPrevInst:HINST;CmdLine:LPSTR;CmdShow:DWORD): longint;
var
  wc   : WNDCLASSEX;
  msg  : MSG;
  hwnd : HWND;

begin
  with wc do
  begin
    cbSize        := sizeof(WNDCLASSEX);
    style         := CS_HREDRAW or CS_VREDRAW;
    lpfnWndProc   := @WndProc;
    cbClsExtra    := 0;
    cbWndExtra    := 0;
    hInstance     := hInst;
    hbrBackground := COLOR_APPWORKSPACE;
    lpszMenuName  := 0;
    lpszClassName := ClassName;
    hIcon         := LoadIcon(0,IDI_APPLICATION);
    hIconSm       := hIcon;
    hCursor       := LoadCursor(0,IDC_ARROW);
  end;
  RegisterClassEx(wc);
  hwnd := CreateWindowEx(WS_EX_CLIENTEDGE + WS_EX_CONTROLPARENT,
          ClassName,AppName,WS_OVERLAPPED + WS_CAPTION + WS_SYSMENU +
          WS_MINIMIZEBOX + WS_MAXIMIZEBOX + WS_VISIBLE,
          CW_USEDEFAULT,CW_USEDEFAULT,350,220,0,0,hInst,0);
  while GetMessage(msg,0,0,0) do
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
  Result := msg.wParam;
end;

begin
  hInstance := GetModuleHandle(0);
  ExitProcess(WinMain(hInstance,0,0,SW_SHOWDEFAULT));
end.

 

Autorem kursu jest Iczelion. Kurs programowania Windows znalazł się na serwerze I LO w Tarnowie za pisemną zgodą autora.
Tłumaczenie z języka angielskiego, opracowanie HTML i konwersję przykładów programów wykonał mgr Jerzy Wałaszek.



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.