Rozdział XX - Przejęcie procedury okna


Na tej lekcji nauczymy się o przejmowaniu procedury okna (window subclassing), co to jest oraz jak zastosować to dla naszych własnych korzyści.

 

 

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

 

Teoria

Jeśli programujesz w systemie Windows już od pewnego czasu, to na pewno napotkałeś sytuację, gdzie jakieś okno posiadało prawie wszystkie potrzebne ci własności, lecz nie do końca. Czy była ci kiedykolwiek potrzebna specjalna kontrolka edycyjna, która filtruje niektóre niepożądane informacje? Bezpośrednim podejściem byłoby zaprojektowanie własnego okna. Ale to na prawdę ciężka i zajmująca czas praca. Rozwiązaniem jest przejęcie procedury okna.

Krótko mówiąc, przejęcie procedury okna pozwala ci "przejąć" to okno. Będziesz miał absolutną kontrolę nad nim. Zobaczmy na prosty przykład. Załóżmy, iż potrzebujesz pola tekstowego, które akceptuje jedynie cyfry szesnastkowe. Jeśli zastosujesz proste pole edycji, to nie będziesz miał nic do powiedzenia, gdy twój użytkownik wpisze coś innego od cyfr szesnastkowych w to pole edycji, tj. jeśli wpisze on "zb+g", to nic nie możesz zrobić, a jedynie odrzucić cały tekst. Mało profesjonalne rozwiązanie. W istocie potrzebujesz możliwości zbadania wprowadzanych przez użytkownika znaków do pola edycyjnego w momencie naciskania przez niego klawiszy.

Teraz zbadajmy, jak tego dokonać. Gdy użytkownik wpisuje cokolwiek do pola edycyjnego, system Windows wysyła wiadomość WM_CHAR do procedury okna tej kontrolki. Ta procedura okna przebywa wewnątrz samego Windows, zatem nie możemy jej zmienić. Jednakże możemy dokonać przekierowania przepływu wiadomości do naszej własnej procedury okna. W ten sposób nasza procedura okna będzie z pierwszej ręki otrzymywała wszystkie wiadomości wysyłane przez system Windows do kontrolki edycyjnej. Jeśli zdecyduje się ona podjąć jakieś działania przy danej wiadomości, to może to bez problemów robić. Lecz jeśli nie chce obsługiwać danej wiadomości, to może przekazać ją do właściwej procedury okna. I tak nasza procedura okna wstawia się pomiędzy Windows a kontrolkę edycji. Spójrz na poniższy schemat:

 

Przed przejęciem procedury okna

System Windows procedura okna kontrolki edycji

Po przejęciu procedury okna

System Windows ® nasza procedura okna ® procedura okna kontrolki edycji

 

Teraz skoncentrujemy się nad sposobem przejmowania procedury okna. Zapamiętaj, iż nie jest to ograniczone do kontrolek, może być zastosowane z każdym oknem.

Zastanówmy się, skąd system Windows wie, gdzie znajduje się procedura okna kontrolki edycyjnej. Jakieś pomysły?... Pole lpfnWndProc struktury WNDCLASSEX. Jeśli moglibyśmy podmienić zawartość tego pola z adresem naszej własnej procedury okna, to system Windows wysyłałby wiadomości do nas, a nie do kontrolki edycyjnej.

Możemy to zrobić wywołując funkcję SetWindowLong.

 

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

Zatem mamy proste zadanie: tworzymy procedurę okna, która obsłuży wiadomości dla kontrolki edycji, a następnie wywołujemy funkcję SetWindowLong ze znacznikiem GWL_WNDPROC przekazując w trzecim parametrze adres naszej procedury okna. Jeśli funkcja się powiedzie, to wartością zwrotną jest 32-bitowa zawartość zamienionego pola struktury WNDCLASSEX, w naszym przypadku jest to adres oryginalnej procedury okna kontrolki. Musimy zapamiętać tę wartość do użytku z naszą procedurą okna.

Pamiętaj, iż pojawi się kilka wiadomości, których nie chcemy obsługiwać i przekażemy je do oryginalnej procedury okna. Możemy tego dokonać wywołując funkcję CallWindowProc:

 

CallWindowProc PROTO lpPrevWndFunc: DWORD, \ 
                     hWnd:          DWORD,\ 
                     Msg:           DWORD,\
                     wParam:        DWORD,\
                     lParam:        DWORD

Pozostałe cztery parametry są parametrami przekazanymi do naszej procedury okna. Po prostu przesyłamy je dalej do CallWindowProc.

Przykład

Plik SUBCLASS.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

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

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

.DATA

ClassName DB "SubclassWinClass", 0
AppName   DB "Przejmowanie procedury okna", 0
EditClass DB "EDIT", 0
Message   DB "W oknie edycyjnym nacisnąłeś klawisz Enter!", 0

