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:

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

Rozszerzanie funkcjonalności klasy TTSInfoDataConverter nie jest konieczne w przypadku, gdy metody konwersji natywnych danych własnego typu na łańcuch znaków (i vice versa) istnieją już w kodzie własnego projektu. W takim przypadku wystarczy od razu przejść do punktu 2.2., czyli rozszerzania funkcjonalności klasy TSimpleTSInfoTree.

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:

copyright © furious programming 2013—2018