2380


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.

0x01 graphic

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).

0x01 graphic

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.

0x01 graphic

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).

0x01 graphic

Rysunek 13.4. Zmiana wyglądu kursora wywołana przez komponent TddgDesignSpiner

0x01 graphic

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 „-”.

0x01 graphic

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.

0x01 graphic

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 TRadioGroupTddgRadioGroupEditor. Pozwala on w wygodny sposób edytować właściwość ItemIndex. Jego kod źródłowy jest przedstawiony na wydruku 13.5.

0x01 graphic

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.

0x01 graphic

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



Wyszukiwarka