.DATA?

hInstance  HINSTANCE ?
hwndEdit   DD ?
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,\
                           ADDR ClassName, ADDR AppName,\
                           WS_OVERLAPPED + WS_CAPTION + \
                           WS_SYSMENU + WS_MINIMIZEBOX + \
                           WS_MAXIMIZEBOX + WS_VISIBLE, CW_USEDEFAULT,\
                           CW_USEDEFAULT, 350, 100, 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 hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    .IF uMsg==WM_CREATE
        INVOKE CreateWindowEx, WS_EX_CLIENTEDGE,\
                               ADDR EditClass, NULL,\
                               WS_CHILD + WS_VISIBLE + WS_BORDER,\
                               20, 20, 300, 25, hWnd, NULL,\
                               hInstance, NULL
        mov    hwndEdit, eax
        INVOKE SetFocus, eax

;-----------------------------------------
; Przejmujemy procedurę okna
;-----------------------------------------

        INVOKE SetWindowLong, hwndEdit, GWL_WNDPROC, ADDR EditWndProc
        mov    OldWndProc, eax
    .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
        .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

 

Za pomocą EditWndProc filtrujemy wiadomości WM_CHAR. Jeśli znak jest pomiędzy 0...9 lub a...f, akceptujemy go przekazując wiadomość oryginalnej procedurze okna. Jeśli znak jest małą literą, zamieniamy go na dużą literę dodając do jego kodu wartość szesnastkową 20h. Zwróć uwagę, iż jeżeli znak nie jest taki, jak oczekujemy, to ignorujemy go. Nie przekazujemy go do oryginalnej procedury okna. Zatem gdy użytkownik wpisze coś innego niż 0...9 lub a...f, to znak ten po prostu nie pojawi się w kontrolce edycyjnej.

 

    .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
        .ELSE
            INVOKE CallWindowProc, OldWndProc, hEdit, uMsg, wParam, lParam
            ret
        .ENDIF

 

Chcę dalej zademonstrować potęgę przejmowania procedury okna przy przechwytywaniu klawisza Enter. EditWndProc sprawdza wiadomość WM_KEYDOWN na klawisz VK_RETURN (klawisz Enter). Jeśli tak jest, to wyświetla okno wiadomości z napisem "W oknie edycyjnym nacisnąłeś klawisz Enter!". Jeśli nie jest to klawisz Enter, to przekazuje wiadomość do oryginalnej procedury okna.

Możesz wykorzystać przejmowanie procedury okna do przejęcia kontroli nad innymi oknami. Jest to jedna z potężnych technik, które powinny się znaleźć w twoim arsenale programisty.

 

Dodatek w Pascalu

Ta sama aplikacja w Pascalu:

 

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

program SubClassing;

uses Windows;

const
  ClassName = 'SubclassWinClass';
  AppName   = 'Przejmowanie procedury okna';
  EditClass = 'EDIT';
  Message   = 'W oknie edycyjnym nacisnąłeś klawisz Enter!';

type

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

var

  hInstance  : HINST;
  hwndEdit   : longword;
  OldWndProc : longword;

function EditWndProc(hEdit:DWORD;uMsg:DWORD;wParam:DWORD;lParam:DWORD):DWORD;
var
  c : char;
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
        CallWindowProc(FOldWndProc(OldWndProc),hEdit,uMsg,ord(c),lParam);
    end;
    WM_KEYDOWN:
      if (wParam and $ff) = VK_RETURN then
      begin
        MessageBox(hEdit,Message,AppName,MB_OK + MB_ICONINFORMATION);
        SetFocus(hEdit);
      end
      else Result := CallWindowProc(FOldWndProc(OldWndProc),hEdit,uMsg,wParam,lParam);
    else Result := CallWindowProc(FOldWndProc(OldWndProc),hEdit,uMsg,wParam,lParam);
  end;
end;

function WndProc(hWnd:HANDLE;uMsg:UINT;wParam:WPARAM;lParam:LPARAM) : longint;
begin
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
      hwndEdit := CreateWindowEx(WS_EX_CLIENTEDGE,EditClass,0,
                  WS_CHILD + WS_VISIBLE + WS_BORDER,20,20,300,25,
                  hWnd,0,hInstance,0);
      SetFocus(hwndEdit);

//-----------------------------------------
// Przejmujemy procedurę okna
//-----------------------------------------

      OldWndProc := SetWindowLong(hwndEdit,GWL_WNDPROC,longint(@EditWndProc));
    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,ClassName,AppName,
          WS_OVERLAPPED + WS_CAPTION + WS_SYSMENU + WS_MINIMIZEBOX +
          WS_MAXIMIZEBOX + WS_VISIBLE,CW_USEDEFAULT,CW_USEDEFAULT,
          350,100,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.