obsługa danych własnego typu
Artykuł ten zawiera szczegółowy opis sposobu rozszerzenia klasy TSimpleTSInfoTree, aby umożliwić zapis danych własnego typu do atrybutów i ich późniejszy odczyt. Do tego celu zaprezentowane są dwie klasy typu helper, pozwalające na rozszerzenie funkcjonalności bazowej klasy, bez konieczności modyfikacji kodu modułów biblioteki.
W opisach używania obiektów drzew konfiguracyjnych używany jest poniższy termin:
- tsiConfig — zmienna zawierająca referencję do instancji klasy TSimpleTSInfoTree, używana w przykładowych kodach.
Aby móc używać bazowej klasy do obsługi plików, do sekcji uses musi zostać dodany moduł TSInfoFiles. W przedstawionych kodach wykorzystywane są elementy ze wszystkich modułów biblioteki. Aby móc z nich skorzystać, moduły te także muszą zostać dodane do sekcji uses.
W przykładach użyte są nazwy plików, zawierające rozszerzenie .tsinfo. Rozszerzenie to jest umowne i nieobowiązkowe, stąd pliki zawierające drzewa TreeStructInfo mogą posiadać inne rozszerzenia, bądź nie posiadać ich w ogóle. Nie wpłynie to w żaden sposób na poprawność funkcjonowania mechanizmów biblioteki.
spis treści
1. własny typ danych
Załóżmy, że projektem jest zręcznościowa gra typu Bomberman. W projekcie tym potrzebny jest odpowiedni typ danych, umożliwiający opisanie i rozróżnienie wszystkich możliwych rodzajów przeciwników. Do tego celu wybrany zostaje typ wyliczeniowy, posiadający tyle wartości, ile faktycznie rodzajów potworków wspomniana gra obsługuje.
Projekt ten posiada moduł GameTypes, w którym to zadeklarowany jest odpowiedni typ danych:
type TEnemyKind = (ekBallom, ekOneal, ekDoll, ekMinvo, ekKondoria, ekOvapi, ekPass, ekPontan);
Dodatkowo zadeklarowany jest także typ określający zbiór powyższych wartości wyliczeniowych:
type TEnemiesSet = set of TEnemyKind;
Służy on przede wszystkim do przechowywania listy wszystkich dostępnych potworków w danym poziomie gry. Rozszerzenie funkcjonalności bazowych klas dotyczyć będzie właśnie zapisu i odczytu zbioru typu TEnemiesSet.
2. dodatkowy moduł
Aby rozszerzenie funkcjonalności było wygodne i czytelne, zalecane jest stworzenie dodatkowego modułu, w którym zostaną zaimplementowane klasy typu helper. Sugerowana nazwa takiego modułu to TSInfoHelpers, na podobieństwo nazw istniejących modułów w oficjalnej bibliotece (TSInfoConsts, TSInfoTypes, TSInfoUtils oraz TSInfoFiles).
Aby móc korzystać z rozszerzonej funkcjonalności bazowych klas do obsługi plików konfiguracyjnych, moduł TSInfoHelpers wystarczy dodać do sekcji uses w danym module własnego projektu, w którym potrzeba wykorzystania nowych metod będzie wymagana.
Rama omawianego modułu powinna wyglądać następująco:
unit TSInfoHelpers; {$mode objfpc}{$H+} interface uses TSInfoConsts, TSInfoTypes, TSInfoUtils, TSInfoFiles, TypInfo, GameTypes; implementation end.
Poszczególne wartości wyliczeniowe w docelowym łańcuchu znaków (jako wartości atrybutu) powinny być rozdzielone konkretnym znakiem lub ciągiem znaków separatora. Skorzystać można ze znaku separatora współrzędnych punktu zawartego w stałej COORDS_DELIMITER lub wykorzystać inny, według własnego uznania.
Przykładowym separatorem wartości będzie sekwencja , , zadeklarowana w poniższy sposób:
const ENEMIES_DELIMITER = ', ';
2.1. klasa dodająca metody konwersji
Pierwszym krokiem w procesie rozszerzania funkcjonalności bazowych klas o obsługę własnego typu danych jest utworzenie klasy typu helper dla bazowej klasy konwertera danych, czyli klasy TTSInfoDataConverter. Deklarację tej klasy należy umieścić we wcześniej utworzonym module o nazwie TSInfoHelpers, według poniższego schematu:
type TTSInfoDataConverterHelper = class helper for TTSInfoDataConverter end;
2.1.1. metoda konwersji natywnych danych na łańcuch znaków
Znając docelowy typ danych, dla którego funkcjonalność zostanie rozszerzona, kolejnym krokiem jest przygotowanie metody umożliwiającej konwersję natywnych danych na łańcuch znaków. Zalecaną nazwą dla nowej metody jest EnemiesToValue, na podobieństwo istniejących w klasie TTSInfoDataConverter metod do konwersji danych na ciągi znaków. Metoda ta powinna być statyczna, tak samo jak pozostałe metody zawarte w tej klasie.
Zadaniem metody będzie konwersja wejściowego zbioru i zwrócenie wartości jako pojedynczego łańcucha znaków. Do tego celu można skorzystać z metody funkcyjnej, zadeklarowanej w sekcji public i posiadającej tylko jeden argument:
type TTSInfoDataConverterHelper = class helper for TTSInfoDataConverter public class function EnemiesToValue(AEnemies: TEnemiesSet): String; end; {..} class function TTSInfoDataConverterHelper.EnemiesToValue(AEnemies: TEnemiesSet): String; var ekEnemyIdx: TEnemyKind; begin Result := ''; for ekEnemyIdx in AEnemies do Result += GetEnumName(TypeInfo(TEnemyKind), Ord(ekEnemyIdx)) + ENEMIES_DELIMITER; SetLength(Result, Length(Result) - Length(ENEMIES_DELIMITER)); end;
Powyższa metoda pobiera zbiór typu TEnemiesSet w parametrze AEnemies i konwertuje go na łańcuch znaków, który to zwraca w rezultacie. Wartości w łańcuchu będą rozdzielone ciągiem , . Jeżeli do metody podany zostanie pusty zbiór, zwróci ona pusty łańcuch znaków.
2.1.2. metoda konwersji ciągu znaków na dane natywne
Konwersja natywnych danych na ciąg znaków nie wystarczy — potrzebna jest jeszcze metoda, która umożliwi dekonwersję łańcucha i pozyskanie natywnego zbioru wartości wyliczeniowych typu TEnemiesSet. Zalecaną nazwą takiej metody jest ValueToEnemies, na podobieństwo do pozostałych metod z klasy TTSInfoDataConverter, umożliwiających konwersję wejściowego ciągu znaków na dane natywne.
Zadaniem wymienionej metody będzie konwersja wejściowego łańcucha znaków na zbiór wartości wyliczeniowych. Dodatkowo, powinna także przyjmować domyśli zbiór, który zostanie zwrócony podczas niepowodzenia konwersji. Tak więc metoda ta powinna przyjmować dwa argumenty i tak samo jak poprzednia, być zadeklarowana jako publiczna metoda statyczna:
type TTSInfoDataConverterHelper = class helper for TTSInfoDataConverter public {..} class function ValueToEnemies(const AValue: String; ADefault: TEnemiesSet): TEnemiesSet; end; {..} class function TTSInfoDataConverterHelper.ValueToEnemies(const AValue: String; ADefault: TEnemiesSet): TEnemiesSet; var vcEnemies: TValueComponents; strRawValue, strEnemyName: String; intEnemiesCnt, intEnumValue: Integer; begin Result := []; strRawValue := ReplaceSubStrings(AValue, ENEMIES_DELIMITER, VALUES_DELIMITER); ExtractValueComponents(strRawValue, vcEnemies, intEnemiesCnt); if intEnemiesCnt > 0 then for strEnemyName in vcEnemies do begin intEnumValue := GetEnumValue(TypeInfo(TEnemyKind), strEnemyName); if intEnumValue = -1 then Exit(ADefault) else Include(Result, TEnemyKind(intEnumValue)); end; end;
Metoda pobiera wejściowy ciąg znaków w argumencie AValue oraz domyślny zbiór typu TEnemiesSet w parametrze ADefault. Rozdziela składowe ciągu wartości, dodając je do macierzy typu TValueComponents, po czym konwertuje uzyskane podciągi na wartości liczbowe i po rzutowaniu dodaje je do wyjściowego zbioru.
Jeżeli wejściowy ciąg znaków z parametru AValue jest pusty, metoda zwraca pusty zbiór, w przeciwnym razie zwraca zbiór uzupełniony w konkretne wartości. Jeżeli podczas próby konwersji wystąpi błąd (dany podciąg nie zawiera prawidłowej nazwy wartości wyliczeniowej), metoda zwraca zbiór domyślny, podany w argumencie ADefault.
2.2. klasa dodająca metody odczytu i zapisu danych
Drugim krokiem procesu rozszerzania funkcjonalności jest stworzenie klasy typu helper dla klasy TSimpleTSInfoTree. Pozwoli to na dodanie do niej nowych metod, umożliwiających wewnętrzną konwersję danych natywnych na ciągi znaków (i odwrotnie) oraz zapisujących i odczytujących dane do i z atrybutów. Deklarację tej klasy także należy umieścić w module TSInfoHelpers, według poniższego schematu:
type TTSInfoTreeHelper = class helper for TSimpleTSInfoTree end;
2.2.1. metoda zapisująca dane do atrybutu
W pierwszej kolejności dodajemy metodę, która umożliwi zapis zbioru typu TEnemiesSet do konkretnego atrybutu. Nazwa tej metody powinna być podobna do pozostałych, zawartych w klasie TSimpleTSInfoTree (np. WriteEnemies). Tak samo jak pozostałe, powinna być umieszczona w sekcji public.
Zadaniem metody będzie pobranie ścieżki lub nazwy atrybutu, do którego zostanie zapisany zbiór wartości. Nie powinna zwracać żadnych wartości, więc można zadeklarować ją jako metodę proceduralną, w poniższy sposób:
type TTSInfoTreeHelper = class helper for TSimpleTSInfoTree public procedure WriteEnemies(const AAttrPath: String; AEnemies: TEnemiesSet); end; {..} procedure TTSInfoTreeHelper.WriteEnemies(const AAttrPath: String; AEnemies: TEnemiesSet); var attrWrite: TTSInfoAttribute; begin if FReadOnly then ThrowException(EM_READ_ONLY_MODE_VIOLATION) else begin attrWrite := FindAttribute(ExcludeTrailingIdentsDelimiter(AAttrPath), True); if attrWrite <> nil then begin attrWrite.Value := TTSInfoDataConverter.EnemiesToValue(AEnemies); FModified := True; end; end; end;
Kod powyższej metody działa dokładnie w taki sam sposób, jak kod pozostałych istniejących metod zapisujących.
W pierwszej kolejności sprawdzany jest stan trybu tylko do odczytu i jeśli jest włączony, za pomocą procedury ThrowException tworzony jest wyjątek o odpowiedniej treści. Następnie wyszukiwany jest atrybut o ścieżce lub nazwie podanej w parametrze AAttrPath — jeśli nie istnieje, zostaje on utworzony. Kolejnym krokiem jest konwersja wejściowego zbioru z parametru AEnemies na łańcuch znaków (za pomocą wcześniej przygotowanej metody) oraz przypisanie go do wartości docelowego atrybutu. Na koniec wykonywana jest modyfikacja wartości pola FModified.
2.2.2. metoda odczytująca dane z atrybutu
Ostatnim krokiem procesu rozszerzania funkcjonalności jest implementacja metody, pozwalającej na odczyt danych typu TEnemiesSet, zawartych w danym atrybucie. Identyfikator tej metody również powinien być podobny do identyfikatorów pozostałych metod z klasy TSimpleTSInfoTree, umożliwiających odczyt danych z atrybutów. Sugerowanym identyfikatorem jest ReadEnemies i tak samo jak WriteEnemies, powinna być umieszczona w sekcji public.
Zadaniem metody będzie pobranie w parametrze ścieżki lub nazwy atrybutu, z którego dane będą odczytywane. Dodatkowo, w drugim parametrze, powinna umożliwiać podanie domyślnego zbioru znaków, zwracanego po nieodnalezieniu docelowego atrybutu lub błędu konwersji ciągu znaków wartości atrybutu na dane natywne. Z racji tej, że metoda musi zwracać zbiór odczytany lub domyślny, można ją zadeklarować jako funkcyjną, np. w poniższy sposób:
type TTSInfoTreeHelper = class helper for TSimpleTSInfoTree public {..} function ReadEnemies(const AAttrPath: String; ADefault: TEnemiesSet = []): TEnemiesSet; end; {..} function TTSInfoTreeHelper.ReadEnemies(const AAttrPath: String; ADefault: TEnemiesSet = []): TEnemiesSet; var attrRead: TTSInfoAttribute; begin attrRead := FindAttribute(ExcludeTrailingIdentsDelimiter(AAttrPath), False); if attrRead = nil then Result := ADefault else Result := TTSInfoDataConverter.ValueToEnemies(attrRead.Value, ADefault); end;
Powyższa metoda działa w identyczny sposób, jak pozostałe istniejące metody odczytujące dane.
Najpierw wyszukiwany jest atrybut w drzewie o ścieżce lub nazwie podanej w parametrze AAttrPath. Następnie sprawdzane jest, czy atrybut został odnaleziony i jeśli nie, nie zostaje on utworzony. Metoda w takim przypadku zwraca zbiór domyślny, przekazany w argumencie ADefault. Jeżeli atrybut istnieje w drzewie, ostatnim krokiem jest konwersja ciągu znaków na zbiór typu TEnemiesSet (za pomocą uprzednio przygotowanej metody konwertującej) i przypisanie go rezultatu.
3. kod całego dodatkowego modułu
unit TSInfoHelpers; {$mode objfpc}{$H+} interface uses TSInfoConsts, TSInfoTypes, TSInfoUtils, TSInfoFiles, TypInfo, GameTypes; const ENEMIES_DELIMITER = ', '; type TTSInfoDataConverterHelper = class helper for TTSInfoDataConverter public class function EnemiesToValue(AEnemies: TEnemiesSet): String; class function ValueToEnemies(const AValue: String; ADefault: TEnemiesSet): TEnemiesSet; end; type TTSInfoTreeHelper = class helper for TSimpleTSInfoTree public procedure WriteEnemies(const AAttrPath: String; AEnemies: TEnemiesSet); function ReadEnemies(const AAttrPath: String; ADefault: TEnemiesSet = []): TEnemiesSet; end; implementation class function TTSInfoDataConverterHelper.EnemiesToValue(AEnemies: TEnemiesSet): String; var ekEnemyIdx: TEnemyKind; begin Result := ''; for ekEnemyIdx in AEnemies do Result += GetEnumName(TypeInfo(TEnemyKind), Ord(ekEnemyIdx)) + ENEMIES_DELIMITER; SetLength(Result, Length(Result) - Length(ENEMIES_DELIMITER)); end; class function TTSInfoDataConverterHelper.ValueToEnemies(const AValue: String; ADefault: TEnemiesSet): TEnemiesSet; var vcEnemies: TValueComponents; strRawValue, strEnemyName: String; intEnemiesCnt, intEnumValue: Integer; begin Result := []; strRawValue := ReplaceSubStrings(AValue, ENEMIES_DELIMITER, VALUES_DELIMITER); ExtractValueComponents(strRawValue, vcEnemies, intEnemiesCnt); if intEnemiesCnt > 0 then for strEnemyName in vcEnemies do begin intEnumValue := GetEnumValue(TypeInfo(TEnemyKind), strEnemyName); if intEnumValue = -1 then Exit(ADefault) else Include(Result, TEnemyKind(intEnumValue)); end; end; procedure TTSInfoTreeHelper.WriteEnemies(const AAttrPath: String; AEnemies: TEnemiesSet); var attrWrite: TTSInfoAttribute; begin if FReadOnly then ThrowException(EM_READ_ONLY_MODE_VIOLATION) else begin attrWrite := FindAttribute(ExcludeTrailingIdentsDelimiter(AAttrPath), True); if attrWrite <> nil then begin attrWrite.Value := TTSInfoDataConverter.EnemiesToValue(AEnemies); FModified := True; end; end; end; function TTSInfoTreeHelper.ReadEnemies(const AAttrPath: String; ADefault: TEnemiesSet = []): TEnemiesSet; var attrRead: TTSInfoAttribute; begin attrRead := FindAttribute(ExcludeTrailingIdentsDelimiter(AAttrPath), False); if attrRead = nil then Result := ADefault else Result := TTSInfoDataConverter.ValueToEnemies(attrRead.Value, ADefault); end; end.
4. przykład wykorzystania nowej funkcjonalności
W poniższych punktach podane są proste przykłady, wykorzystujące nowe możliwości. Aby było możliwe skorzystanie z metod obsługujących dane typu TEnemiesSet, przygotowany moduł TSInfoHelpers musi zostać dodany do sekcji uses. Aby mieć dostęp do typu TEnemiesSet, moduł GameTypes również należy dołączyć do listy.
4.1. zapis danych do atrybutu
Do zapisu zbioru wartości wyliczeniowych należy skorzystać z przygotowanej metody WriteEnemies. W parametrze AAttrName przekazać należy ścieżkę lub nazwę atrybutu, natomiast w argumencie AEnemies podać zbiór typu TEnemiesSet do zapisu.
Poniżej przykład zapisu pustego zbioru:
tsiConfig.WriteEnemies('Enemies', []);
Rezultatem wywołania powyższej metody będzie pusta wartość atrybutu Enemies:
attr Enemies ""
Natomiast jeżeli w parametrze AEnemies zbiór nie będzie pusty, zostanie on przekonwertowany i zapisany w zadanym atrybucie:
tsiConfig.WriteEnemies('Enemies', [ekBallom, ekOneal, ekKondoria, ekPontan]);
Deklaracja atrybutu w pliku po wpisaniu nowej wartości wyglądać będzie następująco:
attr Enemies "ekBallom, ekOneal, ekKondoria, ekPontan"
4.2. odczyt danych z atrybutu
Aby odczytać z atrybutu uprzednio zapisany zbiór wartości, skorzystać należy z przygotowanej metody ReadEnemies. Metoda ta przyjmuje w parametrze AAttrPath ścieżkę lub nazwę atrybutu oraz domyślny zbiór typu TEnemiesSet w argumencie ADefault.
Poniżej deklaracja przykładowego atrybutu, zawierającego zapis zbioru wartości wyliczeniowych:
attr Enemies "ekDoll, ekMinvo, ekOvapi"
Aby odczytać zbiór z powyższego atrybutu, w przypadku gdy znajduje się on w aktualnie otwartym lub głównym węźle drzewa, w parametrze AAttrPath przekazać należy samą jego nazwę, natomiast w parametrze ADefault określić zbiór domyślny:
var ekInLevel: TEnemiesSet; {..} ekInLevel := tsiConfig.ReadEnemies('Enemies', [ekPass]);
Jeżeli atrybut nie jest zawarty w aktualnie otwartym lub głównym węźle, w argumencie AAttrPath należy podać jego pełną ścieżkę:
ekInLevel := tsiConfig.ReadEnemies('Level 4\Enemies', [ekPass]);
Tak wywołana metoda spowoduje odczytanie wartości ekDoll, ekMinvo i ekOvapi oraz zwrócenie ich w postaci zbioru.
Jeśli atrybut nie będzie istniał w drzewie lub jego wartość nie będzie zawierać prawidłowego zapisu wartości zbioru, metoda zwróci zbiór domyślny, w tym przypadku zawierający jedynie wartość ekPass.
5. podsumowanie
Tak przygotowane klasy typu helper oraz zadeklarowane w nich metody, umożliwiają rozszerzenie funkcjonalności klas do obsługi plików konfiguracyjnych TreeStructInfo o obsługę dowolnego typu danych. Podczas dodawania obsługi własnych typów danych, należy pamiętać o tym, aby:
- przygotować typ danych, którego obsługę chcemy dodać,
- stworzyć dodatkowy moduł — np. TSInfoHelpers — służący m.in. do deklaracji klas typu helper,
- rozszerzyć funkcjonalność klasy TTSInfoDataConverter, dodając metody konwersji natywnych danych na ciąg znaków oraz łańcucha znaków na dane natywne (obie jako metody statyczne w sekcji public),
- rozszerzyć funkcjonalność klasy TSimpleTSInfoTree, dodając metody do zapisu natywnych danych do zadanego atrybutu oraz do odczytu danych z atrybutu (obie w sekcji public),
- zachować zgodność nazewnictwa z już istniejącymi metodami,
- zachować zgodność ich działania z działaniem istniejących metod zapisujących i odczytujących dane do i z atrybutów,
- dodać moduł TSInfoHelpers do listy uses, przed użyciem nowych elementów.