Komunikaty

Adam Boduch

W tym artykule zajmę się omawianiem komunikatów. Jest to dosyć ważne w <wiki href="programowanie">programowaniu</wiki>. Być może sprawi Ci to trochę problemów, ale nie załamuj się - z czasem zrozumiesz o co chodzi :) Więc nie zniechęcaj się i zamiast wcisnąć "Back" w swojej przeglądarce przeczytaj ten artykuł do końca.

My tego nie widzimy, ale Windows ciągle generuje komunikaty. Jeżeli naciskasz przycisk myszy do systemu dociera komunikat że Ty właśnie nacisnąłeś właśnie przycisk. Windows rozpoznaje ok. 700 komunikatów. Wszystko to co robisz to komunikaty. Jeżeli ruszasz myszką - jest to komunikat; jeżeli naciskasz klawiszem także. Na samym początku możesz nie rozumieć sensu zastosowania komunikatów dlatego, że Delphi upraszcza to w znacznym stopniu. Zauważ, że jeżeli w Inspektorze Obiektów klikniesz na zakładkę Events to ujrzysz listę zdarzeń. Te zdarzenia to w rzeczywistości komunikaty. Weźmy za przykład zdarzenie OnKeyPress. Zdarzenie to generowane jest w chwili naciśnięcia przez użytkownika jakiegoś klawisza. W rzeczywistości jest to komunikat WM_CHAR. Tak, takie są nazwy komunikatów :) Przykładziki:

Komunikat Opis

WM_PAINT Okno zostało odświeżone.
WM_ACTIVATE Okno stało się aktywne.
WM_CLOSE Występuje podczas zamykania okna.
WM_QUIT Cały program powinien zostać zakończony.
WM_LBUTTONDOWN Lewy przycisk myszy został naciśnięty.

Najpierw podam przykład przechwycenia komunikatu, a później sami wyślemy jakiś komunikat. Może na początek komunikat WM_CHAR:

  1. Do sekcji "private" dodaj linię:
procedure WMChar(var Msg: TMessage); message WM_CHAR; 

Taka struktura jest specyficzna dla komunikatów. W nawiasie znajduje się zmienna typu "Message". Omówię ją później. Najważniejsza jest ostatnia część. Po słowie "Message" następuje nazwa komunikatu - w tym wypadku jest to WM_CHAR, który to komunikat jest generowany w chwili naciśnięcia przez użytkownika przycisku. Ok, teraz należy napisać kod obsługujący ten komunikat. Najedź kursorem myszy na nazwę procedury i wciśnij klawisze: Ctrl + Shift + C - procedura zostanie dodana do sekcji "Implementation". Wpisz taki kod do procedury:

ShowMessage('Naciśnięto klawisz...'); 

Nic nadzwyczajnego - po prostu wyświetlana jest informacja o naciśnięciu klawisza. Teraz możesz uruchomić program i sprawdzić jego działanie.
Teraz najważniejsze pytanie: po co właściwie stosować komunikaty skoro mam odpowiednie zdarzenia?
Odpowiedzi jest kilka:

czasem zaistnieje potrzeba kontrolowania zdarzenia, którego nie ma na karcie Events Inspektora Obiektów. ( przykład dalej ).

gdy zaistnieje potrzeba poinformowania o czymś innego programu.

ta umiejętność przyda Ci się w przyszłości ( choćby podczas programowania w WinAPI ).

Dobra, teraz wyślemy jakiś komunikat. Spis wszystkich komunikatów możesz znaleźć w pliku "Messages.pas" w katalogu z Delphi.
Do wysyłania komunikatów służą dwie funkcje API: SendMessage i PostMessage. Różnica pomiędzy nimi jest jedna - SendMessage zwraca rezultat działania funkcji. Po wysłaniu komunikatu czeka ona na jego wykonanie, a następnie zwraca rezultat. Jeżeli jest to cyfra -1 to komunikat został wysłany pomyślnie - jeżeli różna od zera to wystąpił jakiś błąd. Funkcja PostMessage natomiast po wysłaniu komunikatu nie czeka na jego zakończenie tylko automatycznie powraca do aplikacji.

No dobra teraz trzeba wysłać jakiś komunikat. Oto przykład zamknięcia naszej aplikacji poprzez wysłanie komunikatu:

PostMessage(Handle, WM_QUIT, 0, 0);

