furious programming
2018-02-18 12:14

Dziś co nieco na temat komponentów, a dokładniej, o tekstowych właściwościach i ich przechowywaniu.

Opublikowane właściwości komponentów mogą posiadać wartości domyślne – dzięki temu nie są one zapisywane w plikach .lfm. To definiuje się za pomocą słówka default na końcu deklaracji właściwości. Właściwości tekstowe (typu String) też mogą posiadać wartości domyślne, nie składowane w plikach .lfm, jednak składnia jest nieco inna.

Otóż użyć należy słówka stored, a po nim podać wartość logiczną lub metodę zwracającą dane tego typu. Przykład:

published
  property Foo: String read FFoo write SetFoo stored True;

Powyższe oznacza, że wartość zawsze będzie przechowywana w pliku zasobów. O wiele częściej spotykaną praktyką jest używanie metody w stylu Is*Stored, zamiast literałów:

private
  function IsFooStored(): Boolean;
published
  property Foo: String read FFoo write SetFoo stored IsFooStored;

Metoda IsFooStored może wyglądać np. tak:

function TMyControl.IsFooStored(): Boolean;
begin
  Result := FFoo <> 'foo';
end;

A co jeśli mamy kilka właściwości tekstowych? Pasuje je zgrupować, czyli zamiast tworzyć wiele osobnych pól, stworzyć jedną macierz i odwoływać się do konkretnych pól za pomocą indeksu. Preferuję enumy zamiast zwykłych liczb. Przykład:

const
  MY_CONTROL_FOO = 'foo';
  MY_CONTROL_BAR = 'bar';
  MY_CONTROL_BAZ = 'baz';
 
type
  TMyControlTextKind = (mctFoo, mctBar, mctBaz);
  TMyControlTextsArr = array [TMyControlTextKind] of String;
 
{..}
 
private
  FTexts: TMyControlTextsArr;
private
  function GetText(AKind: TMyControlTextKind): String;
  procedure SetText(AKind: TMyControlTextKind; const AText: String);
published
  property Foo: String index mctFoo read GetText write SetText;
  property Bar: String index mctBar read GetText write SetText;
  property Baz: String index mctBaz read GetText write SetText;

Miodzio. Teraz czas na wartości domyślne. W dokumentacji czytamy (w jakże zacnym punkcie):

6.6.6 Storage information
The stored specifier should be either a boolean constant, a boolean field of the class, or a parameterless function which returns a boolean result. […]

Czyli dla każdej właściwości trzeba zrobić osobną metodę Is*Stored… Odczyt i zapis dało się ładnie uprościć za pomocą indeksowania, a tu taka lipa… Na szczęście przywykłem do tego, aby nie wierzyć dokumentacji (tym bardziej, że najnowsza wersja kompilatora to 3.0.4, a dokumentacja jest dla 3.0.2), więc sprawdziłem, czy faktycznie metoda Is*Stored musi być bezparametrowa:

const
  MY_CONTROL_TEXTS: TMyControlTextsArr = (MY_CONTROL_FOO, MY_CONTROL_BAR, MY_CONTROL_BAZ);
 
{..}
 
private
  function IsTextStored(AKind: TMyControlTextKind): Boolean;
published
  property Foo: String index mctFoo read GetText write SetText stored IsTextStored;
  property Bar: String index mctBar read GetText write SetText stored IsTextStored;
  property Baz: String index mctBaz read GetText write SetText stored IsTextStored;
 
{..}
 
function TMyControl.IsTextStored(AKind: TMyControlTextKind): Boolean;
begin
  Result := FTexts[AKind] <> MY_CONTROL_TEXTS[AKind];
end;

No i działa. Nie tylko kompiluje się bezbłędnie, ale i działa właściwie – poprawnie waliduje ciągi i do pliku .lfm zapisuje je wyłącznie wtedy, gdy różnią się od wartości uznawanych za domyślne (czyli tych ze stałych). To miło, bo dzięki temu można mieć wiele właściwości łańcuchowych w jednej klasie, ale tylko jedną metodę określającą czy posiadają domyślne dane (i tak samo: jedną do odczytu konkretnego ciągu i jedną do zapisu).

Jak to dobrze być dociekliwym. :D

#free-pascal #lazarus

furious programming

Nazwa kategorii dotyczy języków programowania, nie narzędzi. Obecna informuje, że zawiera wątki dotyczące Delphi i wszystkich innych języków wywodzących się z klasycznego Pascala. Tak samo sprawa wygląda np. z kategorią C/C++.

Natomiast całkiem niedawno uzupełniałem opisy kategorii (widoczne na stronie z ich listą), dodając informację o Free Pascalu, Lazarusie, Code Typhoon i Oxygene. Tego brakowało już od dawna, więc trzeba było co nieco poprawić.

cerrato

To jeszcze można byłoby zrobić małą dyskusję, jak traktować Delphi. Moim zdaniem (aczkolwiek są różne opinie/podejścia, więc nie musicie się zgadzać) w przypadku Delphi językiem jest Object Pascal, a słowo Delphi oznacza właśnie tą całą "otoczkę" - głównie IDE i VCL, czyli to co nazwałeś "narzędziem".

furious programming
2018-01-27 00:47

Kolejny wpis na temat ciekawostek Free Pascala.

Jak wiadomo, FPC wspiera szereg operatorów – kupkę standardowych i kupkę innych, zdefiniowanych i nie. Jakiś czas temu umieściłem wpis na temat niezdefiniowanego operatora ** i tego w jaki sposób uczynić z niego pożytek (czyli jak zdefiniować jego zachowanie, tak aby faktycznie umożliwił potęgowanie). Jednak ciekawych operatorów jest więcej.


Oczy kosmity

Jednym z dziwnych operatorów i raczej niewystępujących w tych najpopularniejszych językach programowania jest operator ><. Póki co nie posiada on przyporządkowanej funkcji, więc aby go użyć, należy sobie taką napisać. Co można nim zrobić? Choć dla kompilatora jest to kolejny operator nierówności (funkcja definiująca zachowanie w internalsach istnieje jako $sym_diff), można go użyć do czegokolwiek, byle do wyrażenia wymagającego dwóch operandów. Zwrócić może cokolwiek – wartość logiczną, liczbę, obiekt itd.

