Dynamiczne ustawianie właściwości
Deti
RTTI to skrót od "Runtime Type Information" - wykorzystywany już od najwcześniejszych wersji Delphi. Najprościej mówiąc jest to szereg informacji o klasach wykorzystywanych przez program. Być może póki co jest to niezrozumiałe, lecz z czasem zrozumiesz istotę tego mechanizmu oraz możliwości jakie niesie za sobą. Przed lekturą tegoż artykułu zalecam przeczytanie dwóch innych: Klasy oraz Typy danych - systematyka, aby zagadnienia tam zawarte były już opanowane.
Zapewne wielu z was do wykonania pewnych programów używa tzw "komponentów" dostepnych w całej palecie VCL. Otóż jak wiemy są to pewne klasy, które mogą posiadać metody, własciwości. Rozważmy przykładową klasę TButton. Po bezpośrednim postawieniu jednego z egzemplarzy klasy na formę możemy dowolnie manipulować jego właściwościami - możemy to robić na dwa sposoby: w inspektorze obiektów lub od razu w kodzie. Na przykład:
Button1.Visible := False;
Tym sposobem możemy modyfikować pewne właściwości (Property) klas. Ta akurat (Visible) jest typu Boolean i przypisaliśmy jej wartość False co oczywiście spowoduje, że Button zniknie, stanie się niewidoczny. Jednak gdybyśmy chwieli stworzyć procedurę, która byłaby w stanie zmieniać dowolne właściwości klas - pojawił by się problem...
procedure SetProperty(MyComponent: TComponent; Prop: String; Value: Integer);
Wymyślona procedura SetProperty posiada trzy parametry:
- MyComponent: TComponent - nasz komponent, który chcemy modyfikować (zmieniać właściwości)
- Prop: String; - nazwa właściwości (właściwość typu Integer)
- Value: Integer - konkretna wartość
Jak zmusić kompilator, aby odpowiednio przyjął drugi parametr i ustawił odpowiednią wartość przekazaną przez parametr Value? Oczywiście cała zmiana ma się tyczyć komponentu MyComponent, który jest z kolei przekazywany przez pierwszy parametr naszej procedury.
Zapewne po kilku wymysłach można się pokusić o takie rozwiązanie:
procedure SetProperty(MyComponent: TComponent; Prop: String; Value: Integer);
begin
if Prop = 'top' then TWinControl(MyComponent).Top := Value;
if Prop = 'left' then TWinControl(MyComponent).Left := Value;
if Prop = 'Width' then TWinControl(MyComponent).Width := Value;
if Prop = 'Height' then TWinControl(MyComponent).Height := Value;
// i tak dalej
end;
Rzutowanie komponentu na TWinControl daje nam dostep do właściwości, ale mimo wszystko nie jest to dobra metoda. Procedura SetProperty zadziała tylko dla wcześniej przewidzianych właściwości i "siłą" wypisanych jak nasze cztery powyżej. Nam jednak chodzi o to, aby bezpośrednio ustawić daną wartość. Tym właśnie i nie tylko będziemy się tu zajmować.
Każda klasa posiada informcję RTTI. Aby skorzystać z tych "usług" należy użyć modułu TypInfo.pas zatem najlepiej jest go dodać do sekcji uses. Właśnie za jego pomocą uzyskamy potrzebne nam dane. Dla przykładu chcielibyśmy się dowiedzieć jak najwięcej na temat przycisku(TButton). Postawmy zatem jakiś Button na formę, niech się nazywa Button1. Do zebrania informacji potrzebne nam będą dwie zmienne:
var
ObjectData: PTypeInfo;
ObjectInfo: PTypeData;
ObjectData to zmienna zawierająca informacje o typie danych jakiejś klasy, natomiast ObjectInfo zawiera "wyciągnięte" z niej właściwości, metody, zdarzenia. Odsyłam również do Helpa, gdzie można znaleźć dokładniejszy opis typów: PTypeInfo oraz PTypeData.
Aby sprawdzić dostępne informacje dla naszego Button`a:
ObjectData := Button1.ClassInfo; // Typ danych klasy TButton
ObjectInfo := GetTypeData(ObjectData); // "Wyciąganie" struktury danych
I tym sposobem możemy już wyświetlić ogólne informacje o obiekcie na przykład w kontrolce memo:
Memo.Lines.Add(ObjectData.Name); // Nazwa klasy
Memo.Lines.Add(ObjectInfo.UnitName); // Unit
Memo.Lines.Add(IntToStr(ObjectInfo.PropCount)); // Ilość właściwości
Niektóre tylko informacje RTTI mogą być uzyskane przez metody klasy TObject jak nazwa klasy (ClassName), ale np. ilość właściwości już nie.
Możemy również pokazać całą hierarchię danej klasy, wykorzystajmy do tego naszą kontrolkę:
var
bfClass: TClass;
begin
bfClass := Button1.ClassType;
Memo.Lines.Add(bfClass.ClassName);
repeat
bfClass := bfClass.ClassParent;
Memo.Lines.Add(bfClass.ClassName);
until bfClass.ClassName = 'TObject';
end;
Zmienna bfClass jest typu TClass - w naszym przykładzie pełni rolę swoistego bufora, który zmienia się od danej klasy aż do TObject (koniec pętli repeat), a jak wiemy TObject jest klasą bazową wszystkich klas (przynajmniej w ujęciu VCL).
Są to oczywiście tylko przykładowe, ogólne informacje o klasie - praktycznie całkowicie bezużyteczne. Dopiero manipulowanie właściwościami da nam większe pole do popisu...
W tym celu oprócz poznanych już typów PTypeInfo, PTypeData użyjemy PPropList co jak wskazuje zawierać ma listę właściwości (Property List). Zanim poznamy jak ustawiać właściwości, spróbujmy pobrać ich listę z jakiejś klasy (to znaczy: nazwę oraz typ wartości). Jak się okazuje, nie jest to takie łatwe. Wykorzystajmy do tego nasz Button1...
var
ObjectData: PTypeInfo;
ObjectInfo: PTypeData;
PropList: PPropList;
i: Integer;
begin
ObjectData := Button1.ClassInfo;
ObjectInfo := GetTypeData(ObjectData);
GetMem(PropList, SizeOf(PPropInfo) * ObjectInfo.PropCount);
try
GetPropInfos(Button1.ClassInfo, PropList);
for i := 0 to ObjectInfo.PropCount - 1 do
if not (PropList[i]^.PropType^.Kind = tkMethod) then // odrzucenie wszystkich wlasciwosci zdarzeniowych
Memo.Lines.Add(PropList[i]^.Name + ' : ' + PropList[i]^.PropType^.Name);
finally
FreeMem(PropList, SizeOf(PPropInfo) * ObjectInfo.PropCount);
end;
end;
Jak widzimy cała procedura jest dość skomplikowana. Jeżeli byśmy chcieli sprawdzić, czy dana właściwość występuje w klasie, skorzystamy z takiej funkcji:
Function HasProp(MyComponent: TComponent; Prop: String): Boolean;
var
PropInfo: PPropInfo;
begin
PropInfo := GetPropInfo(MyComponent.ClassInfo, Prop);
Result := (PropInfo <> NIL);
end;
Funkcja zwraca wartość typu Boolean, czy właściwość nazwana jak parametr Prop istnieje w klasie MyComponent. Użyty tu został nowy typ PPropInfo zawierający informacje o właściwości.
Skoro Delphi ma możliwość odczytu właściwości z klas, można je zapisywać. Zapis taki można zrealizować w postaci procedury:
procedure SetProp(MyComponent: TComponent; Prop: String; Value: Integer);
Ograniczymy się póki co do typu Integer. Sama procedure nie wymaga specjalnie dużo wysiłku:
procedure SetProp(MyComponent: TComponent; Prop: String; Value: Integer);
var
PropInfo: PPropInfo;
begin
PropInfo := GetPropInfo(MyComponent.ClassInfo, Prop);
if PropInfo <> NIL then
begin
if PropInfo^.PropType^.Kind = tkInteger
then
SetOrdProp(MyComponent, PropInfo, Integer(Value));
end;
end;
Parametry przekazywane do procedury SetProp są chyba oczywiste, nie wymagają większych wyjaśnień. Procedura ta pierw sprawdza czy w ogóle dana właściwość istnieje w klasie, później dopiero, czy przekazywana wartość jest typu Integer, a samo przypisanie następuje na samym końcu. Dzieje się to za pomocą funkcji SetOrdProp właśnie do tego celu stworzonej. Aby teraz zobaczyć dzieło w akcji postawmy dwa Edit`y nazwane odpowiednio: eProp oraz eValue. Pod zdarzenie Button1.OnClick dopiszmy:
procedure TForm1.Button1Click(Sender: TObject);
begin
SetProp(Button1, eProp.Text, StrToInt(eValue.Text));
end;
Teraz po kliknięciu na Button1 będzie można dowolnie zmieniać jego właściwości. Przykładowo wpiszcie w eProp 'Width' a w eValue '100' - uzyskamy zmianę szerokości naszego przycisku. Możemy manipulować właściwościami dowolnego komponentu, nie tylko Button1, na przykład zamieniając parametr procedury w Sender.
Możemy też rozszerzyć możliwości modyfikowania właściwości o typ string, wystarczy dopisać drugą podobną procedurę i obie opatrzyć klauzulą overload. Pamiętajmy jednak, że funkcja SetOrdProp jest tylko dla typu Integer, ale dostępne są też jej odpowiedniki: SetStrProp() - string, SetFloatProp() - typ zmiennoprzecinkowy, SetVariantProp() - typ wariantowy, SetObjectProp() - obiekt (gdyż jak wiemy czasem właściwością jest obiekt np. czcionka to klasa TFont)
Warto zagłębić się też w PropInfo.PropType.Kind oznaczający typ właściwości. Typ ten zawiera:
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString, tkVariant, tkArray, tkRecord, tkInterface, tkDynArray);
Korzystając z innych typów TTypeKind możemy określać przekazywane wartości.. nie tylko Integer bądź String.
A jakie może być praktyczne zastosowanie takich "kombinacji"? Otóż na pewno w nie jednym programie znalazło by swoje zastosowanie, chociażby przy zapisie ustawień do pliku INI - teraz możemy odczytywać całe linie w INI i za pomocą jednej procedury wszystko ustawiać, a sam odczyt z INI działał by jako swoisty "kompilator" na bieżąco realizując zawarte w nim ustawienia. Oczywiście takich zastosować jest o wiele więcej - to był tylko przykład.
Art pisany w oparciu o książke "Delphi 6: Vademecum profesjonalisty"