Pierwszym parametrem tej funkcji jest tzw. uchwyt okna. Każda kontrolka, czy okno posiada swój uchwyt. Trzeba go określić podczas przesyłania komunikatu. W naszym przypadku jest to główna forma naszego formularza. Kolejnym parametrem jest nazwa samego komunikatu, a dwa ostatnie to dodatkowe parametry. Jeżeli powyższy kod "podepniesz" pod przycisk to jego naciśnięcie spowoduje zamknięcie programu.
Równie dobrze możesz w ten sposób zamykać inne okna:

var
  H : THandle; // zmienna przechowująca uchwyt
begin
// szukanie okna...
  H := FindWindow(nil, 'Pad 1.3 - instalacja');
// wysłanie komunikatu
  PostMessage(H, WM_QUIT, 0, 0);

W tym przypadku nastąpiło zamknięcie innego programu. Do tego potrzebny był nam uchwyt tegoż okna. Zdobyliśmy go poprzez użycie funkcji "FindWindow". Pierwszym parametrem tej funkcji jest klasa ( ponieważ jej nie znamy wpisujemy znak pusty nil ), a drugim jest nazwa okna ( text na pasku okna ). Gdy już zdobyliśmy uchwyt można wysłać komunikat.
Komunikaty można wysyłać do różnych innych komponentów. Przykładowo komponent "Memo" nie posiada procedury "Undo" ( Cofnij ). Trzeba do komponentu wysłać komunikat, który spowoduje cofnięcie ostatniej operacji. Oto przykład:

 PostMessage(Memo1.Handle, WM_UNDO, 0, 0);

Tak jak mówiłem - każdy komponent ma swój uchwyt - komponent "Memo" także posiada uchwyt o nazwie "Handle". W tym wypadku do komponentu wysłany został komunikat "WM_UNDO", który nakazuje mu cofnięcie ostatniej operacji.

Na początku tego artykułu napisałem, że podam przykład wykorzystanie komunikatów. Czy nie fajnie by było gdyby komponent "Label" mógł działać jak odnosnik? Tzn., po najechaniu kursorem myszy na niego zmieniał by kolor czcionki, a po usunięciu kursora powracałby do poprzedniego? Fajnie by było, nie? Można to zrobić korzystając z komunikatów. Trzeba bowiem kontrolować wejście i wyjście kursora myszy w obszar komponentu. Dokonują tego komunikaty: CM_MOUSEENTER i CM_MOUSELEAVE. Komponenty "Label" nie mają na karcie Events w Inspektorze Obiektów obsługi tych zdarzeń ( a widzisz? Kolejny powód dla używania komunikatów ). Trzeba teraz napisać nową KLASĘ, która dziedziczyła by wszystkie właściwości komponentu "Label", a oprócz tego zawierała dwa dodatkowe komunikaty.
W swoim programie pod sekcją "uses" dodaj takie linie:

type
  TCss = class(TLabel)  // nowa klasa - nowy Label
    protected
// komunikaty przechwytujące wejście i wyjście kursora w  
// obszar komponentu
       procedure CMMouseEnter(var Msg:TMessage);
            message cm_MouseEnter;
       procedure CMMouseLeave(var Msg:TMessage);
            message cm_MouseLeave;
    public
      // procedura klikniecia na Labela
       procedure MyOnClick(Sender: TObject); 
   end;

Właśnie stworzyłeś nową klasę dziedziczącą z komponentu "Label". Posiada ona dodatkowo dwa komunikaty obsługujące wejście i wyjście kursora w obszar komponentu. Zauważ, że są one zawarte w sekcji "protected". Teraz w sekcji "Implementation" dodaj deklaracje tych komunikatów:

{ TCss }

procedure TCss.CMMouseEnter(var Msg: TMessage);
begin
  Font.Color := clRed;
  Font.Style := Font.Style - [fsUnderline];
end;

procedure TCss.CMMouseLeave(var Msg: TMessage);
begin
  Font.Color := clBlue;
  Font.Style := Font.Style + [fsUnderline];
end;

Pierwszy komunikat wejścia kursora w obszar komponentu powoduje dodanie podkreślenia do komponentu i zmianę koloru czcionki na niebieski. Drugi komunikat przywraca domyślną czcionkę. Pod tymi procedurami dodaj jeszcze jedną, która będzie uruchamiana w chwili kliknięcia w komponent "Label":

procedure TCss.MyOnClick(Sender: TObject);
begin
ShellExecute(0, 'open', PCHar(Caption), nil, nil, 
SW_SHOWMAXIMIZED);
end;