Operator ten wygląda jak dwie linie przecinające się w punkcie pośrodku, więc dla przykładu zdefiniujmy funkcję, której zadaniem będzie sprawdzenie, czy lewy operand jest lustrzanym odbiciem prawego:

{$INLINE ON}
 
operator >< (const ALeft, ARight: UInt8): Boolean; inline;
begin
  Result := High(UInt8) - ALeft = ARight;
end;

Przykład użycia:

WriteLn('Mirrored: ', 0 >< 255);   // True
WriteLn('Mirrored: ', 239 >< 16);  // True
WriteLn('Mirrored: ', 200 >< 47);  // False

Takie cudo, choć w normalnym kodzie mógłby się przydać do czegoś sensowniejszego. :]


Więcej C nie zaszkodzi

Składnia języków wywodzących się z dziadka C wsiąkła w składnię Free Pascala nieco bardziej, niż sądziłem. Oprócz zapożyczonych z C operatorów do skróconego zapisu inkrementacji, dekrementacji, mnożenia i dzielenia, razem z przypisaniem – czyli odpowiednio +=, -=, *= i /* – wspierane są też operatory << i >> do przesunięć bitowych w lewo i prawo. Te dwa są już na stałe wbudowane, a więc dostępne zawsze, bez względu na tryb kompatybilności (przełącznik {$MODE}) oraz stan opcji -Sc (lub inaczej {$COPERATORS}).

Tak więc jeśli komuś nie podobają się słowne operatory shl i shr to może użyć strzałek. Niestety, ale nie można ich przeciążać.


Dodatek: identyfikator rezultatu

Jak wiadomo, każda funkcja z automatu (oprócz trybu zgodności z dialektem TP) posiada ukrytą zmienną Result, do której można przypisywać dane, które owa funkcja ma zwrócić. Można, bo w dalszym ciągu istnieje możliwość użycia do tego celu identyfikatora funkcji, jak w TP. W przypadku funkcji przeciążających operatory, składnia wymaga podania operatora w miejsce nazwy funkcji (jak w przykładzie wyżej).

Jeśli mowa o funkcjach przeciążających operatory, istnieje możliwość użycia niejawnej zmiennej Result:

operator >< (const ALeft, ARight: UInt8): Boolean; inline;
begin
  Result := High(UInt8) - ALeft = ARight;
end;

Ale nie jest ona jedyną – można też zdefiniować własną nazwę elementu, który przyjmować będzie i zwracać dane. W takim wypadku należy podać dowolny ciąg (ale zgodny z wymaganiami co do identyfikatorów) po nawiasie zamykającym listę parametrów. Od teraz tej nazwy można użyć zamiast Result:

operator >< (const ALeft, ARight: UInt8) Mirrored: Boolean; inline;
begin
  Mirrored := High(UInt8) - ALeft = ARight;
end;

Oczywiście mechanizm code insight w Lazarusie nie ogarnia w takiej funkcji ani nazwy Result, ani też tej własnoręcznie podanej. Nie wiem dlaczego, ale nie podaje ich na liście, więc trzeba je pisać samemu w całości. Takie to IDE uparte. ;)

#free-pascal #lazarus

furious programming
2018-01-18 21:12

Kilka dni temu czytałem co nieco o Lazarusie i Delphi, różne artykuły o różnych rzeczach, kontrolki, biblioteki, itd. Artykuł na temat XML Data Binding zainspirował mnie – nie do użycia XML-a, a do napisania małego bindingu do pliku ustawień swojego projektu. Mowa tutaj o formacie TreeStructInfotutaj możecie zobaczyć jak wygląda przykładowe drzewko.

Uprzedzam, że wpis ten jest długi, więc TL;DR – kod całego modułu wrzucam do naszego pastebin, w razie gdyby ktoś nie miał czasu czytać całości, a chciał zobaczyć jak to wygląda. ;)


API do drzew tsinfo jest silnie hermetyczne – wszystko bazuje na ścieżkach do elementów. Tak więc nie da się zrobić tego w taki sposób, jak robi to wizard od Delphi dla XML.

Kolejne problemy – nie dotyczące mojej biblioteki – to mnogość getterów i setterów, jaka generowana jest dla XML-a. Każda końcowa właściwość posiada własny akcesor i mutator. Skoro w całym drzewie konfiguracji mam wiele atrybutów przechowujących dane tego samego typu, to w zupełności wystarczy mi jedna metoda do odczytu danych i jedna do zapisu. Typ danych jest ten sam – jedyną różnicą są ścieżki do elementów.

Ostatnia rzecz, której nie chcę to globalne procedury i funkcje. Całość ma się sprowadzać do utworzenia jednego obiektu.


Zacząć należy od wymagań i elementów narzuconych przez bibliotekę, a konkretniej przez klasę TTSInfoTree.

Każda natywna właściwość musi posiadać dostęp do obiektu drzewa. Tak więc każdy węzeł musi przechowywać do niego referencję i dlatego pobiera ją w konstruktorze. Każdy setter i getter musi znać ścieżkę atrybutu. Nie da się jej określić z poziomu gettera/settera, więc każdy węzeł również ją pobiera w konstruktorze.

Aby móc uzyskać dostęp do konkretnego atrybutu, potrzebna jest też jego nazwa. Z poziomu metody dostępowej i zmieniającej nie da się okreslić nazwy właściwości, która aktualnie z tej metody korzysta. Dlatego też sama właściwość musi takie dane przekazać do gettera/settera. Aby móc korzystać z uniwersalnych metod do odczytu i zapisu danych, nazwy atrybutów trzeba przechować – wystarczy prosta lista stringów.


Bazowa klasa węzła:

type
  TConfigurationNode = class(TObject)
  private type
    TAttrNames = specialize TFPGList<String>;
  private
    FConfigTree: TTSInfoTree;
    FAttrNames: TAttrNames;
  private
    FPath: String;
  protected
    procedure RegisterAttributes(const ANames: array of String);
  protected
    function GetBoolean(AIndex: Integer): Boolean;
    function GetInteger(AIndex: Integer): Integer;
    function GetColor(AIndex: Integer): TColor;
  protected
    procedure SetBoolean(AIndex: Integer; AValue: Boolean);
    procedure SetInteger(AIndex: Integer; AValue: Integer);
    procedure SetColor(AIndex: Integer; AValue: TColor);
  public
    constructor Create(AConfigTree: TTSInfoTree; const APath: String);
    destructor Destroy(); override;
  end;

Pola inicjalizowane są w konstruktorze – zapamiętywana jest referencja obiektu drzewa oraz ścieżka tworzonego węzła, a także tworzona jest lista nazw atrybutów. Przykładowe drzewko konfiguracji posiada dane trzech typów – Boolean, Integer i Color (jako Integer, ale zapisywany jako wartość heksadecymalna).

Implementuje uniwersalną logikę zarządzania drzewem danych i dostępu do danych atrybutów. Klasy dziedziczące z niej nie będą musiały niczego definiować.


Dzięki wrzuceniu całej logiki do bazowej klasy węzła, implementacja każdej skonkretyzowanej klasy węzła końcowego (czyli takiego, który posiada wyłącznie atrybuty, bez węzłów potomnych), ograniczy się do samej deklaracji właściwości. Przykład:

type
  TCasesNode = class(TConfigurationNode)
  public
    property Quit: Boolean index 0 read GetBoolean write SetBoolean;
    property NewProject: Boolean index 1 read GetBoolean write SetBoolean;
    property SaveSchema: Boolean index 2 read GetBoolean write SetBoolean;
    property RestoreSchema: Boolean index 3 read GetBoolean write SetBoolean;
    property OpenHelp: Boolean index 4 read GetBoolean write SetBoolean;
  end;

Pięć właściwości, wszystkie korzystają z tego samego zestawu dwóch metod do odczytu i zapisu danych. Indeksy używane są wewnątrz setterów i getterów do pobrania nazwy atrybutu, z listy zawartej w polu FAttrNames.


Węzły potomne muszą być przechowywane, ale nie ma możliwości trzymania ich w jednej liście (inne typy). Przez to dla każdego takiego węzła potrzebne jest osobne pole. Przykład:

type
  TWindowsNode = class(TConfigurationNode)
  private
    FMainNode: TMainNode;
    FSettingsNode: TSettingsNode;
  public
    constructor Create(AConfigTree: TTSInfoTree; const APath: String);
    destructor Destroy(); override;
  public
    property Main: TMainNode read FMainNode;
    property Settings: TSettingsNode read FSettingsNode;
  end;

Klasa węzła nadrzędnego musi ten węzeł utworzyć, przekazać mu dane, a także zarejestrować nazwy jego atrybutów:

constructor TThemeNode.Create(AConfigTree: TTSInfoTree; const APath: String);
begin
  inherited Create(AConfigTree, APath);
 
  FScopeNode := TScopeNode.Create(AConfigTree, APath + 'Scope\');
  FScopeNode.RegisterAttributes(['Important Windows', 'All Windows']);
end;

Kolejność nazw atrybutów musi być zgodna z indeksami podanymi w deklaracji właściwości.


Węzeł mieszany? Połączenie dwóch poprzednich:

type
  TConfirmationsNode = class(TConfigurationNode)
  private
    FCasesNode: TCasesNode;
  public
    constructor Create(AConfigTree: TTSInfoTree; const APath: String);
    destructor Destroy(); override;
  public
    property All: Boolean index 0 read GetBoolean write SetBoolean;
    property Chosen: Boolean index 1 read GetBoolean write SetBoolean;
    property None: Boolean index 2 read GetBoolean write SetBoolean;
  public
    property Cases: TCasesNode read FCasesNode;
  end;

Do zdefiniowania jest wyłącznie konstruktor i destruktor.


Aby móc wygodnie korzystać z drzewa właściwości, przyda się klasa opakowująca całość. Niżej przykład prostej klasy realizującej to zadanie:

type
  TConfiguration = class(TObject)
  private
    FConfigTree: TTSInfoTree;
    FWindowsNode: TWindowsNode;
  private
    function IsConfigLoaded(): Boolean;
    function IsConfigModified(): Boolean;
  public
    constructor Create();
    destructor Destroy(); override;
  public
    procedure Load();
    procedure Save();
  public
    property Loaded: Boolean read IsConfigLoaded;
    property Modified: Boolean read IsConfigModified;
  public
    property Windows: TWindowsNode read FWindowsNode;
  end;

Taka skromna klasa. Przykład użycia:

var
  Config: TConfiguration;
begin
  Config := TConfiguration.Create();
  try
    Config.Load();
    Config.Windows.Settings.Theme.Color := clBlue;
 
    if Config.Modified then
      Config.Save();
  finally
    Config.Free();
  end;

Podsumowanie

Nie wyszło tak źle. Stworzenie generatora wypluwającego kod dla Pascala nie było by zbyt trudne. Przyjdzie i na to czas, jednak jeszcze dużo pracy przede mną, bo biblioteka wymaga poprawienia. Dawne słabe pomysły teraz przypominają o sobie. :/


Dodatek – nieduża automatyzacja

Do metody rejestrującej nazwy atrybutów można przekazać obiekt węzła i w pętli pobrać nazwy wszystkich właściwości za pomocą RTTI (muszą być w sekcji published):

procedure TConfigurationNode.RegisterAttributes(ANode: TObject);
var
  LPropIdx, LPropCnt: Integer;
  LPropList: PPropList;
begin
  LPropCnt := GetPropList(ANode.ClassInfo, [tkBool, tkInteger], nil, False);
 
  GetMem(LPropList, LPropCnt * SizeOf(PPropInfo));
  try
    GetPropList(ANode.ClassInfo, [tkBool, tkInteger], LPropList, False);
 
    for LPropIdx := 0 to LPropCnt - 1 do
      FAttrNames.Add(LPropList^[LPropIdx]^.Name);
  finally
    FreeMem(LPropList);
  end;
end;

I to wszystko – metodę tę należy umieścić na końcu ciała konstruktora klasy TConfigurationNode. Od teraz każda dziedzicząca z niej klasa automatycznie uzupełniać będzie listę nazw swoich atrybutów.

Podczas pobierania nazw właściwości, ich lista nie może być sortowana – indeksy właściwości muszą wskazywać na prawidłowe elementy FAttrNames. Po drugie, aby móc użyć RTTI do pobrania nazw właściwości, identyfikatory atrybutów w pliku konfiguracyjnym muszą być zgodne z wytycznymi dotyczącymi nazewnictwa w Pascalu. A jeśli nie są zgodne, to muszą zostać poprawione przed ich dodaniem do FAttrNames.

#free-pascal #lazarus #treestructinfo

furious programming
2017-10-12 01:21

Co jakiś czas ktoś – celowo lub przy okazji – pyta się na forum, czy referencja w Pascalu to wskaźnik, czy nie wskaźnik. Uściślijmy więc.

Tak, referencja instancji klasy to wskaźnik, co prawda traktowany jest w odmienny sposób, jednak to nie zmienia faktu bycia wskaźnikiem. Aby sprawdzić to w praktyce, wystarczy referencję zrzutować na pointer, a ten następnie z powrotem na referencję konkretnej klasy.

Przykład testowej aplikacji poniżej:

uses
  Classes;
var
  ListObj: TStringList;
  ListPtr: Pointer;
begin
  ListObj := TStringList.Create();
  try
    ListObj.Add('free pascal');
 
    ListPtr := Pointer(ListObj);      // przerobienie referencji na wskaźnik
    ListObj := TStringList(ListPtr);  // przerobienie wskaźnika na referencję
 
    Write(ListObj.Text);
  finally
    ListObj.Free();
  end;
end.

Dwa rzutowania i wszystko jasne. Miszung ze wskazaniami nie sprawi, że w linijce z wyświetleniem zawartości listy dostaniemy wyjątek.

Gdzieniegdzie, raczej w starych kodach pisanych np. w Delphi 7, można spotkać konwersję referencji na zwykłą liczbę, w celu jej przesłania w parametrze razem z komunikatem.

Druga sprawa – czasem ktoś wpada na pomysł, aby wykorzystać zwykłą procedurę jako zdarzenie jakiegoś obiektu. I też pojawiają się różne zdania na temat tego, czy da się, czy jednak się nie da. Otóż, bezpośrednio nie da się – zmienna zdarzeniowa nie jest pointerem, więc nie można do niej przypisać adresu zwykłej procedury. A czym jest? Nie jest to jeden wskaźnik, a dwa – jeden przechowuje adres kodu do wykonania, a drugi wskazuje na dodatkowe dane.

Aby móc użyć zwykłej procedury jako zdarzenia, należy przypisać adres tej procedury do pierwszego wskaźnika. A co z drugim, tym na dane? Cóż, można w nim przesłać adres czegokolwiek – wyślijmy więc referencję obiektu, aby dopełnić pierwszą część tego wpisu. ;)

Prosty przykład:

uses
  Classes;
 
  procedure OnListChange(ASender: TObject);  // zwykła procedura, zgodna z TNotifyEvent
  var
    List: TStringList absolute ASender;
  begin
    Write(List.Text);
  end;
 
var
  List: TStringList;
  Method: TMethod;  // TMethod reprezentuje wymaganą strukturę wskaźników
begin
  List := TStringList.Create();
  try
    Method.Code := @OnListChange;  // przypisanie adresu zwykłej procedury
    Method.Data := List;           // przypisanie referencji (ta podana zostanie w parametrze ASender)
 
    List.OnChange := TNotifyEvent(Method);  // ustawienie przygotowanego zdarzenia (rzutowanie konieczne)
    List.Add('free pascal');
  finally
    List.Free();
  end;
end.

Po wywołaniu metody Add, wartość jej parametru zostanie dodana do listy, po czym instancja odpali zdarzenie OnChange, o ile zostało ustawione. My to zrobiliśmy, więc zostanie wykonana procedurka OnListChange – na ekranie konsoli pojawi się zawartość listy, czyli w tym przypadku napis free pascal.

#free-pascal #lazarus #delphi

Azarien

Referencja to referencja. To czy referencja jest samym tylko wskaźnikiem, czy jakimś opakowanym, czy czymś więcej - to szczegół implementacyjny.

furious programming

Owszem, jednak aby wiedzieć z czym ma się do czynienia i co niestandardowego można z referencjami zrobić, warto zagłębić się w temat i podłubać. W razie kolejnego ciekawskiego, będę mógł podlinkować ten wpis, zamiast znów tłumaczyć to samo. :P

furious programming
2017-09-19 19:55

Generyki we Free Pascalu coraz bardziej zaczynają mnie denerwować – straszanie toporny jest ten mechanizm… Pierwsza wkurzająca rzecz to sposób używania zmiennych, przechowujących referencje do list generycznych.


Dla przykładu – chcemy użyć listy generycznej do przechowywania instancji poniższej klasy:

type
  TEntry = class(TObject)
  {..}
  public
    Data: Integer;
  end;

Teraz deklaracja zmiennej dla listy. W stylu Delphi – ale z użyciem dostępnego typu kontenera – było by to tak jak poniżej, ale wyrzuci błąd kompilacji (treść w komentarzu):

var
  Entries: TFPGObjectList<TEntry>;  // Error: Generics without specialization cannot be used as a type for a variable

No dobrze, dodajmy magiczne słówko spezialize:

var
  Entries: spezialize TFPGObjectList<TEntry>;

Taki kod zostanie poprawnie skompilowany, jednak nie mam zielonego pojęcia skąd mam teraz wydłubać konstruktor, aby utworzyć instancję takiej listy. Nie mam określonego typu danych (jawnej klasy), więc trzeba by jakiejś magii, może ze słówkiem specialize, może z generic, a może z czymś innym, jednak różne konfiguracje zawodzą.

Pewnym obejściem jest po prostu zadeklarowanie osobnego typu, z którego będzie możliwe wywołanie konstruktora:

type
  TEntries = specialize TFPGObjectList<TEntry>;
var
  Entries: TEntries;
begin
  Entries := TEntries.Create();
  {..}

No i fajnie – kod się kompiluje, listę da się utworzyć. Po problemie? Nie… :]


Tak utworzona klasa TEntries co prawda potrafi już przechowywać obiekty klasy TEntry, jednak dostęp do nich jest nieco utrudniony. Załóżmy, że chcemy uzyskać dostęp do pierwszego obiektu listy i np. wpisać dane do zmiennej TEntry.Data. Odpowiedni zapis wygląda tak:

Entries[0].Data := $FF; // lub Entries.Items[0].Data := $FF;

Kod jest poprawny, kompiluje się, działa. Gdzie jest problem? W domyślnej właściwości Items. W klasie TFPGObjectList zdefiniowana jest w taki sposób, że zwraca lub modyfikuje T, czyli nie wiadomo co:

property Items[Index: Integer]: T read Get write Put; default;

Kompilator nie widzi problemu i najwyraźniej podczas kompilacji pod to T podstawia sobie klasę, której obiekty moja lista przechowuje. Jednak mechanizm kompletowania kodu gubi się – po klepnięciu kropki po nawiasach z indeksem elementu, powinno pojawić się okienko completion box i coś podpowiedzieć, ale wyrzuca błąd:

Entries[0].  // Error: illegal qualifier . found

Zapewne mechanizm ten dalej widzi typ elementu jako T (no bo tak jest zdefiniowany w klasie bazowej), więc nie może nic podpowiedzieć, a że pisanie kodu bez funkcji kompletowania jest niewygodne, dlatego też trzeba to naprawić. Znów małe obejście – można nadpisać właściwość Items, konkretyzując typ na jakim ma operować:

type
  TEntries = class(spezialize TFPGObjectList<TEntry>)
  public
    property Items[AIndex: Integer]: TEntry read Get write Put; default;
  end;

Na szczęście można skorzystać z istniejących metod pełniących rolę akcesora i mutatora, czyli metod Get i Put, a całość ustawić jako właściwość domyślną, całkowicie przykrywając poprzedniczkę. Plus jest też taki, że w okienku do kompletowania kodu będzie sugerowało naszą (skonkretyzowaną) właściwość Items, a do tej bazowej nie będzie dawać dostępu.

Nazwa takiej właściwości może być inna – nie musi to być akurat Items.


No, w tym momencie da się mieć własną listę generyczną, da się utworzyć jej instancję, kod będzie się kompilował i kompletowanie kodu nie będzie się dławić. Mam nadzieję, że w przyszłości coś się w tej materii zmieni, bo nie jest to zbyt wygodne w obsłudze. Już za kilka tygodni opublikowana zostanie wersja 1.8 środowiska – zobaczymy co nowego się pojawi.

#free-pascal #lazarus

Azarien

Jednak mechanizm kompletowania kodu gubi się – no to jest już problem samego IDE, nie kompilatora… generyki w Delphi może i są lepsze, ale w FPC były pierwsze. A w {$MODE DELPHI} nie działają te delphiowe?

furious programming

@Azarien: w trybie DELPHI działa (https://ideone.com/XiOBmR), jednak to nie jest dla mnie rozwiązanie – chcę korzystać z trybu OBJFPC wszędzie. A przecież nie będę go zmieniał wszędzie tam, gdzie używam generyków.

furious programming
2017-09-13 21:00

Kolejna ciekawostka ze świata Lazarusa – czy wiedziałeś, że edytor kodu wyposażony jest w funkcję zaznaczania kolumnowego (jak w cmd) oraz w tzw. multi-caret? A no jest – zaznaczanie kolumnowe jest od dawna, a obsługa wielu kursorów od wersji 1.6 środowiska.


Standardowe (liniowe) zaznaczanie kodu to po prostu wciśnięcie LPM i przesuwanie myszą. Zaznaczanie kolumnowe aktywuje się poprzez wciśnięcie i trzymanie lewego klawisza Alt i tak jak wyżej – przesuwania myszą. Obsługa z poziomu klawiatury to kombinacja Shift+Alt i strzałki.


Multi-caret – aby dodać kolejny kursor do edytora, należy wcisnąć kombinację Shift+Ctrl i trzymając te klawisze, klikać w docelowe miejsca (w których chcemy mieć dodatkowe kursory). Po puszczeniu ww. klawiszy możemy coś dopisywać, nadpisywać lub kasować. Aby deaktywować dodatkowe kursory, wystarczy kliknąć w inne miejsce gołym LPM.

Więcej na temat tego pluginu można wyczytać tutaj – SynEdit MultiCaret plugin.

#lazarus

furious programming
2017-09-12 21:39

Poprzedni mój wpis na temat bazgrołów graficznych z poziomu kodu nawet się spodobał, więc postanowiłem umieścić nowy – w podobnej tematyce. Dziś pokażę sposób renderowania obrazu z otoczką. Sposób ten wykorzystuję we własnych kontrolkach – zwykłych przyciskach oraz w przyciskach zakładek. Również daje się "motywować", więc kolor obrazu i tła jak najbardziej możliwe są do określenia.

O ile malowanie tekstu z obramowaniem było małym oszustwem (wielokrotnym malowaniem ciemnego tekstu pod spodem), to w przypadku grafiki już tak łatwo nie będzie. Co prawda można – podobnie jak wcześniej – wiele razy namalować obrazek pod spodem, imitując tym samym otoczkę, jednak na pewno nie będzie to sposób efektywny. Dlatego też trzeba podejść do tematu nieco od innej strony.

Jednym z ciekawszych rozwiązań jest wielowarstwowe renderowanie. Gotowy obraz będzie wynikiem wypełnienia tła, przemalowania w locie grafiki wzorca, jego malowania na płótnie, oraz finalnie namalowania drugiej grafiki, zawierającej półprzezroczystą otoczkę.


Etap 1. Przygotowanie grafik

Najpierw należy się zająć grafikami. Pierwszy krok to przygotowanie grafiki wzorca. Aby móc w pełni obsługiwać kolorowe tła, wzorzec musi obsługiwać kanał alfa, dlatego też przygotujmy sobie 32-bitowy obraz PNG. Do jego stworzenia skorzystam z programu Inkscape. Wybierzmy obraz – ja skorzystam z loga 4p (bez tytułu):

0.png

W dowolnym programie do obróbki grafiki rastrowej (np. Paint.NET), przyciemniam ją maksymalnie:

1.png

Teraz otwieram Inkscape i przeciągając plik z obrazem, upuszczam go na dokumencie:

2.png

Aby stworzyć wektorowy kształt na jego podstawie, wystarczy obraz zaznaczyć i skorzystać z narzędzia Path\Trace Bitmap…. Domyślne ustawienia wystarczą – klikamy Ok i mamy wektorową odbitkę:

3.png

Obraz można było wcześniej przyciemnić, ale też można to zrobić w Inkscape – jak kto woli. Teraz czas na otoczkę – zaznaczamy obraz i korzystamy z opcji Path\Linked Offset. Rozciągamy obramowanie na zadaną grubość:

4.png

Obramowanie jest koloru czarnego, z ustawionym kanałem alfa na 85 – po to, aby otoczka zawsze przyciemniała kolor tła (w przeciwnym razie była by bezużyteczna). Kolejny krok to już eksport grafik do docelowych plików – najpierw wzorzec (wypełnienie). W tym celu ustawiam otoczkę na całkowicie przezroczystą i eksportuję całość do pliku:

5.png

Teraz otoczka – najpierw należy ją oddzielić od wypełnienia. Zaznaczamy ją, przywracamy poprzedni stan kanału alfa (tutaj: wartość 85) i używamy opcji Path\Combine – otoczka stanie się osobnym obiektem. Teraz zaznaczamy oba obiekty i korzystamy z opcji Path\Exclusion. W wyniku tych działań otrzymamy samo dziurawe obramowanie (dla podglądu rozsunąłem obiekty):

6.png

Aby wyeksportować samą otoczkę, można albo usunąć główny obiekt (ten czarny), albo ustawić mu maksymalną przezroczystość. Po wszystkim eksportujemy dziada do drugiego pliku:

7.png

W efekcie mamy dwa pliki – jeden z czarnym wzorcem, a drugi z samą przyciemniającą otoczką (oba w tym samym rozmiarze):

8.png


Etap 2: Programowanie

Obrazki są gotowe, więc czas na kod. Co prawda można już oba namalować (jeden nad drugim) jednak mamy tylko jeden kolor – czarny. Potrzebujemy więc metody, która najpierw pomaluje grafikę wzorca na zadany kolor, a dopiero potem namaluje ją na docelowym płótnie. Wydaje się skomplikowane, no bo jak wypełnić kolorem grafikę o niewiadomych kształtach, w dodatku z antialiasingiem? Nic prostszego – pomalować każdy jej piksel. ;)

Ale nie tak prędko – pomalować należy każdy piksel grafiki, nie ruszając kanału alfa. Dlatego też skorzystamy z poczciwego ScanLine i dobierzemy się do składowych każdego piksela. Do tego celu przydadzą się odpowiednie typy danych (ich deklaracja może nie być konieczna):

type
  TRGBTriple = packed record
    B, G, R: UInt8;
  end;
 
type
  TRGBQuadRec = packed record
    B, G, R, A: UInt8;
  end;
 
type
  PRGBQuadArr = ^TRGBQuadArr;
  TRGBQuadArr = packed array [0 .. MaxInt div SizeOf(TRGBQuadRec) - 1] of TRGBQuadRec;

Do właściwego namalowania wzorca – tak jak poprzednio – użyjemy helpera dla klasy TCanvas:

type
  TCanvasHelper = class helper for TCanvas
  public
    procedure DrawGraphicPattern(AX, AY: Integer; APattern: TPortableNetworkGraphic; AColor: TColor);
  end;
 
  procedure TCanvasHelper.DrawGraphicPattern(AX, AY: Integer; APattern: TPortableNetworkGraphic; AColor: TColor);
  var
    LLine: PRGBQuadArr;
    LLineIdx, LPixelIdx: Integer;
    LColor: TRGBTriple;
  begin
    RedGreenBlue(AColor, LColor.R, LColor.G, LColor.B);
    APattern.BeginUpdate();
 
    for LLineIdx := 0 to APattern.Height - 1 do
    begin
      LLine := APattern.ScanLine[LLineIdx];
 
      for LPixelIdx := 0 to APattern.Width - 1 do
      begin
        LLine^[LPixelIdx].R := LColor.R;
        LLine^[LPixelIdx].G := LColor.G;
        LLine^[LPixelIdx].B := LColor.B;
      end;
    end;
 
    APattern.EndUpdate();
    Self.Draw(AX, AY, APattern);
  end;

Zapewne nasuwa się pytanie – dlaczego operuję bezpośrednio na APattern, nie używając grafiki pomocniczej?

Odpowiedź jest prosta – bo mogę. Dopóki nie zmienia się wartości kanału alfa, obraz może być wielokrotnie przemalowywany na dowolne kolory i nie będzie to miało żadnych negatywnych skutków. Drugi plus jest taki, że brak kopiowania obrazu przyspieszy proces renderowania.

Przykładowe wywołanie metody (malowanie tła pomijam – jest takie samo jak w poprzednim wpisie, oliwkowe):

//FPattern: TPortableNetworkGraphic;
Canvas.DrawGraphicPattern(PosX, PosY, FPattern, clWhite);

Rezultat:

9.png

Teraz pozostaje jeszcze otoczka. Tu nie trzeba żadnych dodatków – standardowa metoda Canvas.Draw wystarczy. Małe przypomnienie – skoro obrazy są tego samego rozmiaru, muszą być namalowane w tej samej pozycji, aby obramowanie pasowało do wzorca. Po namalowaniu otoczki mamy wszystko gotowe:

10.png

Tu podany jest sposób działający niezależnie – najpierw malowany jest pattern w zadanym kolorze (jedna metoda), a następnie malowana jest otoczka (druga metoda). Nic nie stoi na przeszkodzie, aby napisać jedną metodę, która wykonywać będzie obie te czynności. Wystarczy drugi parametr typu TPortableNetworkGraphic:

procedure DrawGraphicPattern(AX, AY: Integer; APattern, AOutline: TPortableNetworkGraphic; AColor: TColor);

oraz kod malujący otoczkę, umieszczony na końcu:

  Self.Draw(AX, AY, APattern);
  Self.Draw(AX, AY, AOutline);
end;

Sposób ten nie jest wrażliwy na kolor tła, więc można używać dowolnych kolorów, bez ingerencji w grafiki czy kod renderujący:

11.png


PS: Do ikonki z otoczką najlepiej będzie pasować tekst z otoczką. :]

12.png

#free-pascal #lazarus

czysteskarpety

panie ale to roboty jest, jednak zostanę przy png :)

furious programming

@kate87: jak będę miał coś ciekawego do pokazania to na pewno o tym napiszę. :]

@czysteskarpety: ale z czym? Wbrew pozorom, przygotowanie tych dwóch grafik zajmuje mi – komuś niezbyt ogarniającemu grafikę – dosłownie dwie minuty. Natomiast kod metody malującej pisze się raz – też nie dłużej niż pięć minut.

W swoim projekcie wykorzystuję taką metodę, jednak w ciekawszy sposób. Przycisk posiada nie dwie, a pięć grafik (w postaci zgrupowanych właściwości), w kolejności są to:

  • Normal – dla normalnego stanu,
  • Hover – po najechaniu myszą,
  • Inactive – grafika przeznaczona dla specjalnego trybu, w którym formularz wyświetlany jest w odcieniach szarości,
  • Disabled – dla kontrolki zablokowanej (Enabled na False),
  • Pattern – wzorzec (przeznaczenie jak we wpisie wyżej).

Do tego jest dodatkowa właściwość – ImageCombined typu Boolean – której stan określa sposób renderowania obrazu przycisku.

Jeśli ustawiona jest na False, kontrolka maluje jedynie tło oraz odpowiedni obrazek (któryś z czterech pierwszych, w zależności od stanu). A jeśli jest na True to wypełnia tło, maluje Pattern w zadanym kolorze (jaśniejszym niż tło lub białym, w zależności od ”hover”, a także w kolorze lub odcieniach szarości, w zależności od właściwości InterfaceActive), a następnie maluje któryś z pierwszych czterech obrazów, w których należy umieścić przyciemniające obramowanie – mocno przyciemniające dla Normal i Hover, a słabiej dla Inactive i Disabled.

Dzięki temu na odblokowanym formularzu przycisk malowany jest w kolorach, a na zablokowanym w odcieniach szarości (wszystkie pozostałe kontrolki również). I wygląda to wyśmienicie. :]

furious programming
2017-09-08 02:43

Ostatnio dość dużo dłubię w grafice – i przy jej tworzeniu, i przy programowaniu.

Jedną z takich dłubanin było malowanie tekstu z obramowaniem. Co prawda istnieje natywny dla systemu sposób takiego dekorowania tekstu (BeginPath i EndPath), jednak ma on pewne ograniczenia. Niewątpliwie jednym z najbardziej denerwujących jest brak wsparcia antialiasingu. Szkoda, bo dociąganie kobylastej biblioteki graficznej (lub kopiowanie z niej tego co interesujące) tylko po to, by namalować kilka napisów, nie brzmi zbyt rozsądnie.

Rozwiązanie znalazłem metodą prób i błędów, i choć nietypowe, może nieco dziwne, to zdaje egzamin. Chodzi o to, aby malować tekst bez tła. Przypisać kolor obramowania do Font.Color i namalować cztery razy docelowy tekst – odpowiednio o piksel wyżej i w lewo, wyżej i w prawo, niżej w lewo i niżej w prawo. Po tym wszystkim przywrócić ustawiony wcześniej kolor i namalować docelowy tekst w zadanej pozycji (już bez przesunięcia).


Załóżmy, że chcemy namalować na oliwkowym tle biały tekst z ciemniejszym obramowaniem. Najpierw wypełniamy obszar pod spodem standardową metodą, np. Canvas.FillRect:

Canvas.Brush.Color := $0034765E;
Canvas.FillRect(ClientRect);

Co da poniższy efekt:

0.png

Następnie ustawiamy właściwość Font:

Canvas.Font.Name := 'Ubuntu';
Canvas.Font.Size := 10;
Canvas.Font.Color := clWhite;
Canvas.Font.Style := [fsBold];

wpisujemy kolor obramowania do właściwości Pen (można go przekazać w parametrze – jak kto woli):

Canvas.Pen.Color := $00245241;

i na koniec wołamy metodę malującą tekst z obramowaniem:

Canvas.DrawTextOutlined(PosX, PosY, '4programmers.net');

Metoda działa w dwóch krokach – najpierw maluje cztery napisy z przesunięciem, tworząc ciemniejsze wypełnienie:

1.png

a na koniec maluje tekst w zadanym kolorze na wierzchu. Gotowy efekt poniżej:

2.png

Tekst w powiększeniu wygląda następująco:

3.png

Teraz kwestia kodu – w klasie TCanvas nie istnieje metoda DrawTextOutlined – stworzyłem dla niej helper. Cały kod niżej:

type
  TCanvasHelper = class helper for TCanvas
  public
    procedure DrawTextOutlined(AX, AY: Integer; const AText: String);
  end;
 
  procedure TCanvasHelper.DrawTextOutlined(AX, AY: Integer; const AText: String);
  var
    LOldBrushStyle: TBrushStyle;
    LOldFontColor: TColor;
  begin
    LOldBrushStyle := Self.Brush.Style;
    LOldFontColor := Self.Font.Color;
 
    Self.Brush.Style := bsClear;
    Self.Font.Color := Self.Pen.Color;
 
    Self.TextOut(AX - 1, AY - 1, AText);
    Self.TextOut(AX + 1, AY - 1, AText);
    Self.TextOut(AX - 1, AY + 1, AText);
    Self.TextOut(AX + 1, AY + 1, AText);
 
    Self.Font.Color := LOldFontColor;
    Self.TextOut(AX, AY, AText);
 
    Self.Brush.Style := LOldBrushStyle;
  end;

Zaletą jest to, że w ten sposób można namalować tekst zarówno o braku dodatkowych atrybutów, jak również tekst wytłuszczony, pochylony, podkreślony czy przekreślony. Drugą zaletą jest automatyczne dostosowanie się do systemowych ustawień antialiasingu – jeśli usługa rozmywania tekstu jest wyłączona to obramowanie będzie twarde, ale w dalszym ciągu widoczne.

Niestety, ale metoda ta ma też wady – nie nadaje się do malowania grubszego obramowania (przesunięcia większego niż 1px), co przy większych literach jest raczej konieczne. O ile ogólnie jest to możliwe, to wiązałoby się z malowaniem tekstu dla obramowania o wiele więcej razy, co może negatywnie odbić się na wydajności renderowania. Mimo wszystko do prostych zastosowań wystarczy – mnie wystarczyło.


W bardzo podobny sposób możliwe jest też malowanie tekstu z cieniem pod spodem. Różnica polega na tym, że tekst cienia wystarczy namalować raz, zamiast cztery razy. To jak daleko ma się znajdować cień i jak duży ma być, można zdefiniować w dodatkowych parametrach. Idąc za ciosem, niżej przykład metody malującej taki tekst:

type
  TCanvasHelper = class helper for TCanvas
  public
    procedure DrawTextShadowed(AX, AY, AOffsetX, AOffsetY: Integer; const AText: String);
  end;
 
  procedure TCanvasHelper.DrawTextShadowed(AX, AY, AOffsetX, AOffsetY: Integer; const AText: String);
  var
    LOldBrushStyle: TBrushStyle;
    LOldFontColor: TColor;
  begin
    LOldBrushStyle := Self.Brush.Style;
    LOldFontColor := Self.Font.Color;
 
    Self.Brush.Style := bsClear;
 
    Self.Font.Color := Self.Pen.Color;
    Self.TextOut(AX + AOffsetX, AY + AOffsetY, AText);
 
    Self.Font.Color := LOldFontColor;
    Self.TextOut(AX, AY, AText);
 
    Self.Brush.Style := LOldBrushStyle;
  end;

I znów – ustawiamy font oraz kolor dodatkowy dla cienia (przykładowo tak jak wcześniej) i wołamy metodę:

Canvas.DrawTextShadowed(PosX, PosY, 2, 2, '4programmers.net');

Efekt działania metody poniżej:

4.png

Co w powiększeniu przedstawia się następująco:

5.png

To nie wszystko – przykład zawiera cień o takim samym rozmiarze jak tekst właściwy, jednak nic nie stoi na przeszkodzie, aby cień był większy lub mniejszy, sprawiając wrażenie głębi (im wizualnie głębiej ma być cień, tym większy i z większym offsetem powinien być malowany).

Można się tym bawić bez końca, więc na cieniu zakończmy ten wpis. ;)

#free-pascal #lazarus

Azarien

szybkie góglanie wykazuje, że ludzie nie mają zbyt pochlebnego zdania na temat wydajności CopyRect, zwłaszcza w połączeniu z przezroczystością. jeśli to na tym tracisz wydajność, spróbowałbym czegoś innego np. funkcji WinApi AlphaBlend (nie wiem jak z jej wydajnością) albo czegoś o czym wiadomo że jest wydajne (Direct3D, OpenGL, czy choćby starego DirectDraw)

furious programming

Porzuciłem ten moduł właśnie ze względu na konieczne modyfikacje i optymalizacje – za dużo z tym roboty, a funkcjonalność zbyt mała. Na pewno CopyRect nie jest najlepszym rozwiązaniem, jednak zależało mi na tym, aby nie babrać się nadmiernie w winapi i nie dociągać dodatkowych bibliotek.

Teraz to nawet nie mam na czym sprawdzić tego AlphaBlend – pliki fontów usunąłem dawno temu, łącznie z konsolowym narzędziem do ich generowania. Pozostanie więc ciekawostką i przestrogą, aby nie być „mądrzejszym od telewizora”.

furious programming
2017-08-16 23:21

Wpis na temat ciekawostek ze świata Lazarusa.

Czy wiedziałeś, że składnia Free Pascal posiada wbudowany operator potęgowania? Operator ten opisuje fraza ** i można go bez problemu przeciążyć. Dla zwykłych intów zapis funkcji dokonującej potęgowania może wyglądać np. tak (bez używania zawartości modułu Math):

{$INLINE ON}
 
operator **(const ABase, AExponent: Integer): Integer; inline;
begin
  Result := Trunc(Exp(Ln(ABase) * AExponent));
end;

Zdefiniowanie funkcji dla tego operatora umożliwi potęgowanie w poniższy sposób:

Number := 2 ** 16;  // w zmiennej wygląduje liczba 65536

I teraz WTF – póki co operator ten nie posiada przyporządkowanej domyślnej funkcji z biblioteki standardowej, więc aby móc go użyć do obliczeń, należy samemu ten operator przeciążyć… Wsparcie tego operatora zapewne wprowadzono stosunkowo niedawno i obstawiam, że zapomnieli o uzupełnieniu stdliba. :D

PS: Biorę jeszcze pod uwagę celowość braku takich funkcji, jednak nie wiem co tym celem miałoby być.

#free-pascal #lazarus

furious programming

Dla zainteresowanych tematyką Lazarusa dodam, że już niebawem pojawi się nowa wersja środowiska (o numerze 1.8) oraz kolejna odsłona FPC (numer 3.0.4). Release candidate można pobrać z repo i potestować.