furious programming
wczoraj, 21:45

Pod moim poprzednim wpisem na temat tekstowych właściwości, @spartanPAGE skromnie stwierdził, że właściwości we Free Pascalu są chyba najbardziej rozwklekłe syntaktycznie jakie w ogóle są. Trudno mi powiedzieć (nie znam wszystkich języków), jednak jest w tym dużo prawdy – choć wynika to nie tylko z narzutu składniowego, ale też ze sporej funkcjonalności.

Prawda, jest trochę do napisania, nawet jeśli chodzi o zwykłą, typową właściwość:

public
  property Size: Integer read GetSize write SetSize;

No nie jest to krótki zapis, choć jakiś szczególnie długi też nie. Oczywiście nic nie stoi na przeszkodzie, aby skorzystać z kilku ficzerów i stworzyć takiego potwora:

public
  property Size[AFoo: Integer; ABar: Boolean; const ABaz: String]: Integer read GetSize write SetSize; default; platform; deprecated 'use something else';

Brakło mi sporego kawałka ekranu. Ja wiem, że to przykład bez większego sensu, jednak to daje jakieś pojęcie o możliwej objętości linii deklaracji właściwości (a była by jeszcze dłuższa, gdyby użyć dłuższej nazwy dla właściwości, a tym samym również dla settera i gettera).


Z drugiej strony, Free Pascal umożliwia deklarację właściwości w sposób bardzo krótki. Mowa o poniższej konstrukcji:

public
  property Size;

Ufff… zrobiło się przyjemniej. :]

Ale ale – nie ma typu danych, nie ma też określonej metody dostępowej i/lub zmieniającej, więc jak to niby ma działać…? A no działa, bo jest to specjalna konstrukcja. Specjalna, bo służy przede wszystkim do zmiany widoczności właściwości.

Konstrukcja ta stosowana jest powszechnie w komponentach – bazowe klasy komponentów posiadają określony, niemały zbiór właściwości uniwersalnych, które zadeklarowane są w sekcji public. Dzięki temu domyślnie nie są one wyświetlane w oknie inspektora obiektów. Każda klasa dziedzicząca po bazowej może używać danej właściwości, ale nie musi. Jeśli autor klasy komponentu uzna, że użytkownik powinien mieć do niej dostęp z poziomu okna inspektora obiektów1, zwiększa jej widoczność zapisując samą jej nazwę:

published
  property Size;  // w klasie bazowej znajduje się w sekcji public – zwiększamy widoczność

W ten sposób autor klasy ma możliwość określenia wymaganego zbioru właściwości, bez zaśmiecania inspektora tymi, które i tak nie będą wykorzystywane.


[1] Przeniesienie właściwości z sekcji public na published powoduje więcej zmian, niż tylko pokazanie jej w oknie inspektora obiektów.

#free-pascal #lazarus

Azarien

nikt ci przecież nie zabrania podzielić tej długiej linii na kilka…

furious programming

Tak samo w przypadku każdej innej dłuższej linijki, np. deklaracji metod – czy to cokolwiek zmienia? W dalszym ciągu zapis jest długi i jak nie zajmuje zbyt wiele miejsca w poziomie, to zajmuje go w pionie.

Sam nie mam zbyt wiele powodów do narzekania – mnie składnia FP odpowiada (małym wyjątkiem są generyki).

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-22 16:58

Kolejna ciekawostka na temat Pascala.

Czy wiesz, co oznacza zapis ^M, jeśli nie mówimy o typie wskaźnikowym? Albo zapis ^7 lub ^!? Jest to bardzo stara funkcjonalność, starsza ode mnie, na dodatek wciąż wspierana (zapewne ze względu na wsteczną kompatybilność). Przykład użycia:

const
  MAGIC = ^M;

Dlaczego to się kompiluje? Czym jest to M? Co zawiera stała? Sprawdźmy:

Write(MAGIC);

Hmm… pusta konsola… No to sprawdźmy rozmiar:

Write(SizeOf(MAGIC));

W konsoli wyświetla 1, czyli jeden bajt. Sprawdźmy więc wartość liczbową tego ”czegoś”, rzutując na typ liczbowy:

Write(UInt8(MAGIC));

Wyświetla 13 – czego tak? Może sprawdźmy inny znak:

const
  MAGIC = ^J;

Wyświetla 10. A coś z interpunkcji?

const
  MAGIC = ^!;

Wyświetla 97, dla kropki 110, a dla spacji (wpisanej bezpośrednio) wyświetla 96 – skonfundowani? ;)


Operator ^ ma kilka znaczeń. Pierwsza i dziś powszechnie wykorzystywana jego funkcjonalność to możliwość zapisu typu wskaźnikowego, gdzie token po operatorze to identyfikator typu bazowego. Można deklarować osobny typ danych (w sekcji type) lub bezpośrednio w linii deklaracji zmiennej (w bloku var):

type
  TPointerToInt = ^Integer;
var
  PointerToInt: ^Integer;

Druga i równie często używana funkcjonalność to odwołanie się do zawartości zmiennej wskaźnikowej:

PointerToInt^ := $100;

Trzecią, zapomnianą i dość magiczną funkcjonalnością tego operatora (czego przykład podałem na początku tego wpisu) jest zwrócenie znaku na podstawie tokenu po jego prawej stronie. Tokenem tym musi być znak – nie kod numeryczny, nie literał, po prostu znak (i tylko jeden). Znaki jakie zwracane są z takich fraz mają specyficzną kolejność:

// fraza   znak   kod znaku   odpowiednik
 
   ^A      SOH    1           Chr(Ord('A') - 64)
   ^Z      SUB    26          Chr(Ord('Z') - 64)
 
   ^a      SOH    1           Chr(Ord('a') - 96)
   ^z      SUB    26          Chr(Ord('z') - 96)
 
   ^0      'p'    112         Chr(Ord('0') + 64)
   ^9      'y'    121         Chr(Ord('9') + 64)

Nie pasują do żadnego typowego kodowania. Skoro fraza zwraca znak, to i możliwe jest tworzenie z nich łańcuchów:

var
  MagicString: String = ^f^r^e^e^ ^p^a^s^c^a^l;

W łańcuchu znajdą się głównie znaki kontrolne. Zapis równoważny z powyższym – ale zapisany w znany wszystkim sposób – poniżej:

var
  MagicString: String = #6#18#5#5#96#16#1#19#3#1#12;

Zapewne mało kto dziś wie o tym ukrytym ficzerze, a jeszcze węższe grono go wykorzystuje. Sam natknąłem się na taki zapis stosunkowo niedawno i na pierwszy rzut oka nie zrozumiałem o co chodzi. Jednak wczoraj przypomniał mi się tamten kod (bo znów natknąłem się na tego typu zapis) i postanowiłem dogłębnie zbadać sprawę – magia rozgryziona.

Tak więc jeśli kiedykolwiek analizować będziecie stare kody napisane w Pascalu, to już nie powinien nikogo dziwić poniższy zapis.

const
  CR  = ^M;
  LF  = ^J;
  TAB = ^I;

#pascal #free-pascal

vpiotr

Ja z nich też korzystałem - ale jako z komentarzy specjalnych (do wyłączania tymczasowego kodu).

Azarien

nie są przestarzałe, są mniej znane ;-)

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