Rozdział13
Komponenty międzyplatformowe
Trzy poprzednie rozdziały poświęcone były architekturze i projektowaniu komponentów VCL, przeznaczonych dla platformy MS Windows. W niniejszym rozdziale zajmiemy się podstawami projektowania komponentów CLX, umożliwiających tworzenie aplikacji zarówno dla Windows, jak i dla Linuksa. Tak się szczęśliwie składa, iż znaczna część umiejętności nabyta podczas projektowania komponentów VCL okaże się przydatna również w odniesieniu do komponentów CLX.
CLX — co to jest?
CLX — wymawiane najczęściej jako „clicks” — to akronim od Component Library for Cross-Platform, czyli „międzyplatformowej biblioteki komponentów”; pojawił się po raz pierwszy w związku z Kyliksem — wywodzącym się z Delphi narzędziem typu RAD dla Linuksa. Biblioteka CLX jest jednak czymś więcej niż tylko adaptacją VCL na gruncie Linuksa: jej obecność w Delphi 6 umożliwia (po raz pierwszy w historii Delphi) przekroczenie granic Windows i tworzenie aplikacji zgodnych zarówno z Delphi, jak i Kyliksem.
Ponadto biblioteka VCL (w Delphi) utożsamiana bywa raczej z komponentami wizualnymi (również w nazwie), co nie powinno dziwić wobec faktu, iż komponenty te stanowią większą jej część (i jednocześnie lwią część palety komponentów). Tymczasem architektura CLX jest nieco bardziej złożona, bo oprócz wizualnych komponentów grupy VisualCLX zawiera także komponenty BaseCLX, DataCLX i NetCLX.
BaseCLX to grupa klas i modułów wspólnych dla Delphi 6 i Kyliksa — należą do niej m.in. moduły System, SysUtils i Classes, określane w Delphi (od początku) mianem biblioteki RTL. Mimo iż moduły te stanowić mogą składniki aplikacji obydwu typów — VCL i CLX — za aplikację CLX zwykło się uważać taką, której strona wizualna zrealizowana została na podstawie klas grupy VisualCLX.
VisualCLX stanowi odmianę tego, co większość programistów skłonna jest uważać (w Delphi) za VCL, jednak oparta jest nie na standardowych kontrolkach Windows z bibliotek User32.DLL czy ComCtl32.DLL, lecz na tzw. widżetach zawartych w bibliotece Qt. Do grupy DataCLX zaliczają się komponenty zapewniające dostęp do danych za pomocą nowej technologii dbExpress, natomiast nowe, międzyplatformowe oblicze WebBrokera ucieleśniają komponenty grupy NetCLX.
W niniejszym rozdziale skoncentrujemy się głównie na VisualCLX, ze szczególnym uwzględnieniem tworzenia nowych komponentów na bazie tej architektury. Opiera się ona (jak już wcześniej wspominaliśmy) na bibliotece Qt („cute”) firmy Troll Tech, stanowiącej niezależną od konkretnej platformy bibliotekę klas C++, realizujących funkcjonalność widżetów składających się na interfejs użytkownika. Ściślej — w chwili obecnej biblioteka Qt zawiera elementy charakterystyczne dla środowisk MS Windows oraz X Window System, może więc być wykorzystywana zarówno na potrzeby aplikacji windowsowych, jak i linuksowych; na jej podstawie zrealizowano właśnie linuksowy menedżer okien KDE.
Biblioteka Qt nie jest bynajmniej jedyną dostępną międzyplatformową biblioteką klas; to, iż Borland zdecydował się właśnie na nią, wynika z kilku istotnych przyczyn. Po pierwsze, jej klasy podobne są w dużym stopniu do klas komponentów VCL — na przykład ich właściwości zrealizowane zostały z udziałem metod dostępowych Getxxxx/Setxxxx, po drugie — wykorzystują podobny do VCL mechanizm powiadamiania o zdarzeniach (tzw. sygnały). Wreszcie po trzecie — jej widżety to nic innego jak standardowe kontrolki interfejsu użytkownika, spełniające tę samą rolę co standardowe kontrolki Windows. To wszystko pozwoliło na stworzenie komponentów biblioteki CLX przez „nadbudowanie” pascalowych otoczek wokół gotowych widżetów — zamiast budowania całej architektury „od zera”.
Architektura CLX
Jak przed chwilą stwierdziliśmy, VisualCLX jest grupą klas Object Pascala zbudowanych na bazie funkcjonalności widżetów biblioteki Qt — co stanowi analogię do komponentów VCL zbudowanych na bazie standardowych kontrolek Windows i biblioteki Windows API. Podobieństwo to nie jest bynajmniej przypadkowe, lecz wynika z jednego z celów projektowych: łatwości przystosowywania istniejących aplikacji VCL do architektury CLX. Rysunki 13.1 i 13.2 przedstawiają hierarchiczną strukturę klas w obydwu tych środowiskach; przyciemnione prostokąty na rysunku 13.1 wyróżniają podstawowe klasy biblioteki VCL.
Już na pierwszy rzut oka widać różnicę pomiędzy obydwiema hierarchiami — w architekturze CLX pojawiły się nowe (w stosunku do VCL) klasy, niektóre zostały przesunięte do innych gałęzi. Różnice te zaznaczone zostały na rysunku 13.2 za pomocą rozjaśnionych prostokątów. I tak, na przykład, komponent zegarowy (TTimer) nie wywodzi się już bezpośrednio z klasy TComponent, lecz z nowej klasy THandleComponent, stanowiącej klasę bazową do obsługi wszystkich tych przypadków, gdy komponent niewizualny wymaga dostępu do uchwytu (handle) jakiejś kontrolki Qt. Innym przykładem jest etykieta TLabel, nie będąca już kontrolką graficzną, lecz wywodząca się z klasy TFrameControl, która wykorzystuje różnorodne możliwości kształtowania obrzeża widżetów Qt.
Rysunek 13.1. Hierarchia klas VCL
Nieprzypadkowe jest także podobieństwo nazw klas bazowych kontrolek wizualnych — TWinControl (VCL) i TWidgetControl (CLX): charakterystyczny dla Windows człon „Win” ustąpił miejsca charakterystycznemu dla CLX „Widget”. Mając na względzie łatwość przenoszenia kodu źródłowego Borland zdefiniował klasę TWinControl także w bibliotece CLX, stanowi ona jednak tylko synonim klasy TWidgetControl. Można było oczywiście uniknąć tej nieco mylącej w skutkach (zwłaszcza dla nieświadomego użytkownika) operacji i utworzyć dwa oddzielne moduły dla obydwu grup kontrolek, a później odróżniać je za pomocą symboli kompilacji warunkowej; utrudniłoby to jednak przenoszenie kodu źródłowego (identyfikator TWinControl straciłby rację bytu, a jego systematyczna zmiana na TWidgetControl wymagałaby dodatkowej fatygi), zaś w aplikacjach międzyplatformowych konieczne byłoby utrzymywanie dwóch identyfikatorów na oznaczenie klasy bazowej kontrolek.
Notatka
Zwróć uwagę, iż utrzymywanie w pojedynczym pliku kodu dla obydwu typów komponentów (VCL i CLX) jest czymś jakościowo różnym od tworzenia kodu uniwersalnego komponentu CLX, dającego się użyć zarówno w Delphi 6, jak i w Kyliksie (takimi komponentami zajmiemy się w dalszej części rozdziału).
Rysunek 13.2. Hierarchia klas CLX
Na szczęście różnice przedstawione na rysunku 13.2 nie mają zbyt dużego znaczenia dla twórców aplikacji, ponieważ większość komponentów VCL posiada na gruncie CLX identycznie nazwane odpowiedniki. Nie mają jednak takiego szczęścia twórcy nowych komponentów — zmiany w hierarchii klas mają dla nich znaczenie zasadnicze.
Wskazówka
Strukturę hierarchii klas można łatwo zobaczyć za pomocą przeglądarki obiektów (Object Browser) w Delphi 6 i w Kyliksie; jednak ze względu na synonim TWinControl, uzyskamy dwie identyczne hierarchie (dla TWidgetControl i TWinControl).
Pomiędzy VCL i CLX istnieje jeszcze więcej podobieństw, których nie sposób uwzględnić na przedstawionych rysunkach. Na przykład znane z VCL płótno (Canvas) ma w CLX niemal identyczną naturę i wykorzystywane jest w bardzo zbliżony sposób, choć oczywiście różnice pomiędzy obydwoma środowiskami przesądzają o jego odmiennej implementacji: w VCL jest ono otoczką kontekstu urządzenia, zaś w CLX — analogicznego mechanizmu zwanego malarzem (painter), mimo to obydwa te mechanizmy reprezentowane są przez tę samą właściwość Handle. Ponadto, z uwagi na wymóg łatwości przenoszenia kodu, niemal identycznie wyglądają interfejsy komponentów w obydwu grupach — pod względem repertuaru właściwości publicznych (public) i publikowanych (published) oraz zdarzeń (OnClick, OnChange, OnKeyPress) i ich metod dyspozycyjnych (Click(), Change() i KeyPress()).
Z Windows do Linuksa
Mimo wielu podobieństw pomiędzy analogicznymi elementami komponentów VCL i CLX, istniejące między tymi środowiskami różnice dają znać o sobie tym wyraźniej, im bliższa staje się zależność konkretnego elementu od mechanizmu charakterystycznego tylko dla jednego ze środowisk. I tak, w środowisku Kyliksa traci sens większość odwołań do Win32 API; mechanizmy typowe jedynie dla Windows — jak np. MAPI — muszą być zastąpione równoważnymi mechanizmami linuksowymi, a używające ich komponenty nie nadają się po prostu do przeniesienia na platformę linuksową. Z kolei niektóre problemy rozwiązywane przez funkcje biblioteki RTL muszą być rozwiązane w inny sposób — przykładem może być czułość Linuksa na wielkość liter w nazwach plików; Pascal, niewrażliwy na wielkość liter w identyfikatorach, staje się pod Kyliksem wrażliwy na wielkość liter w nazwach modułów w dyrektywach uses!
Linux pozbawiony jest też wielu znanych z Windows mechanizmów systemowych — nie ma tu technologii COM, są jednak obsługiwane interfejsy; nie ma też dokowania okien, „dwukierunkowej” (bidirectional) obsługi tekstu, lokalizowania charakterystycznego dla krajów azjatyckich itp.
Pewnym problemem dla autorów aplikacji i komponentów jest istnienie oddzielnych modułów, dedykowanych tylko określonym platformom, na przykład kod kontrolek windowsowych znajduje się w pliku Controls.pas, zaś kod dla widżetów CLX — w pliku QControls.pas. Stwarza to możliwość „pomieszania” obydwu środowisk w sytuacji, gdy komponent CLX lub aplikacja przeznaczona dla Kyliksa opracowywane są w środowisku Delphi 6. Tak skonstruowany komponent, jeżeli zawiera elementy typowe wyłącznie dla VCL, będzie bez problemu pracował w Delphi 6, najczęściej jednak odmówi współpracy pod Kyliksem. Można uniknąć takiej sytuacji, gdy, za radą Borlanda, komponenty i aplikacje przeznaczone dla Kyliksa będziemy opracowywać pod Kyliksem — niestety, środowisko Kyliksa jest (w zgodnej opinii programistów) mniej komfortowe od Delphi 6.
Wskazówka
Wobec opisanych konsekwencji różnic pomiędzy VCL i CLX, nie wydaje się uzasadnione używanie komponentów CLX w aplikacjach przeznaczonych wyłącznie dla Windows.
Nie ma komunikatów…
Linux (a raczej — podsystem X Window) nie implementuje typowego dla Windows mechanizmu komunikatów; w efekcie nie do zaakceptowania jest w Kyliksie kod źródłowy odwołujący się do identyfikatorów w rodzaju wm_LButtonDown, wm_SetCursor czy wm_Char. Reagowaniem na zachodzące w systemie zdarzenia zajmują się w bibliotece Qt wyspecjalizowane klasy — dzieje się tak niezależnie od platformy systemowej, tak więc komponent CLX nie jest zdolny reagować na komunikaty nawet pod Windows; zamiast znanych z Delphi metod z klauzulą message (np. CMTextChanged()), powinien on korzystać z równoważnych metod dynamicznych (TextChanged()), co wyjaśnimy dokładniej w następnym punkcie.
Przykładowe komponenty
W niniejszym punkcie przyjrzymy się nieco dokładniej przykładom transformacji komponentów VCL na równoważne komponenty CLX. Na początek zajmiemy się popularnym „spinerem” — to pomocniczy komponent współpracujący najczęściej z polem edycyjnym, dokonujący jego automatycznej inkrementacji lub dekrementacji; realizuje on wiele interesujących mechanizmów (jak specyficzne rysowanie), współpracę z klawiaturą i myszą, przyjmowanie i utratę skupienia (focus) itp.
Trzy kolejne komponenty to pochodne bazowego spinera. Pierwszy z nich wzbogacony jest o obsługę myszy i wyświetlanie specyficznych kursorów już na etapie projektowania, drugi realizuje współpracę z listą obrazków (ImageList), trzeci natomiast współpracuje z kontrolką reprezentującą pole bazy danych.
Wskazówka
Wszystkie prezentowane w tym rozdziale moduły nadają się do wykorzystania zarówno w Delphi 6, jak i w Kyliksie.
Podstawa — komponent TddgSpinner
Rysunek 13.3 przedstawia trzy egzemplarze komponentu TddgSpinner na formularzu aplikacji CLX. W odróżnieniu od pionowo ułożonych strzałek, charakterystycznych dla windowsowego spinera, komponent ten posiada odpowiednie przyciski w układzie poziomym: inkrementujący i dekrementujący.
Rysunek 13.3. Komponent TddgSpinner pomocny przy wprowadzaniu liczb całkowitych
Wydruk 13.1 przedstawia kompletny kod źródłowy modułu QddgSpin.pas implementującego komponent TddgSpinner. Podobnie jak spiner windowsowy, wywodzi się on z klasy TCustomControl — tyle że w tym przypadku jest to klasa CLX i komponent może być używany zarówno w Windows, jak i pod Linuksem.
Choć migracja na platformę CLX rzadko wiąże się ze zmianą nazw komponentów, to jednak zasadą jest poprzedzanie nazwy modułu VCL literą Q dla podkreślenia zależności tegoż modułu od biblioteki Qt.
Notatka
Każdy z prezentowanych wydruków zawiera „wykomentowane” linie stanowiące część kodu VCL; komentarze oznaczone dodatkowo jako VCL -> CLX podkreślają elementy specyficzne dla przenoszenia komponentu z VCL do CLX.
Wydruk 13.1. QddgSpin.Pas — kod źródłowy komponentu TddgSpinner
{==================================================================
QddgSpin
Niniejszy moduł implementuje komponent TddgSpinner z biblioteki CLX,
zawierający poziomo ułożone przyciski zmieniające wprowadzaną wartość
(w odróżnieniu od windowsowego spinera posiadającego pionowo ułożone
strzałki). Komponent ten wywodzi się z CLX-owej wersji TCustomControl
i demonstruje wiele interesujących możliwości, jak przyjmowanie i utrata
skupienia, specyficzne rysowanie i współpracę z myszą
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgSpin;
interface
uses
SysUtils, Classes, Types, Qt, QControls, QGraphics;
(*
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
ImgList;
*)
type
TddgButtonType = ( btMinus, btPlus );
TddgSpinnerEvent = procedure (Sender: TObject; NewValue: Integer;
var AllowChange: Boolean ) of object;
TddgSpinner = class( TCustomControl )
private
// Pola komponentu
FValue: Integer;
FIncrement: Integer;
FButtonColor: TColor;
FButtonWidth: Integer;
FMinusBtnDown: Boolean;
FPlusBtnDown: Boolean;
// Wskaźniki do procedur zdarzeniowych
FOnChange: TNotifyEvent;
FOnChanging: TddgSpinnerEvent;
(*
// VCL->CLX: poniższe metody komunikacyjne nie są dostępne w CLX
// Obsługa komunikatu Windows
procedure WMGetDlgCode( var Msg: TWMGetDlgCode );
message wm_GetDlgCode;
// Obsługa komunikatu komponentu
procedure CMEnabledChanged( var Msg: TMessage );
message cm_EnabledChanged;
*)
protected
procedure Paint; override;
procedure DrawButton( Button: TddgButtonType; Down: Boolean;
Bounds: TRect ); virtual;
// Metody obsługi
procedure DecValue( Amount: Integer ); virtual;
procedure IncValue( Amount: Integer ); virtual;
function CursorPosition: TPoint;
function MouseOverButton( Btn: TddgButtonType ): Boolean;
// VCL->CLX: EnabledChanged zastępuje metodę komunikacjną
// cm_EnabledChanged
//
procedure EnabledChanged; override;
// Nowe metody obsługi zdarzeń
procedure Change; dynamic;
function CanChange( NewValue: Integer ): Boolean; dynamic;
// przedefiniowane metody obsługi zdarzeń
procedure DoEnter; override;
procedure DoExit; override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure MouseDown( Button: TMouseButton; Shift: TShiftState;
X, Y: Integer ); override;
procedure MouseUp( Button: TMouseButton; Shift: TShiftState;
X, Y: Integer ); override;
(*
// VCL->CLX: poniższe deklaracje zostały zmienione w CLX
function DoMouseWheelDown( Shift: TShiftState;
MousePos: TPoint ): Boolean; override;
function DoMouseWheelUp( Shift: TShiftState;
MousePos: TPoint ): Boolean; override;
*)
function DoMouseWheelDown( Shift: TShiftState;
const MousePos: TPoint ): Boolean; override;
function DoMouseWheelUp( Shift: TShiftState;
const MousePos: TPoint ): Boolean; override;
// Metody dostępowe właściwości
procedure SetButtonColor( Value: TColor ); virtual;
procedure SetButtonWidth( Value: Integer ); virtual;
procedure SetValue( Value: Integer ); virtual;
public
// Nie zapomnij o klauzuli override w konstruktorze
constructor Create( AOwner: TComponent ); override;
published
// Nowe deklaracje właściwości
property ButtonColor: TColor
read FButtonColor
write SetButtonColor
default clBtnFace;
property ButtonWidth: Integer
read FButtonWidth
write SetButtonWidth
default 18;
property Increment: Integer
read FIncrement
write FIncrement
default 1;
property Value: Integer
read FValue
write SetValue;
// Nowe deklaracje zdarzeń
property OnChange: TNotifyEvent
read FOnChange
write FOnChange;
property OnChanging: TddgSpinnerEvent
read FOnChanging
write FOnChanging;
// Odziedziczone właściwości i zdarzenia
property Color;
(*
property DragCursor; // VCL->CLX: właściwość niedostępna w CLX
*)
property DragMode;
property Enabled;
property Font;
property Height default 18;
property HelpContext;
property Hint;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property TabOrder;
property TabStop default True;
property Visible;
property Width default 80;
property OnClick;
property OnDragDrop;
property OnDragOver;
property OnEndDrag;
property OnEnter;
property OnExit;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnStartDrag;
end;
implementation
{===================================}
{== Metody komponentu TddgSpinner ==}
{===================================}
constructor TddgSpinner.Create( AOwner: TComponent );
begin
inherited Create( AOwner );
// Inicjacja pól
FButtonColor := clBtnFace;
FButtonWidth := 18;
FValue := 0;
FIncrement := 1;
FMinusBtnDown := False;
FPlusBtnDown := False;
// Inicjacja odziedziczonych właściwości
Width := 80;
Height := 18;
TabStop := True;
// VCL->CLX: TWidgetControl ustawia swój kolor na clNone
Color := clWindow;
// VCL->CLX: InputKeys zastępuje metodę obsługi komunikatu
// wm_GetDlgCode
InputKeys := InputKeys + [ ikArrows ];
end;
{== Metody dostępowe właściwości ==}
procedure TddgSpinner.SetButtonColor( Value: TColor );
begin
if FButtonColor <> Value then
begin
FButtonColor := Value;
Invalidate;
end;
end;
procedure TddgSpinner.SetButtonWidth( Value: Integer );
begin
if FButtonWidth <> Value then
begin
FButtonWidth := Value;
Invalidate;
end;
end;
procedure TddgSpinner.SetValue( Value: Integer );
begin
if FValue <> Value then
begin
if CanChange( Value ) then
begin
FValue := Value;
Invalidate;
// Wygeneruj zdarzenie Change
Change;
end;
end;
end;
{== Metody związane z wyświetlaniem ==}
procedure TddgSpinner.Paint;
var
R: TRect;
YOffset: Integer;
S: string;
XOffset: Integer; // VCL->CLX: dodane dla CLX
begin
inherited Paint;
with Canvas do
begin
Font := Self.Font;
Pen.Color := clBtnShadow;
if Enabled then
Brush.Color := Self.Color
else
begin
Brush.Color := clBtnFace;
Font.Color := clBtnShadow;
end;
// Wyświetl wartość
(*
// VCL->CLX: SetTextAlign niedostępne w CLX
SetTextAlign( Handle, ta_Center or ta_Top ); // funkcja GDI
*)
R := Rect( FButtonWidth - 1, 0,
Width - FButtonWidth + 1, Height );
Canvas.Rectangle( R.Left, R.Top, R.Right, R.Bottom );
InflateRect( R, -1, -1 );
S := IntToStr( FValue );
YOffset := R.Top + ( R.Bottom - R.Top -
Canvas.TextHeight( S ) ) div 2;
// VCL->CLX: Oblicz offset (niedostępna funkcja SetTextAlign)
XOffset := R.Left + ( R.Right - R.Left -
Canvas.TextWidth( S ) ) div 2;
(*
// VCL->CLX: Zmień wywołanie TextRect (niedostępna funkcja SetTextAlign)
TextRect( R, Width div 2, YOffset, S );
*)
TextRect( R, XOffset, YOffset, S );
DrawButton( btMinus, FMinusBtnDown,
Rect( 0, 0, FButtonWidth, Height ) );
DrawButton( btPlus, FPlusBtnDown,
Rect( Width - FButtonWidth, 0, Width, Height ) );
if Focused then
begin
Brush.Color := Self.Color;
DrawFocusRect( R );
end;
end;
end; {= TddgSpinner.Paint =}
procedure TddgSpinner.DrawButton( Button: TddgButtonType;
Down: Boolean; Bounds: TRect );
begin
with Canvas do
begin
if Down then // ustaw kolor tła
Brush.Color := clBtnShadow
else
Brush.Color := FButtonColor;
Pen.Color := clBtnShadow;
Rectangle( Bounds.Left, Bounds.Top,
Bounds.Right, Bounds.Bottom );
if Enabled then
begin
(*
// w CLX clActiveCaption ustawione jest na clActiveHighlightedText
Pen.Color := clActiveCaption;
Brush.Color := clActiveCaption;
*)
Pen.Color := clActiveBorder;
Brush.Color := clActiveBorder;
end
else
begin
Pen.Color := clBtnShadow;
Brush.Color := clBtnShadow;
end;
if Button = btMinus then // wyświetl przycisk dekrementujący
begin
Rectangle( 4, Height div 2 - 1,
FButtonWidth - 4, Height div 2 + 1 );
end
else // wyświetl przycisk inkrementujący
begin
Rectangle( Width - FButtonWidth + 4, Height div 2 - 1,
Width - 4, Height div 2 + 1 );
Rectangle( Width - FButtonWidth div 2 - 1,
( Height div 2 ) - (FButtonWidth div 2 - 4),
Width - FButtonWidth div 2 + 1,
( Height div 2 ) + (FButtonWidth div 2 - 4) );
end;
Pen.Color := clWindowText;
Brush.Color := clWindow;
end;
end; {= TddgSpinner.DrawButton =}
procedure TddgSpinner.DoEnter;
begin
inherited DoEnter;
// kontrolka przyjmuje skupienie - wyświetl ją ponownie,
// by ukazać sygnalizującą to ramkę
Repaint;
end;
procedure TddgSpinner.DoExit;
begin
inherited DoExit;
// kontrolka traci skupienie - wyświetl ją ponownie,
// by usunąć ramkę sygnalizującą skupienie
Repaint;
end;
// VCL->CLX: EnabledChanged zastępuje metodę obsługi
// komunikatu cm_EnabledChanged
procedure TddgSpinner.EnabledChanged;
begin
inherited;
// odśwież obraz kontrolki stosownie do jej stanu
Repaint;
end;
{== Metody obsługi zdarzeń ==}
{==================================================================
TddgSpinner.CanChange
Ta metoda obsługuje zdarzenie OnChanging; zwróć uwagę, iż jest ona
funkcją, nie procedurą jak w VCL. Wartość przypisywana zmiennej
Result ustalana jest domyślnie przed wywołaniem metody zdefiniowanej
przez użytkownika.
==================================================================}
function TddgSpinner.CanChange( NewValue: Integer ): Boolean;
var
AllowChange: Boolean;
begin
AllowChange := True;
if Assigned( FOnChanging ) then
FOnChanging( Self, NewValue, AllowChange );
Result := AllowChange;
end;
procedure TddgSpinner.Change;
begin
if Assigned( FOnChange ) then
FOnChange( Self );
end;
// zwróć uwagę, iż poniższe metody modyfikują właściwość Value,
// nie zaś bezpośrednio pole FValue
procedure TddgSpinner.DecValue( Amount: Integer );
begin
Value := Value - Amount;
end;
procedure TddgSpinner.IncValue( Amount: Integer );
begin
Value := Value + Amount;
end;
{== Metody współpracy z klawiaturą ==}
(*
// VCL->CLX: Poniższą metodę zastępuje przypisanie wartości do pola
// InputKeys (w konstruktorze)
procedure TddgSpinner.WMGetDlgCode( var Msg: TWMGetDlgCode );
begin
inherited;
Msg.Result := dlgc_WantArrows; // kontrolka zdolna jest obsługiwać
// klawisze strzałek
end;
*)
procedure TddgSpinner.KeyDown( var Key: Word; Shift: TShiftState );
begin
inherited KeyDown( Key, Shift );
// VCL->CLX: zmiana identyfikatorów w CLX
// przedrostek "vk_" zmienił się na "Key_"
case Key of
Key_Left, Key_Down:
DecValue( FIncrement );
Key_Up, Key_Right:
IncValue( FIncrement );
end;
end;
{== metody obsługi myszy ==}
function TddgSpinner.CursorPosition: TPoint;
begin
GetCursorPos( Result );
Result := ScreenToClient( Result );
end;
function TddgSpinner.MouseOverButton(Btn: TddgButtonType): Boolean;
var
R: TRect;
begin
// uzyskaj granice odpowiedniego przycisku
if Btn = btMinus then
R := Rect( 0, 0, FButtonWidth, Height )
else
R := Rect( Width - FButtonWidth, 0, Width, Height );
// czy kursor znajduje się w wyznaczonym obszarze?
Result := PtInRect( R, CursorPosition );
end;
procedure TddgSpinner.MouseDown( Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
inherited MouseDown( Button, Shift, X, Y );
if not ( csDesigning in ComponentState ) then
SetFocus; // przenieś skupienie na spiner
// tylko w czasie wykonania programu
if ( Button = mbLeft ) and
( MouseOverButton(btMinus) or MouseOverButton(btPlus) ) then
begin
FMinusBtnDown := MouseOverButton( btMinus );
FPlusBtnDown := MouseOverButton( btPlus );
Repaint;
end;
end;
procedure TddgSpinner.MouseUp( Button: TMouseButton;
Shift: TShiftState; X, Y: Integer );
begin
inherited MouseUp( Button, Shift, X, Y );
if Button = mbLeft then
begin
if MouseOverButton( btPlus ) then
IncValue( FIncrement )
else if MouseOverButton( btMinus ) then
DecValue( FIncrement );
FMinusBtnDown := False;
FPlusBtnDown := False;
Repaint;
end;
end;
function TddgSpinner.DoMouseWheelDown( Shift: TShiftState;
const MousePos: TPoint ): Boolean;
begin
inherited DoMouseWheelDown( Shift, MousePos );
DecValue( FIncrement );
Result := True;
end;
function TddgSpinner.DoMouseWheelUp( Shift: TShiftState;
const MousePos: TPoint ): Boolean;
begin
inherited DoMouseWheelUp( Shift, MousePos );
IncValue( FIncrement );
Result := True;
end;
end.
Jak widać, kod źródłowy modułu niewiele różni się od wersji w VCL. Mimo iż różnic jest niewiele, większość z nich jest jednak niezwykle istotna.
Po pierwsze, zwróć uwagę na nazewnictwo modułów: Qt, QControls i QGraphics; pojawił się też nowy moduł Types wspólny dla VCL i CLX.
Po drugie, mimo iż deklaracja klasy komponentu niewiele odbiega w swej postaci od wersji VCL — pod względem deklaracji pól i procedur zdarzeniowych — nie ma w niej metod obsługujących komunikaty, (wm_GetDlgCode i cm_EnableChanged). Kontrolka TControl (w CLX) zamiast wysyłać komunikat cm_EnableChanged przy zmianie jej właściwości Enabled, wywołuje po prostu (dynamiczną) metodę EnableChanged() — toteż do niej przeniesiona została treść wyeliminowanej metody komunikacyjnej.
Podczas tworzenia komponentu częstym problemem jest obsługa klawiszy strzałek (na klawiaturze); w przypadku komponentu TddgSpinner powodują one zmianę reprezentowanej przez komponent wartości. W bibliotece VCL informacja o zestawie klawiszy obsługiwanych przez kontrolkę przekazywana była za pomocą komunikatu wm_GetDlgCode; w CLX nie ma komunikatów i trzeba znaleźć równoważne rozwiązanie zastępcze. Tak się szczęśliwie składa, iż kontrolka TWidgetControl definiuje w tym celu właściwość InputKeys, której w konstruktorze przypisuje się stosowną wartość (i poszerza domyślny repertuar obsługiwanych klawiszy o klawisze strzałek).
Wynika stąd praktyczny wniosek, iż komponenty VCL używające raczej procedur zdarzeniowych (i metod zarządzających zdarzeniami) niż komunikatów łatwiej poddają się przenoszeniu na platformę CLX.
Po trzecie — w konstruktorze, oprócz ustawienia właściwości InputKeys, dokonywana jest korekta standardowego ustawienia koloru kontrolki. W VCL kontrolka TWinControl dziedziczy swój kolor od kontrolki macierzystej (co symbolizuje wartość clWindow), tymczasem w CLX konstruktor klasy TWidgetControl ustawia kolor kontrolki na clNone; konieczna jest więc zmiana tego koloru na clWindow.
Na początku niniejszego rozdziału stwierdziliśmy, iż umiejętności nabyte w trakcie projektowania komponentów VCL przydadzą się w dużym stopniu przy projektowaniu komponentów CLX. Istotnie — deklaracje właściwości, metody dostępowe, a nawet procedury zdarzeniowe nie różnią się zasadniczo od tych używanych przez komponenty VCL. Pewnym wyjątkiem w tym względzie jest metoda Paint(), wymagająca nieco więcej przeróbek.
Pierwszą przyczyną tego stanu rzeczy jest nieobecność w CLX funkcji SetTextAlign(), która w wersji VCL dokonywała wyśrodkowania wyświetlanego tekstu. Funkcja ta wymaga kontekstu urządzenia GDI, dostępnego w VCL pod właściwością Canvas.Handle i nieobecnego w CLX, gdzie wspomniana właściwość ma całkiem inne znaczenie — wskazuje na obiekt odpowiedzialny za wyświetlanie (painter). Odpowiednie położenie tekstu można jednak wyliczyć „ręcznie”, za pomocą dostępnych „geometrycznych” metod płótna.
Kolejna ingerencja związana jest z kolorem, w którym (domyślnie) zostałyby wyświetlone przyciski. Na obydwu platformach jest to kolor clActiveCaption, jednak w CLX wartość ta utożsamiana jest z clActiveHighlightedText (w module QGraphics.pas).
Wskazówka
Wszelkie operacje wykonywane na płótnie komponentu CLX poza jego metodą Paint() muszą być poprzedzone wywołaniem metody Canvas.Start(); po zakończeniu rysowania należy wywołać metodę Canvas.Stop().
Ostatnim z nieprzenośnych elementów VCL, przysparzającym czasem mnóstwa kłopotów, są kody wirtualnych klawiszy vk_xxxx, stanowiące część Windows API. CLX definiuje w ich miejsce całkowicie nowy zestaw stałych rozpoczynających się od przedrostka Key_. W przypadku naszego komponentu nie jest to jednak dużym problemem, ze względu na ubogi repertuar klawiszy (4) obsługiwanych w sposób specyficzny.
I tak oto uzyskaliśmy uniwersalny komponent, przydatny zarówno w Delphi 6, jak i w środowisku Kyliksa. Najbardziej spektakularnym aspektem tej uniwersalności jest możliwość użycia tego samego kodu źródłowego w obydwu środowiskach!
Interakcja ze środowiskiem — komponent TddgDesignSpiner
Jak widać, przenoszenie komponentu do środowiska CLX nie musi być wcale trudne (chociaż odkrycie właściwości InputKeys wymagało nieco wysiłku). Kiedy jednak przystąpimy do rozbudowy komponentu CLX, różnice pomiędzy VCL i CLX staną się bardzo wyraźne.
Wydruk 13.2 przedstawia kod źródłowy komponentu TddgDesignSpiner, pochodnego w stosunku do TddgSpinner. Na rysunku 13.4 widać wyraźnie, jak kursor myszy zmienia swój wygląd, gdy znajdzie się nad jednym z przycisków; rysunek 13.5 pokazuje natomiast zmianę reprezentowanej wartości na etapie projektowania (poprzez kliknięcie jednego z przycisków).
Rysunek 13.4. Zmiana wyglądu kursora wywołana przez komponent TddgDesignSpiner
Rysunek 13.5. Edycja właściwości komponentu TddgDesignSpiner na etapie projektowania aplikacji
Wydruk 13.2. QddgDsnSpn.Pas — kod źródłowy komponentu TddgDesignSpiner
{==================================================================
QddgDsnSpn
Niniejszy moduł implementuje komponent TddgDesignSpinner, wywodzący się
z TddgSpinner i dokonujący zmiany wyglądu kursora w czasie, gdy kursor
ten znajduje się nad jednym z przycisków. Możliwa jest ponadto zmiana
właściwości Value na etapie projektowania, poprzez kliknięcie przycisku
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgDsnSpn;
interface
uses
SysUtils, Classes, Qt, QddgSpin;
(*
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
ddgSpin;
*)
type
TddgDesignSpinner = class( TddgSpinner )
private
// VCL->CLX: Custom cursor stored in QCursorH field
FThumbCursor: QCursorH;
(*
// VCL->CLX: Obsługa kursorów i interakcja z IDE obsługiwane
// są w CLX zupełnie inaczej niż w VCL.
// Poniższy blok jest specyficzny dla VCL
FThumbCursor: HCursor;
// Obsługa komunikatu Windows
procedure WMSetCursor( var Msg : TWMSetCursor );
message wm_SetCursor;
// Obsługa komunikatu komponentu
procedure CMDesignHitTest( var Msg: TCMDesignHitTest );
message cm_DesignHitTest;
*)
protected
procedure Change; override;
// VCL->CLX: Poniższe metody są przedefiniowane w CLX
procedure MouseMove( Shift: TShiftState;
X, Y: Integer ); override;
function DesignEventQuery( Sender: QObjectH;
Event: QEventH ): Boolean; override;
public
constructor Create( AOwner: TComponent ); override;
destructor Destroy; override;
end;
implementation
(*
// VCL->CLX: CLX nie obsługuje zasobów kursorowych
{$R DdgDsnSpn.res} // przyłącza zasób zawierający kursor
*)
uses
Types, QControls, QForms; // VCL->CLX: moduły CLX
// VCL->CLX: Two arrays of bytes (one for the image and one for
// the mask) are used to represent custom cursors in CLX
// VCL->CLX: Poniższe dwie tablice reprezentują w CLX bitmapy kursora
//
const
Bits: array[0..32*4-1] of Byte = (
$00, $30, $00, $00, $00, $48, $00, $00,
$00, $48, $00, $00, $00, $48, $00, $00,
$00, $48, $00, $00, $00, $4E, $00, $00,
$00, $49, $C0, $00, $00, $49, $30, $00,
$00, $49, $28, $00, $03, $49, $24, $00,
$04, $C0, $24, $00, $04, $40, $04, $00,
$02, $40, $04, $00, $02, $00, $04, $00,
$01, $00, $04, $00, $01, $00, $04, $00,
$00, $80, $08, $00, $00, $40, $08, $00,
$00, $40, $08, $00, $00, $20, $10, $00,
$00, $20, $10, $00, $00, $7F, $F8, $00,
$00, $7F, $F8, $00, $00, $7F, $E8, $00,
$00, $7F, $F8, $00, $00, $00, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00 );
Mask: array[0..32*4-1] of Byte = (
$00, $30, $00, $00, $00, $78, $00, $00,
$00, $78, $00, $00, $00, $78, $00, $00,
$00, $78, $00, $00, $00, $7E, $00, $00,
$00, $7F, $C0, $00, $00, $7F, $F0, $00,
$00, $7F, $F8, $00, $03, $7F, $FC, $00,
$07, $FF, $FC, $00, $07, $FF, $FC, $00,
$03, $FF, $FC, $00, $03, $FF, $FC, $00,
$01, $FF, $FC, $00, $01, $FF, $FC, $00,
$00, $FF, $F8, $00, $00, $7F, $F8, $00,
$00, $7F, $F8, $00, $00, $3F, $F0, $00,
$00, $3F, $F0, $00, $00, $7F, $F8, $00,
$00, $7F, $F8, $00, $00, $7F, $E8, $00,
$00, $7F, $F8, $00, $00, $00, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00 );
{===============================}
{== Metody TddgDesignSpinner ==}
{===============================}
constructor TddgDesignSpinner.Create( AOwner: TComponent );
var
BitsBitmap: QBitmapH;
MaskBitmap: QBitmapH;
begin
inherited Create( AOwner );
(*
// VCL->CLX: W CLX nie ma ładowania kursora z zasobu
FThumbCursor := LoadCursor( HInstance, 'DdgDSNSPN_BTNCURSOR' );
*)
// VCL->CLX: Tworzenie kursora na podstawie tablic
BitsBitmap := QBitmap_create( 32, 32, @Bits, False );
MaskBitmap := QBitmap_create( 32, 32, @Mask, False );
try
FThumbCursor := QCursor_create( BitsBitmap, MaskBitmap, 8, 0 );
finally
QBitmap_destroy( BitsBitmap );
QBitmap_destroy( MaskBitmap );
end;
end;
destructor TddgDesignSpinner.Destroy;
begin
(*
VCL->CLX: QCursor_Destroy zamiast DestroyCursor
DestroyCursor( FThumbCursor ); // zwolnij obiekt GDI
*)
QCursor_Destroy( FThumbCursor );
inherited Destroy;
end;
// gdy kursor znajdzie się nad jednym z przycisków, zmień jego wygląd
(*
// VCL->CLX: w CLX nie ma komunikatów
procedure TddgDesignSpinner.WMSetCursor( var Msg: TWMSetCursor );
begin
if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then
SetCursor( FThumbCursor )
else
inherited;
end;
*)
// VCL->CLX: Przedefiniowanie metody MouseMove w celu obsługi zmiany kursora
procedure TddgDesignSpinner.MouseMove( Shift: TShiftState;
X, Y: Integer );
begin
if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then
QWidget_setCursor( Handle, FThumbCursor )
else
QWidget_UnsetCursor( Handle );
inherited;
end;
(*
// VCL->CLX: W CXL nie ma komunikatów. Użyj w zamian metody
// DesignEventQuery.
procedure TddgDesignSpinner.CMDesignHitTest( var Msg:
TCMDesignHitTest );
begin
// Obsługując ten komunikat pozwalamy na zmianę właściwości
// Value na etapie projektowania za pomocą lewego przycisku myszy.
// Gdy kursor myszy znajdzie się nad jednym z przycisków,
// wartością zwrotną komunikatu będzie 1 - stanowi to dla Delphi
// instrukcję, by przekazywać do komponentu obsługę zdarzeń
// związanych z myszą
if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then
Msg.Result := 1
else
Msg.Result := 0;
end;
*)
function TddgDesignSpinner.DesignEventQuery( Sender: QObjectH;
Event: QEventH ): Boolean;
var
MousePos: TPoint;
begin
Result := False;
if ( Sender = Handle ) and
( QEvent_type(Event) in [QEventType_MouseButtonPress,
QEventType_MouseButtonRelease,
QEventType_MouseButtonDblClick]) then
begin
// Note: bieżąca pozycja kursora myszy nie jest w tym przypadku
// istotna, pokazujemy jednak, jak ją obliczyć.
MousePos := Point( QMouseEvent_x( QMouseEventH( Event ) ),
QMouseEvent_y( QMouseEventH( Event ) ) );
if MouseOverButton( btMinus ) or MouseOverButton( btPlus ) then
Result := True
else
Result := False;
end;
end;
procedure TddgDesignSpinner.Change;
var
Form: TCustomForm;
begin
inherited Change;
// Uaktualnij wyświetlaną w inspektorze obiektów wartość
// właściwości Value
if csDesigning in ComponentState then
begin
Form := GetParentForm( Self );
(*
// VCL->CLX: Form.Designer zastąpiono przez DesignerHook
if ( Form <> nil ) and ( Form.Designer <> nil ) then
Form.Designer.Modified;
*)
if ( Form <> nil ) and ( Form.DesignerHook <> nil ) then
Form.DesignerHook.Modified;
end;
end;
end.
Po przeanalizowaniu komentarzy ponownie można zauważyć wyeliminowanie kodu związanego z systemem komunikatów Windows. Dla kontrolki Windows sygnałem do zmiany wyglądu kursora jest otrzymanie przez nią komunikatu wm_SetCursor, w odpowiedzi na co powinna wywołać funkcję SetCursor() z odpowiednim parametrem. W CLX nie ma komunikatów, trzeba zatem kontrolować położenie kursora za pomocą zdarzenia OnMouseMove; gdy kursor znajdzie się nad jednym z przycisków, należy nadać kursorowi żądany wygląd za pomocą funkcji QWidget_setCursor(), w przeciwnym razie — zapewnić jego kształt domyślny przez wywołanie funkcji QWidget_UnsetCursor().
Osobnym problemem jest samo określenie kształtu kursora. W Windows robi się to bardzo prosto, na przykład przez wczytanie odpowiedniego zasobu za pomocą funkcji LoadCursor(). W bibliotece Qt przeciążona funkcja QCursor_create() udostępnia różnorodne sposoby tworzenia kursorów, nie przewidując jednak wykorzystania w tym celu zasobów. Rozwiązaniem zastępczym (które wykorzystaliśmy w naszym przykładzie) jest wówczas zdefiniowanie dwóch bitmap, z których pierwsza określa rozmieszczenie czarnych i białych pikseli w obrazie kursora, druga natomiast stanowi maskę określającą jego przezroczyste regiony.
Kolejne zadanie polega na spowodowaniu przejęcia przez komponent (niektórych) zdarzeń myszy na etapie projektowania. Komponent VCL sygnalizuje gotowość do takiej obsługi, zwracając wartość 1 w odpowiedzi na komunikat cm_DesignHitTest. W CLX analogiczne zadanie spełnia dynamiczna metoda DesignEventQuery(), zwracająca wynik typu Boolean; komponent powinien ją przedefiniować stosownie do swej specyfiki.
I ostatni problem — skoro komponent TddgDesignSpiner posiada zdolność modyfikowania swej właściwości Value, modyfikacja ta nie może odbywać się bez wiedzy inspektora obiektów. W VCL mechanizmem zapewniającym aplikacji łączność z projektantem formularzy jest interfejs ukrywający się pod właściwością Designer formularza będącego właścicielem komponentu; w CLX analogiczna właściwość nosi nazwę DesignerHook. Aby zasygnalizować zmianę zawartości komponentu, należy wywołać metodę Modified wspomnianego interfejsu, w ramach obsługi zdarzenia OnChange — robi się to tak samo w VCL i CLX.
Wykorzystanie bitmap — komponent TddgImgListSpinner
Kolejne rozszerzenie naszego komponentu (który nosi teraz nazwę TddgImgListSpinner) polega na wyposażeniu go w bitmapy określające wygląd obydwu przycisków. Bitmapy te stanowią elementy listy typu ImageList (patrz rysunek 13.6) i wyświetlane są zamiast zwykłych znaków „+” i „-”.
Rysunek 13.6. Bitmapy nadające wygląd przyciskom komponentu TddgImgListSpinner
Kod źródłowy modułu implementującego komponent jest przedstawiony na wydruku 13.3; w porównaniu z wydrukiem 13.2 zmiany wynikające z przejścia na platformę CLX są znacznie mniejsze.
Wydruk 13.3. QddgILSpin.pas — kod źródłowy komponentu TddgImgListSpinner
{==================================================================
QddgILSpin
Niniejszy moduł implementuje komponent TddgImgListSpinner wywodzący
się z TddgDesignSpinner. Wygląd jego przycisków określony jest przez
bitmapy stanowiące zawartość stowarzyszonej z nim listy.
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgILSpin;
interface
uses
Classes, Types, QddgSpin, QddgDsnSpn, QImgList;
(*
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
ddgSpin, ddgDsnSpn, ImgList;
*)
type
TddgImgListSpinner = class( TddgDesignSpinner )
private
FImages: TCustomImageList;
FImageIndexes: array[ 1..2 ] of Integer;
FImageChangeLink: TChangeLink;
// Wewnętrzne procedury obsługi zdarzeń
procedure ImageListChange( Sender: TObject );
protected
procedure Notification( AComponent : TComponent;
Operation : TOperation ); override;
procedure DrawButton( Button: TddgButtonType; Down: Boolean;
Bounds: TRect ); override;
procedure CalcCenterOffsets( Bounds: TRect; var L, T: Integer);
procedure CheckMinSize;
// Metody dostępowe właściwości
procedure SetImages( Value: TCustomImageList ); virtual;
function GetImageIndex( PropIndex: Integer ): Integer; virtual;
procedure SetImageIndex( PropIndex: Integer;
Value: Integer ); virtual;
public
constructor Create( AOwner: TComponent ); override;
destructor Destroy; override;
published
property Images: TCustomImageList
read FImages
write SetImages;
property ImageIndexMinus: Integer
index 1
read GetImageIndex
write SetImageIndex;
property ImageIndexPlus: Integer
index 2
read GetImageIndex
write SetImageIndex;
end;
implementation
uses
QGraphics; // VCL->CLX: Moduł CLX
{================================}
{== Metody TddgImgListSpinner ==}
{================================}
constructor TddgImgListSpinner.Create( AOwner: TComponent );
begin
inherited Create( AOwner );
FImageChangeLink := TChangeLink.Create;
FImageChangeLink.OnChange := ImageListChange;
// ponieważ użytkownik komponentu nie ma bezpośredniego dostępu
// do obiektu TChangeLink, nie może samodzielnie przypisywać
// procedur obsługi jego zdarzeniom
FImageIndexes[ 1 ] := -1;
FImageIndexes[ 2 ] := -1;
end;
destructor TddgImgListSpinner.Destroy;
begin
FImageChangeLink.Free;
inherited Destroy;
end;
procedure TddgImgListSpinner.Notification( AComponent: TComponent;
Operation: TOperation );
begin
inherited Notification( AComponent, Operation );
if ( Operation = opRemove ) and ( AComponent = FImages ) then
SetImages( nil ); // wywołanie metody dostępowej
end;
function TddgImgListSpinner.GetImageIndex( PropIndex:
Integer ): Integer;
begin
Result := FImageIndexes[ PropIndex ];
end;
procedure TddgImgListSpinner.SetImageIndex( PropIndex: Integer;
Value: Integer );
begin
if FImageIndexes[ PropIndex ] <> Value then
begin
FImageIndexes[ PropIndex ] := Value;
Invalidate;
end;
end;
procedure TddgImgListSpinner.SetImages( Value: TCustomImageList );
begin
if FImages <> nil then
FImages.UnRegisterChanges( FImageChangeLink );
FImages := Value;
if FImages <> nil then
begin
FImages.RegisterChanges( FImageChangeLink );
FImages.FreeNotification( Self );
CheckMinSize;
end;
Invalidate;
end;
procedure TddgImgListSpinner.ImageListChange( Sender: TObject );
begin
if Sender = Images then
begin
CheckMinSize;
// Wywołaj Update, zamiast Invalidate, by zapobiec
// nadmiernemu migotaniu
Update;
end;
end;
procedure TddgImgListSpinner.CheckMinSize;
begin
// zapewnij taką wielkość każdego z przycisków, by pomieścił
// całą bitmapę
if FImages.Width > ButtonWidth then
ButtonWidth := FImages.Width;
if FImages.Height > Height then
Height := FImages.Height;
end;
procedure TddgImgListSpinner.DrawButton( Button: TddgButtonType;
Down: Boolean;
Bounds: TRect );
var
L, T: Integer;
begin
with Canvas do
begin
Brush.Color := ButtonColor;
Pen.Color := clBtnShadow;
Rectangle( Bounds.Left, Bounds.Top,
Bounds.Right, Bounds.Bottom );
if Button = btMinus then // wyświetl bitmapę przycisku "minus"
begin
if ( Images <> nil ) and ( ImageIndexMinus <> -1 ) then
begin
(*
// VCL->CLX: Lista ImageList w CLX nie umożliwia wyboru stylu rysowania
// użyj w zamian właściwości BkColor.
if Down then
FImages.DrawingStyle := dsSelected
else
FImages.DrawingStyle := dsNormal;
*)
if Down then
FImages.BkColor := clBtnShadow
else
FImages.BkColor := clBtnFace;
CalcCenterOffsets( Bounds, L, T );
(*
// VCL->CLX: TImageList.Draw ma w CLX inne parametry
FImages.Draw( Canvas, L, T, ImageIndexMinus, Enabled );
*)
FImages.Draw( Canvas, L, T, ImageIndexMinus, itImage,
Enabled );
end
else
inherited DrawButton( Button, Down, Bounds );
end
else // wyświetl bitmapę przycisku "plus"
begin
if ( Images <> nil ) and ( ImageIndexPlus <> -1 ) then
begin
(*
// VCL->CLX: Lista ImageList w CLX nie umożliwia wyboru stylu rysowania
// użyj w zamian właściwości BkColor.
if Down then
FImages.DrawingStyle := dsSelected
else
FImages.DrawingStyle := dsNormal;
*)
if Down then
FImages.BkColor := clBtnShadow
else
FImages.BkColor := clBtnFace;
CalcCenterOffsets( Bounds, L, T );
(*
// VCL->CLX: TImageList.Draw ma w CLX inne parametry
FImages.Draw( Canvas, L, T, ImageIndexPlus, Enabled );
*)
FImages.Draw( Canvas, L, T, ImageIndexPlus, itImage,
Enabled );
end
else
inherited DrawButton( Button, Down, Bounds );
end;
end;
end; {= TddgImgListSpinner.DrawButton =}
procedure TddgImgListSpinner.CalcCenterOffsets( Bounds: TRect;
var L, T: Integer );
begin
if FImages <> nil then
begin
L := Bounds.Left + ( Bounds.Right - Bounds.Left ) div 2 -
( FImages.Width div 2 );
T := Bounds.Top + ( Bounds.Bottom - Bounds.Top ) div 2 -
( FImages.Height div 2 );
end;
end;
end.
Lista obrazków TImageList, w VCL stanowiąca otoczkę standardowej kontrolki implementowanej w bibliotece ComCtl32.Dll, w CLX zaimplementowana została w zupełnie inny sposób, za pomocą tzw. prymitywów graficznych biblioteki Qt. Tę nową implementację zawiera moduł QImgList, który tym samym zastąpił na liście uses moduł ImgList. Nie zmienił się jednak zasadniczo sposób korzystania z listy, ani też nazwa jej klasy, czego wymierną korzyścią jest niezmieniona deklaracja klasy komponentu (w stosunku do jego wersji VCL). Identyczne są też metody komponentu w obydwu wersjach, z jednym wszakże wyjątkiem.
Wyjątkiem tym jest metoda DrawButton(), różnica w stosunku do VCL wynika z faktu, iż w CLX lista TImageList nie operuje pojęciem stanu przycisku (zwolniony, naciśnięty, itp.) reprezentowanego w VCL przez właściwość DrawingStyle, stan ten należy więc rozróżniać samodzielnie poprzez zmianę koloru tła, który reprezentowany jest przez właściwość BkColor. Ponadto metoda TImageList.Draw() ma w CLX nieco inne parametry niż w VCL, inne jest więc jej wywołanie.
Współpraca z bazą danych — komponent TddgDBSpinner
Skoro nadaliśmy estetyczny wygląd przyciskom komponentu, warto teraz poeksperymentować z przechowywaną przez niego wartością, a raczej — z jej źródłem. Kolejny komponent pochodny — TddgDBSpinner — czerpie tę wartość z pola zbioru danych, które reprezentowane jest przez jego właściwości DataSource i DataField. Rysunek 13.7 przedstawia komponent TddgDBSpinner skojarzony z polem VenueNo zbioru Events.
Rysunek 13.7. Komponent TddgDBSpinner używany do edycji pola bazy danych
Kod źródłowy modułu implementującego komponent został przedstawiony na wydruku 13.4.
Wydruk 13.4. QddgDBSpin.Pas — kod źródłowy komponentu TddgDBSpinner
{==================================================================
QddgDBSpin
Niniejszy moduł implementuje komponent TddgDBSpinner. Ilustruje
on sposób skojarzenia komponentu TddgImgListSpinner z polem
bazy danych.
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgDBSpin;
interface
uses
SysUtils, Classes, Qt, QddgILSpin, DB, QDBCtrls;
(*
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
ddgILSpin, DB, DBCtrls;
*)
type
TddgDBSpinner = class( TddgImgListSpinner )
private
FDataLink: TFieldDataLink; // zapewnia dostęp do danych
// wewnętrzne procedury zdarzeniowe
procedure DataChange( Sender: TObject );
procedure UpdateData( Sender: TObject );
procedure ActiveChange( Sender: TObject );
(*
// VCL->CLX: w CLX nie ma komunikatów
procedure CMExit( var Msg: TCMExit ); message cm_Exit;
procedure CMDesignHitTest( var Msg: TCMDesignHitTest );
message cm_DesignHitTest;
*)
protected
procedure Notification( AComponent : TComponent;
Operation : TOperation ); override;
procedure CheckFieldType( const Value: string ); virtual;
// przedefiniowane metody zarządzające zdarzeniami
procedure Change; override;
procedure KeyPress( var Key : Char ); override;
// VCL->CLX: DoExit zamiast CMExit
procedure DoExit; override;
// VCL->CLX: DesignEventQuery zamiast CMDesignHitTest
function DesignEventQuery( Sender: QObjectH;
Event: QEventH ): Boolean; override;
// przedefiniowane metody modyfikujące właściwość Value
procedure DecValue( Amount: Integer ); override;
procedure IncValue( Amount: Integer ); override;
// metody dostępowe właściwości
function GetField: TField; virtual;
function GetDataField: string; virtual;
procedure SetDataField( const Value: string ); virtual;
function GetDataSource: TDataSource; virtual;
procedure SetDataSource( Value: TDataSource ); virtual;
function GetReadOnly: Boolean; virtual;
procedure SetReadOnly( Value: Boolean ); virtual;
// dostęp do pola i do łącznika danych
property Field: TField
read GetField;
property DataLink: TFieldDataLink
read FDataLink;
public
constructor Create( AOwner: TComponent ); override;
destructor Destroy; override;
published
property DataField: string
read GetDataField
write SetDataField;
property DataSource: TDataSource
read GetDataSource
write SetDataSource;
// ta metoda steruje dostępnością łącznika do zapisu
property ReadOnly: Boolean
read GetReadOnly
write SetReadOnly
default False;
end;
type
EInvalidFieldType = class( Exception );
resourcestring
SInvalidFieldType = 'Skojarzone pole danych musi mieć typ ' +
'Integer, Smallint, Word lub Float';
implementation
uses
Types; // VCL->CLX: Moduł CLX
{===========================}
{== Metody TddgDBSpinner ==}
{===========================}
constructor TddgDBSpinner.Create( AOwner: TComponent );
begin
inherited Create( AOwner );
FDataLink := TFieldDataLink.Create;
// poinformuj łącznik danych, iż spiner jest kontrolką
// stowarzyszoną z polem danych
FDataLink.Control := Self;
// przyporządkuj procedury zdarzeniowe; użytkownik nie ma bezpośredniego
// dostępu do łącznika i nie może tego zrobić samodzielnie
FDataLink.OnDataChange := DataChange;
FDataLink.OnUpdateData := UpdateData;
FDataLink.OnActiveChange := ActiveChange;
end;
destructor TddgDBSpinner.Destroy;
begin
FDataLink.Free;
FDataLink := nil;
inherited Destroy;
end;
procedure TddgDBSpinner.Notification( AComponent: TComponent;
Operation: TOperation );
begin
inherited Notification( AComponent, Operation );
if ( Operation = opRemove ) and
( FDataLink <> nil ) and
( AComponent = FDataLink.DataSource ) then
begin
DataSource := nil; // pośrednio wywołuje SetDataSource
end;
end;
function TddgDBSpinner.GetField: TField;
begin
Result := FDataLink.Field;
end;
function TddgDBSpinner.GetDataField: string;
begin
Result := FDataLink.FieldName;
end;
procedure TddgDBSpinner.SetDataField( const Value: string );
begin
CheckFieldType( Value );
FDataLink.FieldName := Value;
end;
function TddgDBSpinner.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TddgDBSpinner.SetDataSource( Value: TDataSource );
begin
if FDatalink.DataSource <> Value then
begin
FDataLink.DataSource := Value;
// Wywołanie FreeNotification jest konieczne, ponieważ komponent
// reprezentujący zbiór danych może znajdować się w innym formularzu
// lub module danych
if Value <> nil then
Value.FreeNotification( Self );
end;
end;
function TddgDBSpinner.GetReadOnly: Boolean;
begin
Result := FDataLink.ReadOnly;
end;
procedure TddgDBSpinner.SetReadOnly( Value: Boolean );
begin
FDataLink.ReadOnly := Value;
end;
procedure TddgDBSpinner.CheckFieldType( const Value: string );
var
FieldType: TFieldType;
begin
// sprawdź, czy skojarzone pole bazy danych jest typu
// ftInteger, ftSmallInt, ftWord lub ftFLoat - jeżeli nie,
// wygeneruj wyjątek
if ( Value <> '' ) and
( FDataLink <> nil ) and
( FDataLink.Dataset <> nil ) and
( FDataLink.Dataset.Active ) then
begin
FieldType := FDataLink.Dataset.FieldByName( Value ).DataType;
if ( FieldType <> ftInteger ) and
( FieldType <> ftSmallInt ) and
( FieldType <> ftWord ) and
( FieldType <> ftFloat ) then
begin
raise EInvalidFieldType.Create( SInvalidFieldType );
end;
end;
end;
procedure TddgDBSpinner.Change;
begin
// poinformuj łącznik, że zmieniły się dane
if FDataLink <> nil then
FDataLink.Modified;
inherited Change; // Generuje zdarzenie OnChange
end;
procedure TddgDBSpinner.KeyPress( var Key: Char );
begin
inherited KeyPress( Key );
if Key = #27 then // czy naciśnięto ESC ?
begin
FDataLink.Reset; // tak, anuluj to naciśnięcie, by // Esc key pressed
Key := #0; // nie nastąpiło zakończenie edycji
end;
end;
procedure TddgDBSpinner.DecValue( Amount: Integer );
begin
if ReadOnly or not FDataLink.CanModify then
begin
// uniemożliwienie niedozwolonych zmian
(*
// VCL->CLX: MessageBeep is a Windows API function
MessageBeep( 0 )
*)
Beep;
end
else
begin
// ustaw zbiór danych w tryb edycji i zmodyfikuj wartość
if FDataLink.Edit then
inherited DecValue( Amount );
end;
end;
procedure TddgDBSpinner.IncValue( Amount: Integer );
begin
if ReadOnly or not FDataLink.CanModify then
begin
// uniemożliwienie niedozwolonych zmian
(*
// VCL->CLX: MessageBeep is a Windows API function
MessageBeep( 0 )
*)
Beep;
end
else
begin
// ustaw zbiór danych w tryb edycji i zmodyfikuj wartość
if FDataLink.Edit then
inherited IncValue( Amount );
end;
end;
{==================================================================
TddgDBSpinner.DataChange
Niniejsza metoda może zostać wywołana z rozmaitych powodów:
1. Zmienia się wartość pola stowarzyszonego z kontrolką
2. Stowarzyszony zbiór danych przełączany jest w tryb edycji
3. Zmienia się zawartość stowarzyszonego zbioru danych
4. Zmienia się bieżący rekord w zbiorze danych
5. Rekord zostaje zresetowany przez wywołanie metody Cancel
6. Właściwość DataField zmienia wskazanie na inne pole
==================================================================}
procedure TddgDBSpinner.DataChange( Sender: TObject );
begin
if FDataLink.Field <> nil then
Value := FDataLink.Field.AsInteger;
end;
{==================================================================
TddgDBSpinner.UpdateData
Niniejsza metoda wywoływana jest w sytuacji, gdy zawartość skojarzonego
pola i zawartość kontrolki wymagają synchronizacji. Wywoływana
tylko wtedy, gdy kontrolka jest w stanie zmieniać zawartość pola.
==================================================================}
procedure TddgDBSpinner.UpdateData( Sender: TObject );
begin
FDataLink.Field.AsInteger := Value;
end;
{==================================================================
TddgDBSpinner.ActiveChange
Niniejsza metoda wywoływana jest w momencie zamykania lub otwierania
zbioru danych, tj. gdy zmienia się jego właściwość Active.
Nowy stan otwarcia zbioru danych można odczytać z właściwości
FDataLink.Active
==================================================================}
procedure TddgDBSpinner.ActiveChange( Sender: TObject );
begin
// przy otwieraniu zbioru danych sprawdź typ skojarzonego pola
if ( FDataLink <> nil ) and FDataLink.Active then
CheckFieldType( DataField );
end;
(*
// VCL->CLX: DoExit zamiast CMExit
procedure TddgDBSpinner.CMExit( var Msg: TCMExit );
begin
try // Próba uaktualnienia rekordu, gdy spiner traci skupienie
FDataLink.UpdateRecord;
except
SetFocus; // nie pozwól na utratę skupienia,
// gdy aktualizacja się nie powiodła
raise; // ponów wyjątek
end;
inherited;
end;
*)
procedure TddgDBSpinner.DoExit;
begin
try // Próba uaktualnienia rekordu, gdy spiner traci skupienie
FDataLink.UpdateRecord;
except
SetFocus; // nie pozwól na utratę skupienia,
// gdy aktualizacja się nie powiodła
raise; // ponów wyjątek
end;
inherited;
end;
(*
// VCL->CLX: DesignEventQuery zamiast CMDesignHitTest
procedure TddgDBSpinner.CMDesignHitTest(var Msg: TCMDesignHitTest);
begin
// Tym razem należy zablokować możliwość edycji wartości
// na etapie projektowania, gdyż wiązałoby się to z koniecznością
// przestawienia skojarzonego zbioru danych w tryb edycji
Msg.Result := 0;
end;
*)
function TddgDBSpinner.DesignEventQuery( Sender: QObjectH;
Event: QEventH ): Boolean;
begin
// Tym razem należy zablokować możliwość edycji wartości
// na etapie projektowania, gdyż wiązałoby się to z koniecznością
// przestawienia skojarzonego zbioru danych w tryb edycji
Result := False;
end;
end.
Kojarzenie komponentu z polem danych przebiega tu niemal identycznie jak w VCL. Mamy więc łącznik z polem danych (TFieldDataLink) i obsługę zdarzeń OnDataChange i OnUpdateData; konkretne pole reprezentowane jest przez właściwości DataSource i DataField, zaś dopuszczalność zmian w zbiorze danych kontrolowana jest przez właściwość ReadOnly.
Notatka
Zwróć uwagę, iż zamiast modułu DBCtrls wykorzystywany jest moduł QDBCtrls. Obydwa te moduły implementują klasę TFieldDataLink, jednakże próba użycia modułu DBCtrls pod Kyliksem da w efekcie mnóstwo błędów syntaktycznych.
Jedyna różnica w stosunku do VCL wynika (znowu) z nieobecności komunikatów w CLX i dotyczy komunikatu cm_Exit, w odpowiedzi na który większość komponentów bazodanowych dokonuje aktualizacji kontrolowanych przez siebie pól, wywołując metodę UpdateRecord. W CLX analogiczną rolę pełni metoda DoExit().
Jak pamiętamy, przodek naszego bazodanowego komponentu — TddgImgListSpinner — posiadał możliwość modyfikacji kontrolowanej przez siebie wielkości na etapie projektowania. W stosunku do komponentów bazodanowych mechanizm taki traci jednak rację bytu z prostego powodu: otóż rozpoczęcie edycji odnośnej wartości (tu ukrywającej się pod właściwością Value) spowoduje przełączenie skojarzonego zbioru danych w tryb edycji, którego nie da się opuścić na etapie projektowania. Komponent TddgDBSpinner konsekwentnie odżegnuje się więc od wszelkich prób samodzielnej obsługi zdarzeń na etapie projektowania, niezmiennie zwracając False jako wynik metody DesignEventQuery().
Edytory środowiskowe CLX
Edytory komponentów CLX i ich właściwości implementowane są podobnie, jak ich odpowiedniki w VCL, choć oczywiście występują pewne różnice. Na przykład moduł DsgnIntf zmienił swą nazwę na DesignIntf; ponadto w większości przypadków konieczne będzie dołączenie do listy uses modułu DesignEditors, gdyż moduł DesignIntf zawiera definicje interfejsów wykorzystywanych przez projektanta formularzy i inspektor obiektów, zaś w module DesignEditors zawarta jest implementacja podstawowych klas edytorów komponentów i edytorów właściwości.
Niestety, nie wszystkie charakterystyczne dla VCL mechanizmy IDE przeniesione zostały do CLX — nie ma na przykład edytorów właściwości charakteryzujących się specyficzną formą graficzną (owner-drawing property editors). Generalnie — implementację edytorów charakterystycznych dla CLX zawiera moduł CLXEditors, zaś edytorów specyficznych dla VCL — moduł VCLEditors.
Rysunek 13.8 przedstawia efekt działania specjalizowanego edytora komponentu TRadioGroup — TddgRadioGroupEditor. Pozwala on w wygodny sposób edytować właściwość ItemIndex. Jego kod źródłowy jest przedstawiony na wydruku 13.5.
Rysunek 13.8. Ułatwiony wybór pozycji z listy komponentu TRadioGroup
Wydruk 13.5. QddgRgpEdt.pas — kod źródłowy edytora TddgRadioGroupEditor
{==================================================================
QddgRgpEdt - Edytor TddgRadioGroupEditor
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgRgpEdt;
interface
uses
DesignIntf, DesignEditors, QExtCtrls, QDdgDsnEdt;
type
TddgRadioGroupEditor = class( TddgDefaultEditor )
protected
function RadioGroup: TRadioGroup; virtual;
public
function GetVerbCount: Integer; override;
function GetVerb( Index: Integer ) : string; override;
procedure ExecuteVerb( Index: Integer ); override;
end;
implementation
uses
QControls;
{==================================}
{== Metody TddgRadioGroupEditor ==}
{==================================}
function TddgRadioGroupEditor.RadioGroup: TRadioGroup;
begin
// pomocnicza funkcja zapewniająca wygodny dostęp do edytowanego
// komponentu; przy okazji daje gwarancję, iż komponent ten
// należy do klasy TRadioGroup lub pochodnej
Result := Component as TRadioGroup;
end;
function TddgRadioGroupEditor.GetVerbCount: Integer;
begin
// zwraca liczbę nowych opcji menu do wyświetlenia
Result := RadioGroup.Items.Count + 1;
end;
function TddgRadioGroupEditor.GetVerb( Index: Integer ): string;
begin
// tekst opcji menu kontekstowego
if Index = 0 then
Result := 'Edit Items...'
else
Result := RadioGroup.Items[ Index - 1 ];
end;
procedure TddgRadioGroupEditor.ExecuteVerb( Index: Integer );
begin
if Index = 0 then
EditPropertyByName( 'Items' ) // zdefiniowane w QDdgDsnEdt.pas
else
begin
if RadioGroup.ItemIndex <> Index - 1 then
RadioGroup.ItemIndex := Index - 1
else
RadioGroup.ItemIndex := -1; // usuń zaznaczenie
Designer.Modified;
end;
end;
end.
Efektem działania specjalizowanego edytora TddgRadioGroupEditor jest wzbogacenie menu kontekstowego komponentu TRadioGroup w etykiety poszczególnych jego elementów; wybranie któregoś elementu w menu kontekstowym powoduje jego zaznaczenie (w ramach komponentu). Oprócz tego do menu kontekstowego dodawana jest opcja Edit items… poprzedzająca etykiety elementów i powodująca uruchomienie standardowego edytora pozycji — uruchomienie następuje w wyniku wywołania metody EditPropertyByName() klasy TddgDefaultEditor. Metoda ta, otrzymując nazwę właściwości edytowanego komponentu, wywołuje aktualnie przypisany do niej edytor (zarejestrowany w środowisku IDE). Klasa edytora TddgDefaultEditor zdefiniowana jest w module QddgDsnEdt.pas, którego treść przedstawia wydruk 13.6.
Wydruk 13.6. QddgDsnEdt.pas — kod źródłowy edytora TddgDefaultEditor
{==================================================================
QddgDsnEdt - Definicja klasy TddgDefaultEditor
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgDsnEdt;
interface
uses
Classes, DesignIntf, DesignEditors;
type
TddgDefaultEditor = class( TDefaultEditor )
private
FPropName: string;
FContinue: Boolean;
FPropEditor: IProperty;
procedure EnumPropertyEditors(const PropertyEditor: IProperty);
procedure TestPropertyEditor( const PropertyEditor: IProperty;
var Continue: Boolean );
protected
procedure EditPropertyByName( const APropName: string );
end;
implementation
uses
SysUtils, TypInfo;
{===============================}
{== Metody TddgDefaultEditor ==}
{===============================}
procedure TddgDefaultEditor.EnumPropertyEditors( const
PropertyEditor: IProperty );
begin
if FContinue then
TestPropertyEditor( PropertyEditor, FContinue );
end;
procedure TddgDefaultEditor.TestPropertyEditor( const
PropertyEditor: IProperty;
var Continue: Boolean );
begin
if not Assigned( FPropEditor ) and
( CompareText( PropertyEditor.GetName, FPropName ) = 0 ) then
begin
Continue := False;
FPropEditor := PropertyEditor;
end;
end;
procedure TddgDefaultEditor.EditPropertyByName( const
APropName: string );
var
Components: IDesignerSelections;
begin
Components := TDesignerSelections.Create;
FContinue := True;
FPropName := APropName;
Components.Add( Component );
FPropEditor := nil;
try
GetComponentProperties( Components, tkAny, Designer,
EnumPropertyEditors );
if Assigned( FPropEditor ) then
FPropEditor.Edit;
finally
FPropEditor := nil;
end;
end;
end.
Pakiety
„Nośnikami” komponentów CLX przeznaczonych do rejestracji w IDE Delphi 6 lub Kyliksa są pakiety, podobnie jak w przypadku komponentów VCL. Należy jednak wyraźnie zaznaczyć, iż pakiety skompilowane w Delphi 6 nie mogą być instalowane w Kyliksie z powodu różnic w implementacji — pakiety windowsowe mają postać specyficznych bibliotek DLL, podczas gdy w środowisku Linuksa pakiety implementowane są jako tzw. obiekty współdzielone (shared objects) w postaci plików .so. Format i składnia pliku źródłowego pakietu są jednak takie same w obydwu środowiskach.
Zawartość pliku źródłowego pakietu różni się nieco w obydwu środowiskach, na przykład lista dyrektywy requires zawiera w Linuksie odwołanie do pakietu baseclx, nieobecnego w Delphi 6. Na liście tej, podobnie jak w VCL, powinny znaleźć się wszystkie pakiety zawierające instalowane komponenty CLX.
Konwencje nazewnicze
Wykorzystywane na użytek tego rozdziału komponenty zawarte są w pakietach wymienionych w tabelach 13.1 i 13.2. Obydwie tabele zawierają nazwy pakietów (w postaci źródłowej i skompilowanej) oraz nazwy innych pakietów wymaganych do instalacji — odpowiednio dla Delphi 6 i Kyliksa.
Tabela 13.1. Przykładowe pakiety CLX dla Delphi 6
Plik źródłowy |
Plik skompilowany |
Pakiety wymagane |
QddgSamples.dpk |
QddgSamples60.bpl |
visualclx |
QddgSamples_Dsgn.dpk |
QddgSamples_Dsgn60.bpl |
visualclx |
|
|
designide |
|
|
QddgSamples |
QddgDBSamples.dpk |
QddgDBSamples60.bpl |
visualclx |
|
|
dbrtl |
|
|
visualdbclx |
|
|
QddgSamples |
QddgDBSamples_Dsgn.dpk |
QddgDBSamples_Dsgn60.bpl |
visualclx |
|
|
QddgSamples_Dsgn |
|
|
QddgSamples |
Tabela 13.2. Przykładowe pakiety CLX dla Kyliksa
Plik źródłowy |
Plik skompilowany |
Pakiety wymagane |
QddgSamples.dpk |
bplQddgSamples.so.6 |
baseclx |
|
|
visualclx |
QddgSamples_Dsgn.dpk |
bplQddgSamples_Dsgn.so.6 |
baseclx |
|
|
visualclx |
|
|
designide |
|
|
QddgSamples |
QddgDBSamples.dpk |
bplQddgDBSamples.so.6 |
baseclx |
|
|
visualclx |
|
|
visualdbclx |
|
|
dataclx |
|
|
QddgSamples |
QddgDBSamples_Dsgn.dpk |
bplQddgDBSamples_Dsgn.so.6 |
baseclx |
|
|
visualclx |
|
|
QddgSamples_Dsgn |
|
|
QddgSamples |
Jak widać, odpowiedniość nazw pakietu źródłowego i skompilowanego rządzi się pewnymi (zwyczajowymi) regułami, różnymi dla Windows i Linuksa. W Delphi 6 do nazwy pliku źródłowego dodawany jest przyrostek 60, podkreślający przynależność pakietu do konkretnej wersji. Zauważmy, że w poprzednich wersjach Delphi nazwa pakietu skompilowanego była tożsama z jego nazwą źródłową; w Delphi 6, w celu zapewnienia przenośności kodu, dodano kilka dyrektyw umożliwiających kształtowanie nazwy wynikowej przez dodawanie przedrostków i (lub) przyrostków do nazwy źródłowej. Na wydruku 13.7 nietrudno odnaleźć dyrektywę $LIBSUFFIX ustalającą przyrostek nazwy w windowsowej wersji pakietu.
Mimo iż Borland nadaje niektórym pakietom nazwy rozpoczynające się od dcl (by wskazać, iż mamy do czynienia z pakietem środowiskowym), staramy się tego unikać w naszych przykładach, stosując w zamian przyrostek _Dsgn.
Wszystkie skompilowane pakiety windowsowe (środowiskowe i wykonywalne) posiadają rozszerzenie .bpl. W Linuksie tę konwencję realizuje poprzedzenie nazwy pakietu przyrostkiem bpl — decyduje o tym dyrektywa $SOPREFIX, którą nietrudno odnaleźć na wydruku 13.7; ponadto konkretna wersja (skompilowanego) pakietu znajduje odzwierciedlenie w ostatnim członie nazwy jego pliku, zgodnie z dyrektywą $SOVERSION.
Pakiety wykonywalne
Wydruki 13.7 i 13.8 przedstawiają kod źródłowy pakietów związanych z przykładowymi komponentami wykorzystywanymi w niniejszym rozdziale. Zwróć uwagę na symbole kompilacji warunkowej MSWINDOWS i LINUX — pierwszy z nich obowiązujący jest podczas kompilacji pakietu w Delphi 6, drugi — podczas kompilacji w Kyliksie.
Wydruk 13.7. QddgSamples.dpk — plik źródłowy pakietu wykonywalnego dla komponentów nie współpracujących z bazą danych
package QddgSamples;
{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO ON}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION ON}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO OFF}
{$SAFEDIVIDE OFF}
{$STACKFRAMES OFF}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST ON}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DESCRIPTION 'DDG: CLX Components'}
{$IFDEF MSWINDOWS}
{$LIBSUFFIX '60'}
{$ENDIF}
{$IFDEF LINUX}
{$SOPREFIX 'bpl'}
{$SOVERSION '6'}
{$ENDIF}
{$RUNONLY}
{$IMPLICITBUILD OFF}
requires
{$IFDEF LINUX}
baseclx,
{$ENDIF}
visualclx;
contains
QddgSpin in 'QddgSpin.pas',
QddgDsnSpn in 'QddgDsnSpn.pas',
QddgILSpin in 'QddgILSpin.pas';
end.
Wskazówka
Uzależniając określone fragmenty kodu źródłowego od konkretnej platformy, powinniśmy posługiwać się odrębnymi konstrukcjami {$IFDEF}…{$ENDIF} (jak na wydruku 13.7), a unikać konstrukcji {$IFDEF}…{$ELSE}… w rodzaju
{$IFDEF MSWINDOWS}
// kod specyficzny dla Windows
{$ELSE}
// kod specyficzny dla Linuksa
{$ENDIF}
Dzięki temu, jeżeli w przyszłości Borland zaimplementuje w Delphi obsługę także innych platform (poza Windows i Linuksem), kod przeznaczony dla Linuksa będzie widoczny także dla każdej innej platformy „niewindowsowej”.
Wydruk 13.8. QddgDBSamples.dpk — plik źródłowy pakietu wykonywalnego dla komponentów bazodanowych
package QddgDBSamples;
{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO ON}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION ON}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO OFF}
{$SAFEDIVIDE OFF}
{$STACKFRAMES OFF}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST ON}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DESCRIPTION 'DDG: CLX Components (Data-Aware)'}
{$IFDEF MSWINDOWS}
{$LIBSUFFIX '60'}
{$ENDIF}
{$IFDEF LINUX}
{$SOPREFIX 'bpl'}
{$SOVERSION '6'}
{$ENDIF}
{$RUNONLY}
{$IMPLICITBUILD OFF}
requires
{$IFDEF MSWINDOWS}
dbrtl,
{$ENDIF}
{$IFDEF LINUX}
baseclx,
dataclx,
{$ENDIF}
visualclx,
visualdbclx,
QddgSamples;
contains
QddgDBSpin in 'QddgDBSpin.pas';
end.
Pakiety środowiskowe
Mimo iż możliwe jest umieszczenie zaprojektowanych komponentów w pojedynczym pakiecie środowiskowo-wykonywalnym, postępowanie takie nie jest zalecane. Jeżeli bowiem pakiet taki zawiera edytor (komponentu lub właściwości) zaprojektowany przez użytkownika, wymaga on do swego funkcjonowania pakietu designide — który nie może być rozpowszechniany wraz z gotową aplikacją. Należy wówczas stworzyć dwa odrębne pakiety — wykonywalny i środowiskowy — i powierzyć pakietowi środowiskowemu rejestrację komponentów wykorzystywanych przez pakiety wykonywalne. Wydruki 13.9 i 13.10 przedstawiają treść plików źródłowych pakietów środowiskowych, odpowiadających pakietom wykonywalnym prezentowanym na wydrukach 13.7 i 13.8.
Wydruk 13.9. QddgSamples_Dsgn.dpk — plik źródłowy pakietu środowiskowego dla komponentów nie współpracujących z bazą danych
package QddgSamples_Dsgn;
{$R *.res}
{$R 'QddgSamples_Reg.dcr'}
{$ALIGN 8}
{$ASSERTIONS OFF}
{$BOOLEVAL OFF}
{$DEBUGINFO OFF}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS OFF}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION ON}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO OFF}
{$SAFEDIVIDE OFF}
{$STACKFRAMES OFF}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST ON}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DESCRIPTION 'DDG: CLX Components'}
{$LIBSUFFIX '60'}
{$LIBVERSION '6'}
{$DESIGNONLY}
{$IMPLICITBUILD OFF}
requires
{$IFDEF LINUX}
baseclx,
{$ENDIF}
visualclx,
designide,
QddgSamples;
contains
QddgSamples_Reg in 'QddgSamples_Reg.pas',
QddgDsnEdt in 'QddgDsnEdt.pas',
QddgRgpEdt in 'QddgRgpEdt.pas';
end.
Ostrzeżenie
Tworząc pojedynczy plik pakietu dla Delphi 6 i Kyliksa, musimy pamiętać o wrażliwości Linuksa na wielkość liter w nazwach plików. Nazwy odnośnych pakietów (w dyrektywach requires i contains) powinny być zapisywane w swej wiernej postaci (a więc np. designide, nie DesignIDE), w przeciwnym razie kompilator Kyliksa nie będzie mógł zlokalizować właściwego pliku — pod Linuksem DesignIDE.dcp to nie to samo co designide.dcp.
Wydruk 13.10. QddgDBSamples_Dsgn.dpk — plik źródłowy pakietu środowiskowego dla komponentów bazodanowych
package QddgDBSamples_Dsgn;
{$R *.res}
{$ALIGN 8}
{$ASSERTIONS OFF}
{$BOOLEVAL OFF}
{$DEBUGINFO OFF}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS OFF}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION ON}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO OFF}
{$SAFEDIVIDE OFF}
{$STACKFRAMES OFF}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST ON}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DESCRIPTION 'DDG: CLX Components (Data-Aware)'}
{$LIBSUFFIX '60'}
{$LIBVERSION '6'}
{$DESIGNONLY}
{$IMPLICITBUILD OFF}
requires
{$IFDEF LINUX}
baseclx,
{$ENDIF}
visualclx,
QddgSamples_Dsgn,
QddgDBSamples;
contains
QddgDBSamples_Reg in 'QddgDBSamples_Reg.pas';
end.
Moduły rejestracyjne
Podobnie jak komponenty VCL, także komponenty CLX zawarte w pakietach środowiskowych wymagają rejestracji. Rejestrację tę wykonuje procedura Register() zawarta w jednym z modułów wymienionych w dyrektywie contains. Wydruk 13.11 prezentuje kod źródłowy modułu „rejestracyjnego” QddgSamples_reg pakietu QddgSamples_Dsgn — rejestracji podlegają komponenty TddgSpinner, TddgDesignSpinner i TddgImgListSpinner oraz edytorTddgRadioGroupEditor.
Wydruk 13.11. Moduł rejestracyjny pakietu QddgSamples_Dsgn
{==================================================================
QddgSamples_Reg
Moduł rejestracyjny dla komponentów niebazodanowych
Copyright © 2001 by Ray Konopka
==================================================================}
unit QddgSamples_Reg;
interface
procedure Register;
implementation
uses
Classes, DesignIntf, DesignEditors, QExtCtrls,
QddgSpin, QddgDsnSpn, QddgILSpin,
QddgRgpEdt;
{=============================}
{== procedura rejestracyjna ==}
{=============================}
procedure Register;
begin
{== rejestracja komponentów ==}
RegisterComponents( 'DDG-CLX',
[ TddgSpinner,
TddgDesignSpinner,
TddgImgListSpinner ] );
{== rejestracja edytora komponentu ==}
RegisterComponentEditor( TRadioGroup, TddgRadioGroupEditor );
end;
end.
Ikony komponentów
Nowo stworzonym komponentom można przyporządkować ikony identyfikujące je w palecie komponentów — ikony te muszą być 16-kolorowymi bitmapami o rozmiarze 24×24 piksele. Zgodnie z sugestiami zawartymi w systemie pomocy Delphi i Kyliksa, należy utworzyć odrębne zasoby bitmap dla każdego z komponentów. Tymczasem edytor pakietów dla każdego dodawanego do pakietu modułu .dcu poszukuje odpowiadającego mu pliku .dcr nawet wtedy, gdy pakiet jest pakietem wykonywalnym; wspomniane bitmapy nie są w pakiecie wykonywalnym do niczego potrzebne i tylko bezproduktywnie zajmują miejsce.
Tak więc, zamiast tworzyć osobne pliki .dcr dla każdego z komponentów, należy raczej utworzyć pojedynczy plik z bitmapami dla wszystkich komponentów w pakiecie. Tak się szczęśliwie składa, że pliki zasobowe dołączane do wykonywalnych plików Linuksa mają format identyczny z plikami zasobowymi Win32 (mimo iż same pliki wykonywalne różnią się w obydwu tych środowiskach). Można więc, używając dowolnego edytora zasobów windowsowych, utworzyć żądany plik .res i zmienić jego rozszerzenie na .dcr.
Edycję bitmapy dla jednego z opisywanych wcześniej komponentów przedstawia rysunek 13.9.
Rysunek 13.9. Edycja bitmapy zawartej w pliku .dcr
Zwróć uwagę na to, iż w obydwu naszych przykładowych pakietach środowiskowych plik .dcr ma taką samą nazwę jak odpowiedni moduł rejestracyjny; umieszczając więc ten ostatni w pakiecie, automatycznie powodujemy również dołączenie stosownej bitmapy. Dla pakietów wykonywalnych nie istnieją moduły rejestracyjne, nie ma więc też niepotrzebnych bitmap.
Na zakończenie jeszcze dobra rada: mimo iż ikona reprezentująca komponent w palecie nie ma żadnego wpływu na jego funkcjonowanie, nie można nie doceniać jej znaczenia. Jest ona wizytówką komponentu i kształtuje pierwsze wyobrażenie o nim; niedbała wizytówka może stwarzać (być może niesłusznie) wrażenie, iż opatrzony nią komponent wykonany jest równie niedbale. Jak istotne jest to w przypadku komponentów wykonywanych dla celów komercyjnych, nie trzeba nikogo przekonywać…
Podsumowanie
Niniejszy rozdział poświęciliśmy pewnemu — rzec by można: rozwojowemu — aspektowi tworzenia aplikacji i komponentów w środowisku typu RAD, mianowicie uwzględnieniu przyszłej ich migracji na platformy inne niż MS Windows. To właśnie Delphi, jako pierwsze popularne narzędzie do błyskawicznego tworzenia aplikacji, przekroczyło zaklętą granicę Windows, gdy pod postacią Kyliksa zaistniało w systemie Linux. Możliwość tworzenia aplikacji uniwersalnych, akceptowanych zarówno w Delphi, jak i w Kyliksie, pojawiła się w Delphi 6 pod postacią biblioteki CLX, będącej zestawem komponentów i zrealizowanej na podstawie międzyplatformowych mechanizmów biblioteki Qt.
Tworzenie aplikacji międzyplatformowych wiąże się z pewnymi ograniczeniami w stosunku do aplikacji opartych na bibliotece VCL; ograniczenia te wynikają po prostu z braku pewnych mechanizmów Windows w innych systemach operacyjnych, między innymi w Linuksie, i są naturalną ceną płaconą za uniwersalność.
Nawet podczas tworzenia aplikacji przeznaczonych wyłącznie dla Windows warto zdawać sobie sprawę z faktu, iż (ewentualne) ich przystosowanie do wymogów CLX (w przyszłości) będzie tym łatwiejsze, w im większym stopniu respektowane będą owe ograniczenia. Ów „respekt” wyrażać się powinien przede wszystkim w unikaniu (wszędzie, gdzie to tylko możliwe i akceptowalne) mechanizmów specyficznych dla Windows — głównie komunikatów, których obsługę należy zastąpić metodami dyspozycyjnymi, oraz bezpośrednich odwołań do funkcji GDI, które powinny ustąpić miejsca odpowiednim metodom płótna. Zaprezentowane w niniejszym rozdziale implementacje przykładowych komponentów obfitują w takie właśnie „eliminacje”.
Jednym z najistotniejszych przejawów uniwersalności aplikacji międzyplatformowej jest akceptowalność jej jedynego kodu źródłowego na różnych platformach (na razie — w Delphi 6 i Kyliksie). W sytuacji, gdy pewne rozwiązania nie dadzą się łatwo zaprogramować w sposób uniwersalny, możliwe jest wydzielenie fragmentów kodu dedykowanych tylko konkretnemu środowisku; temu celowi służą odpowiednie symbole kompilacji warunkowej (na razie — MSWINDOWS i LINUX).
To spolszczona postać angielskiego terminu widget, który jest zlepkiem słów Visual Gadget (przyp. tłum.).
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\WINNT\Profiles\adamb\Pulpit\Delphi\r13-komponenty CLX.doc