Do listy uses dodaj jeszcze słowo "ShellAPI". Ta funkcja powoduje otwarcie przeglądarki i wczytanie adresu, który jest zapisany w "Labelu". Zaraz pod słowem "Implementation" dodaj linię:

var
  Css : TCss;

Jest to zmienna, która wskazuje na nasz nowy komponent ( a dokładniej klasę ). Teraz w sekcji "private" klasy formularza [NIE "CSS" ( cały kod programu podany jest niżej! )] dodaj takie linie:


  procedure WMCreate(var Msg: TMessage); message WM_CREATE;
  procedure WMDestroy(var Msg: TMessage); message WM_DESTROY;

To są komunikaty WM_CREATE ( występuje podczas tworzenia formy ) i WM_DESTROY ( występuje podczas niszczenia formy ). A więc uzupełnij kod tych komunikatów w ten sposób:

procedure TForm1.WMCreate(var Msg: TMessage);
begin
  Css := TCss.Create(Self); // stworzenia klasy
  with Css do // instrukcja wiążąca
  begin
    Parent := Self;
  // wlasciwosc "Caption"
    Caption := 'http://www.4programmers.net';
  // pozycja Label'a
    Left := 100;
    Top := 100;
    Font.Color := clBlue; // kolor czcionki
    Cursor := crHandPoint; // kursor
  // procedura klikniecia na Label'a
    OnClick := MyOnClick;
  // dodaj podkreslenie
    Font.Style := Font.Style + [fsUnderline];
 end;
end;
procedure TForm1.WMDestroy(var Msg: TMessage);
begin
  Css.Free; // zwolnij pamiec
end;

Tym sposobem zakończyliśmy pisanie programu. Napisaliśmy program nie kładąc żadnego komponentu! Takie tworzenie programu nazywa się tworzeniem dynamicznym gdyż w komunikacie "WM_CREATE" następuje stworzenie Label'a, a dokładniej klasy "TCss", który dziedziczy z komponentu "Label". Oto cały kod programu ( uwaga! kod może się nie kompilować w niższych wersjach Delphi ).

unit Unit1;
{ Copyright (c) 2001 by Adam Boduch }
{ E-mail: [email protected]         }
{ http://www.4programmers.net    }

interface


uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, ShellAPI; { <- ważny moduł }

type
  TCss = class(TLabel)  // nowa klasa - nowy Label
    protected
  // komunikaty przechwytujace wejscie i 
  // wyjscie kursora w obszar komponentu
       procedure CMMouseEnter(var Msg:TMessage);
          message cm_MouseEnter;
       procedure CMMouseLeave(var Msg:TMessage);
          message cm_MouseLeave;
    public
   // procedura klikniecia na Labela
       procedure MyOnClick(Sender: TObject); 
   end;

type
  TForm1 = class(TForm)
  private
// komunikaty wystepujace podczas tworzenia i
// niszczenia formy
    procedure WMCreate(var Msg: TMessage); message WM_CREATE;
    procedure WMDestroy(var Msg: TMessage); message WM_DESTROY;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

var
  Css : TCss;

{$R *.DFM}

{ TCss }

procedure TCss.CMMouseEnter(var Msg: TMessage);
begin
  Font.Color := clRed;
  Font.Style := Font.Style - [fsUnderline];
end;

procedure TCss.CMMouseLeave(var Msg: TMessage);
begin
  Font.Color := clBlue;
  Font.Style := Font.Style + [fsUnderline];
end;

procedure TCss.MyOnClick(Sender: TObject);
begin
ShellExecute(0, 'open', PCHar(Caption), nil, nil, 
SW_SHOWMAXIMIZED);
end;

procedure TForm1.WMCreate(var Msg: TMessage);
begin
  Css := TCss.Create(Self); // stworzenia klasy
  with Css do // instrukcja wiążąca
  begin
    Parent := Self;
  // wlasciwosc "Caption';
    Caption := 'http://www.4programmers.net';
  // pozycja Label'a
    Left := 100;
    Top := 100;
    Font.Color := clBlue; // kolor czcionki
    Cursor := crHandPoint; // kursor
  // procedura klikniecia na Label'a
    OnClick := MyOnClick;
  // dodaj podkreślenie
    Font.Style := Font.Style + [fsUnderline];
 end;
end;

procedure TForm1.WMDestroy(var Msg: TMessage);
begin
  Css.Free; // zwolnij pamięć
end;

end.

Ty także możesz przekazywać swoje komunikaty. W sekcji "Interface" dodaj takie linie:

const
  Moj_Komunikat = WM_USER + 100;

Właśnie zadeklarowałeś swój komunikat. Komunikat deklaruje się podając nazwę WM_USER + numer komunikatu. To Ty określasz ten numer. Jeżeli deklarujesz kilka swoich komunikatów każdy z nich musi mieć inny numer. Górna granica tych numerów to ponad 30000. Najlepiej jest stosować numery powyżej 100 by uniknąć kolizji komunikatów. Teraz do sekcji "private" dodaj taką linię:

 procedure MojKomunikat(var Msg: TMessage); message Moj_Komunikat;
W tej linii obsługiwany będzie właśnie nasz zadeklarowany komunikat. Teraz uzupełnij tę procedurę w podany poniżej sposób:

procedure TForm1.MojKomunikat(var Msg: TMessage);
var
  S : String;
begin
  S := Format('Błąd wystąpił w linii: %d, w znaku: %d',
  [Msg.WParam, Msg.LParam]);

  MessageDlg(S, mtInformation, [mbOK], 0);
end;

W tym wypadku po otrzymaniu komunikatu wyświetlone jest okienko. Zmienna "S" przechowuje tekst. Nie będę tutaj omawiał funkcji "Format" gdyż nie ma ona znaczenia dla działania programu. W tym komunikacie pobierane są parametry Msg.WParam i Msg.LParam. Tak jak mówiłem wcześniej są to dodatkowe parametry.
Teraz możesz wysłać już swój komunikat:

Perform(Moj_Komunikat, 20, 200);

Kilka nowych komunikatów

Ostatnio szukałem informacji na temat edycji parametrów okna i chcę się podzielić swoimi odkryciami, a mianowicie:

Powiesz: "Zaraz, a gdzie funkcja PostMessage"?". No tak zapomniałem powiedzieć. Istnieje jeszcze jedna funkcja do przesyłania komunikatów - "Perform". Nadaje się ona jednak tylko do przesyłania komunikatów w obrębie programu i tylko do komponentów znajdujących się w programie. Zauważ, że nie posiada ona jedngo parametru jakim jest uchwyt okna. Po prostu pierwszym parametrem jest nazwa komunikatu, a dwa pozostałe to dodatkowe parametry "WParam" i "LParam". Najlepiej będzie jeżeli uruchomić program i sam sprawdzisz działanie tego programu.

<font size="3">Przeciąganie okna na dowolnym komponencie:</span>

Dodajemy do OnMouseDown Furmułę:

ReleaseCapture;

SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, 0);

W tym momencie podczas naciśnięcia dowolnego klawisza myszy okno zostanie przeciągnięte, jak widzicie jest to trochę bez sensu, najlepiej będzie gdy tylko lewy przycisk myszki będzie wywoływał ten komunikat, więc poprawiamy kod:

If Button = mbLeft then
begin
   ReleaseCapture;
   SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, 0);
end;

Aby rozciągać formę mamy do dyspozycji:

HTLEFT
HTRIGHT
HTTOP
HTTOPLEFT
HTTOPRIGHT
HTBOTTOM
HTBOTTOMLEFT
HTBOTTOMRIGHT

Powyższe komunikaty wprowadzamy do pustego miejsca:
SendMessage(Handle, WM_NCLBUTTONDOWN, , 0);

4 komentarzy

public
procedure WMSysCommand
(var Msg: TWMSysCommand) ;
message WM_SYSCOMMAND;

procedure TForm1.WMSysCommand;
begin
if (Msg.CmdType = SC_MINIMIZE) or
(Msg.CmdType = SC_MAXIMIZE) then
MessageBeep(0) ;

DefaultHandler(Msg) ;
end;

Może przyda się komuś innemu

W D7 jest coś takiego WM_SYSCOMMAND (w odpowiedzi jednym z typów komunikatów jest "minimize") niestety nie działa mi jeżeli komunikat "MINIMIZE" moja aplikacja otrzyma od innej np. od "Pokaż pulpit".

Warto powiedzieć, że lista komunikatów znajduje się w module Messages.pas

Gdzie znaleść listę komuniatów. (Miałem problem z obsłużeniem minimalizacji okna "WM_MINIMIZED" nie istnieje w delphi)