mandag den 31. oktober 2011

TList.Sort();


Jeg formoder at alle bekendt med, at Delphi fra version 2009 har generiske typer. Herunder generiske lister.

Helt kort: Hvis man skal bruge en type stærk liste af en bestemt type - det kan være en simpletype, en klasse eller en record - så skal man:

1) Uses Generics.Collections
2) Erklære en variable:   List : TList<TMyType>;
3) Kalde dens constructor:    List := TList<TMyType>.Create;

... Og så bruger man den bare som en almindelig typestærk liste.

Et lille hurtigt eksempel kunne se således ud:

procedure Demo;
var
  i: Integer;
  PointList: TList<TPoint>;
begin
  PointList := TList<TPoint>.Create;

  for i := 1 to 10 do
    PointList.Add(Point(Random(100), Random(100)));

  PointList.Sort;

  FreeAndNil(PointList);
end;


MEEEN! Nu er det behovet opstår for SELV at bestemme, hvordan listen skal sorteres.
I Delphi kan man ikke bruge lighedsoperatoren til at sammenligne to generiske elementer i listen med. I stedet for implementerer man sin egen TComparer og giver den med - enten til listens constructor eller til sorteringen. 

I det følgende vælger jeg at sortere mine punkter efter afstanden til centrum, men først lige en lille hjælper, som giver mig et par metoder på min TPoint: 

TPointHelper = record helper for TPoint
  public
    function DistanceToCenterSquare: Integer; inline;
    function DistanceToCenter: Extended; inline;
  end;

{ TPointHelper }

function TPointHelper.DistanceToCenter: Extended;
begin
  Result := Sqrt(DistanceToCenterSquare);
end;

function TPointHelper.DistanceToCenterSquare: Integer;
begin
  Result := X * X + Y * Y;
end;


Og så skal vi have vore TComparer implementeret: 


type
  TPointComparer = class(TComparer<TPoint>)
    function Compare(const Left, Right: TPoint): Integer; override;
  end;

{ TPointComparer }

function TPointComparer.Compare(const Left, Right: TPoint): Integer;
begin
  Result := Left.DistanceToCenterSquare - Right.DistanceToCenterSquare;
end;




Tilbage er der blot at tage vores nye klasse i brug:

procedure Demo2;
var
  i: Integer;
  PointList: TList<TPoint>;
begin
  PointList := TList<TPoint>.Create(TPointComparer.Create);

  for i := 1 to 10 do
    PointList.Add(Point(Random(100), Random(100)));

  PointList.Sort;

  FreeAndNil(PointList);
end;




BEMÆRK: TList er ikke trådsikker. Ej heller TList<T>. Hvordan løser man så det? Man pakker tilgangen ind i en TMonitor, og styrer tilgangen ad den vej. Men det må blive genstand for et senere blog indlæg :-)

Jens Borrisholt

Implementering af "for in operatorer" vha. TEnumerator og  TEnumerable

Jeg har i et tidligere indlæg vist, hvordan man laver en typestærk liste ved hjælp af Generics. Det kostede ikke meget sved på panden i modsætning til i Delphi 2007, hvor man ikke har generics og således skal kode det hele selv.

Som jeg skrev i slutningen af mit indlæg, får man med brugen af TObjectList<T> eller TList<T> kun en standard enumerator - altså man kan "kun" benytte sig af den data type som den typestærke klasse indeholder. Hvis man vil noget andet, skal man stadig selv implementere sin enumerator.

Jeg vil i dette indlæg beskrive, hvordan man laver sine egne enumerators på en generisk klasse. Jeg har brugt en klasse, der kan vise helligdage i forskellige lande (Tyskland, Danmark, Norge, Sverige, USA, Grønland og Frankrig) på henholdsvis engelsk og det respektive sprog. Jeg vil i det følgende kun vise uddrag af koden, det komplette eksempel kan hentes her:  Helligdage demo.

Lad mig starte med lidt klasse-definitioner, her bragt i uddrag :

type
  THoliDay = class
  strict private
    ....
  public
    constructor Create(const Native, English: string; aDate: TDateTime);
    property NativeName: string read FNativeName write SetNativeName;
    property EnglishName: string read FEnglishName write SetEnglishName;
    property Date: TDateTime read FDate write SetDate;
    property DateAsStr: string read GetDateAsStr;
    property FriendlyDate: Boolean read FFriendlyDate write SetFriendlyDate;
  end;


  THolidayList = class(TObjectList<THoliDay>)
    ...
  public
    ...
    property NativeNames: TNativeNameCollection read GetNativeNames;
    property EnglishNames: TEnglishNameCollection read GetEnglishNames;
  end;



Så har jeg en klasse, der binder det hele sammen og regner på påsken, mm.

type
  TStdDato = class(THolidayList)
    ....
  public
    constructor Create;
    function GetEasterDay(Year: Word = 0): TDateTime;
    procedure MakeHoliDays(Year: Word = 0);
    function GetHoliDaysInMonth(Month: Integer = 0; Year: Integer = 0): THolidayList;
    function GetHoliDayName(aDate: TDateTime): string;
    property HoliDays[Index: Integer]: THoliDay read GeTHoliDay;
  published
    property Country: TCountryLocale read FCountry write SetCountry;
    property Year: Integer read FYear write SetYear;
    property MarkSunday: Boolean read FMarkSunday write SetMarkSunday;
    property MarkHolidays: Boolean read FMarkHolidays write SetMarkHolidays;
    property Language: TLanguage read FLanguage write SetLanguage;
    property SundayStr: string read FSundayStr;
  end;



Så langt så godt. Indtil videre er der ikke noget mystisk ved det.

Som det er nu, kan jeg gøre det følgende, når jeg skal bruge klassen TStdDato:

var
  STDDato: TStdDato;
begin
  STDDato := TStdDato.Create;
  for HolyDay in STDDato do
  begin
    ...
  end;
   
  FreeAndNil(STDDato);
end;

Men det jeg ønsker at opnå er:


var
  STDDato: TStdDato;
  aName: string;
begin
  STDDato := TStdDato.Create;

  ListBox1.Clear;
  for aName in STDDato.EnglishNames do
    ListBox1.Items.Add(aName);

  FreeAndNil(STDDato);
end;


Bemærk at jeg benytter mig af en string som enumerator. Det kræver man implementerer det selv.

Det første der skal gøres er, at implementere en nedarvning af TEnumerator<string>. Den har jeg valgt at lægge som en public type på THolidayList, da det er der den hører hjemme. Selve klassen minder meget om den vi så ovre i mit Delphi 2007 eksempel:

type
  THolidayList = class(TObjectList<THoliDay>)
  public type
    TEnglishNameEnumerator = class(TEnumerator<string>)
    private
      FList: THolidayList;
      FIndex: Integer;
      function GetCurrent: string; virtual;
    protected
      function DoGetCurrent: string; override;
      function DoMoveNext: Boolean; override;
    public
      constructor Create(aList: THolidayList);
      property Current: string read GetCurrent;
      function MoveNext: Boolean;
    end;
   ...


Implementeringen af klassen er triviel og kan ses i det komplette eksempel.

Nu hvor vi skal tilføje en enumerator til vores klasse, kræver det endnu en klasse som i dette tilfælde implementerer en nedarvning af TEnumerable<string>:

type
  TEnglishNameCollection = class(TEnumerable<string>)
  private
    FList: THolidayList;
    function GetCount: Integer;
  protected
    function DoGetEnumerator: TEnumerator<string>; override;
  public
    constructor Create(ADictionary: THolidayList);
    function GetEnumerator: TEnglishNameEnumerator; reintroduce;
    function ToArray: TArray<string>; override; final;
    property Count: Integer read GetCount;
  end;




Implementeringen af den er også ganske ligetil, så den vil jeg udelade her.

Til sidst skal vi bare have det hele koblet på klassen, det gøres således:

type
  THolidayList = class(TObjectList<THoliDay>)
   ....
  private
    FEnglishNames: TEnglishNameCollection;
     ...
    function GetEnglishNames: TEnglishNameCollection;
  public
     ...
     property EnglishNames: TEnglishNameCollection read GetEnglishNames;
  end;

...

function THolidayList.GetEnglishNames: TEnglishNameCollection;
begin
  if FEnglishNames = nil then
    FEnglishNames := TEnglishNameCollection.Create(Self);
  Result := FEnglishNames;
end;



Hermed har vi opnået vores mål. Den nye enumerator kan nu bruges som vi ønskede:

var
  STDDato: TStdDato;
  aName: string;
begin
  STDDato := TStdDato.Create;

  ListBox1.Clear;
  for aName in STDDato.EnglishNames do
    ListBox1.Items.Add(aName);

  FreeAndNil(STDDato);
end;


Den opmærksomme læser bemærker straks, at jeg i det ovenstående "kun" har tilføjet en enumerator for de engelske navne. Ville det være smart, hvis man også kunne gøre det samme med lokale navne? Ja, det kunne det, og det kan gøres nemt og enkelt ved hjælp af arv:

type
  THolidayList = class(TObjectList<THoliDay>)
  public type
    ...

    TNativeNameEnumerator = class(THolidayList.TEnglishNameEnumerator)
    private
      function GetCurrent: string; override;
    end;
    
    ...

    TNativeNameCollection = class(THolidayList.TEnglishNameCollection)
    public
      function GetEnumerator: TNativeNameEnumerator; reintroduce;
    end;
  private
    ...
    FNativeNames: TNativeNameCollection;

    ...
    function GetNativeNames: TNativeNameCollection;
  public
    ...

    property NativeNames: TNativeNameCollection read GetNativeNames;
  end;

...

{ THolidayList.TNativeNameEnumerator }

function THolidayList.TNativeNameEnumerator.GetCurrent: string;
begin
  Result := FList[FIndex].NativeName;
end;

{ THolidayList.TNativeNameCollection }

function THolidayList.TNativeNameCollection.GetEnumerator: TNativeNameEnumerator;
begin
  Result := TNativeNameEnumerator.Create(FList);
end;
Som sagt var der ikke mange ben i det, men jeg synes det skulle med alligevel for at gøre dette indlæg komplet.

Her til slut vil jeg lige endnu en gang nævne at hele eksemplet kan hentes her:  Helligdage demo og i den forbindelse vil jeg da lige vise er skærmbillede derfra :


Jens Borrisholt

"For in"-operatoren ved hjælp af Generics

I et tidligere indlæg viste jeg, hvordan man lavede en "for in"-operator i Delphi 2007. Dette indlæg vil kort vise, hvordan det gøres ved hjælp af generics, og således i Delphi 2009 og senere.

Til forskel fra tidligere (pre generics), hvor man skulle implementere sine egne lister vha. nedarvning for at få den type stærke, klares det i dag med nogle enkelte linjers kode, og erklæringen af selve listen er kun en enkelt linjes kode.

For eksemplets skyld bruger jeg den samme liste som vist i det tidligere indlæg.

unit MyListU;

interface

uses
  Generics.Collections;

type
  TMyClass = class
  private
    FSomeString: String;
    FSomeInteger: Integer;
    procedure SetSomeInteger(const Value: Integer);
    procedure SetSomeString(const Value: String);
  public
    property SomeInteger: Integer read FSomeInteger write SetSomeInteger;
    property SomeString: String read FSomeString write SetSomeString;
  end;

  TMyList = TObjectList<TMyClass>;

implementation

{ TMyClass }

procedure TMyClass.SetSomeInteger(const Value: Integer);
begin
  FSomeInteger := Value;
end;

procedure TMyClass.SetSomeString(const Value: String);
begin
  FSomeString := Value;
end;

end.

Som det ses, er der betragteligt mindre kode end i Delphi 2007-versionen: 108 linjer i 2007-versionen og kun 36 i den ovenstående! "Besparelsen" er til at få øje på.

"For in"-operatoren "opstår af sig selv", så man kan straks tage sin liste i brug og iterere gennem listen :

procedure TMainForm.BtnTest1Click(Sender: TObject);
var
  MyList : TMylist;
  MyObject : TMyClass;
begin
  MyList := TMylist.Create;
  //Todo: Tilføj elementer

  for MyObject in MyList do
    begin
      //Todo : brug MyObject
    end;

  FreeAndNil(MyList);
end;


Nemt og enkelt og uden nogle ben. Men hvad så hvis man ikke vil bruge et objekt til løbe min liste i gennem, men kun vil kigge på fx. integer-delen af objektet? Eller hvad nu hvis man ønsker flere muligheder for at traversere listen igennem? Ja, så er man nødt til at lave sin egen enumerator-klasse, som arver fra TEnumerator<T>, men det vil jeg behandle i et andet indlæg.

So stay tuned!

Jens Borrisholt

søndag den 30. oktober 2011

Class og Record Helpers

"Hjælpe-klasser" til både records og klasser var en af de store nyheder til Delphi 2007. Her flere år efter er det stadig ikke ret udbredt, ihvertfald ikke i den kode jeg er kommet forbi.

På min arbejdsplads sidder vi 8 Delphi-udviklere, og til trods for det, er der ikke andet end en enkelt eller to (ud over mig), der for alvor har taget class/record helpers i brug. Personligt forstår jeg det ikke, fordi det er sådan et fantastisk værktøj, men som med alt muligt andet skal man også være opmærksom på  "don't overdo a good thing", som man siger på engelsk.

Grund ideen bag class helpers er at tilføje nye metoder til en klasse uden at skulle nedarve fra den. Et meget godt eksempel er TStrings klassen, som danner ramme for en del ting: Lines på en TMemo, Items på en TlistBox og TComboBox samt en masse andet.

TStrings' Add-metode tillader kun en string som parameter, hvilket så betyder man selv skal konvertere til en string, hvis man derudover ønsker at tilføje noget andet, fx en integer eller en boolean.

Det kan man implementere vha. en class helper:

unit TStringsHelperU;

interface

uses
  Classes;

type
  TStringsHelper = class helper for TStrings
  public
    function Add(const V: Variant): Integer; overload;
    function Add(const Args: array of Variant): Integer; overload;
  end;

implementation

uses
  Variants;

function TStringsHelper.Add(const Args: array of Variant): Integer;
var
  Tmp: string;
  I: Integer;
  V: Variant;
begin
  Tmp := '';
  for V in Args do
    Tmp := Tmp + VarToStr(V);
  Result := Add(Tmp);
end;

function TStringsHelper.Add(const V: Variant): Integer;
begin
  Result := Add([V]);
end;

end.


Brugen af en class helper er næsten 100% transparent. Det eneste man som programmør skal gøre er, at tilføje den unit man har implementeret sin class helper i, og så ellers bare bruge den:

with Memo1.Lines do
  begin
    Add('Delphi på dansk');
    Add(2011);
    Add(true);
    Add([1, ' linje med blandet indhold: ', 1, #32, true]);
    Add(['π ~', PI]);
    Add(['Det ', 21, '. bogstav i det græske alfabet er φ' ]);
  end;



Resultatet ses her:


Jens Borrisholt

"For in"-operatoren i Delphi 2007

"For in"-operatoren blev introduceret i Delphi 2007, tror jeg nok. Den var der med sikkerhed ikke i Delphi 7, og jeg må indrømme at jeg ikke rigtigt har brugt nogle af de mellemliggende Delphi versioner. De var efter min smag ikke stabile nok, og Delphi 8 fandt jeg aldrig rigtigt ud af, hvad meningen var med.

Men pyt med det! Måske nogle af mine læsere ved bedre end mig, og kan oplyse mig om hvorvidt "For in"-operatoren fandtes i Delphi 2005 og eller 2006?

Først vil jeg lige komme med en opfriskning af, hvordan operatoren ser ud i Delphi:

var
  s : String;
begin
  for s in Memo1.Lines do
    Memo2.Lines.Add(s);
end;

Et lidt tænkt eksempel, men dog et eksempel :o) Jeg antager de fleste er bekendt med den ovenstående syntaks, og således vil jeg ikke gå i yderligere detaljer med dens betydning. Derimod vil jeg her vise, hvordan man selv kan implementere operatoren på sine egne klasser. Typisk lister.

Skal man have en typestærk liste i Delphi 2007, er man nød til selv at lave en nedarvning fra TObjectList, i stil med den her:

unit MyListU;

interface

uses
  Contnrs;

type
  TMyClass = class
  private
    FSomeString: String;
    FSomeInteger: Integer;
    procedure SetSomeInteger(const Value: Integer);
    procedure SetSomeString(const Value: String);
  public
    property SomeInteger : Integer read FSomeInteger write SetSomeInteger;
    property SomeString : String read FSomeString write SetSomeString;
  end;

  TMyList = class(TObjectList)
  private
    function GetItem(Index: Integer): TMyClass;
    procedure SetItem(Index: Integer; const Value: TMyClass);
  public
    property Items[Index: Integer]: TMyClass read GetItem write SetItem; default;
  end;

implementation

{ TMyClass }

procedure TMyClass.SetSomeInteger(const Value: Integer);
begin
  FSomeInteger := Value;
end;

procedure TMyClass.SetSomeString(const Value: String);
begin
  FSomeString := Value;
end;

{ TMyList }

function TMyList.GetItem(Index: Integer): TMyClass;
begin
  Result := inherited GetItem(Index) as TMyClass;
end;

procedure TMyList.SetItem(Index: Integer; const Value: TMyClass);
begin
  inherited SetItem(Index, Value);
end;

end.

Når så man vil bruge sin liste og vil iterere sig i gennem den, kan man fx. gøre det sådan her:

procedure TMainForm.BtnTest1Click(Sender: TObject);
var
  MyList : TMylist;
  MyObject : TMyClass;
  i : Integer;
begin
  MyList := TMylist.Create;
  //Todo: Tilføj elementer

  for I := 0 to MyList.Count - 1 do
    begin
      MyObject := MyList[i];
      //Todo: brug MyObject
    end;

  FreeAndNil(MyList);
end;

Der findes også en anden mulighed: At implementere sin egen "for in"-operator. Jeg tager udgangspunkt i min liste fra før, og har nu lavet nogle tilføjelser, som er dokumenteret i koden.

unit MyListU;

interface

uses
  Contnrs;

type
  {
    Først skal man lave et par Forward declarations, for at få det hele til at
    compilere.
  }
  TMyClass = class;
  TMyList = class;

  {
    Så skal du bugge en klasse op over den nedenstående skabelon.
    Det er en helt triviel øvelse som vist ikke kræver det store :o)
  }
  TMyListEnumerator = class
  private
    FIndex: Integer;
    FList: TMyList;
  public
    constructor Create(AList: TMyList);
    function GetCurrent: TMyClass;
    function MoveNext: Boolean;
    property Current: TMyClass read GetCurrent;
  end;

  TMyClass = class
  private
    FSomeString: String;
    FSomeInteger: Integer;
    procedure SetSomeInteger(const Value: Integer);
    procedure SetSomeString(const Value: String);
  public
    property SomeInteger : Integer read FSomeInteger write SetSomeInteger;
    property SomeString : String read FSomeString write SetSomeString;
  end;

  TMyList = class(TObjectList)
  private
    function GetItem(Index: Integer): TMyClass;
    procedure SetItem(Index: Integer; const Value: TMyClass);
  public
    {
      GetEnumerator er en metode som Delphi kalder når man itererer sig igennem
      listen, med en "for in"-operator.
    }
    function GetEnumerator: TMyListEnumerator;
    property Items[Index: Integer]: TMyClass read GetItem write SetItem; default;
  end;

implementation

{ TMyClass }

procedure TMyClass.SetSomeInteger(const Value: Integer);
begin
  FSomeInteger := Value;
end;

procedure TMyClass.SetSomeString(const Value: String);
begin
  FSomeString := Value;
end;

{ TMyList }

function TMyList.GetEnumerator: TMyListEnumerator;
begin
  //Der skal bare retuneres en ny instans. Delphi nedlægger den selv efter brug.
  Result := TMyListEnumerator.Create(Self);
end;

function TMyList.GetItem(Index: Integer): TMyClass;
begin
  Result := inherited GetItem(Index) as TMyClass;
end;

procedure TMyList.SetItem(Index: Integer; const Value: TMyClass);
begin
  inherited SetItem(Index, Value);
end;

{ TMyListEnumerator }

constructor TMyListEnumerator.Create(AList: TMyList);
begin
  inherited Create;
  FIndex := -1;
  FList := AList;
end;

function TMyListEnumerator.GetCurrent: TMyClass;
begin
  Result := FList[FIndex];
end;

function TMyListEnumerator.MoveNext: Boolean;
begin
  Result := FIndex < FList.Count - 1;
  if Result then
    Inc(FIndex);
end;

end.


Således er jeg nu klar til at bruge den operator jeg lige har implementeret:

procedure TMainForm.BtnTest2Click(Sender: TObject);
var
  MyList : TMylist;
  MyObject : TMyClass;
begin
  MyList := TMylist.Create;
  //Todo: Tilføj elementer

  for MyObject in MyList do
    begin
      //Todo : brug MyObject
    end;

  FreeAndNil(MyList);
end;



Jens Borrisholt

Locale: TFormatSettings;

TFormatSettings var en af de store nyheder i Delphi 6, men alligevel har den aldrig rigtigt slået igennem. Jo, nogen har nok brugt den, men at dømme ud fra Delphi koden der findes rundt omkring på nettet, så er det ikke vildt udbredt. Grundæggende set kan jeg jo kun tale for mig selv, jeg har kun periodisk brugt record'en.

For nyligt var der et par indlæg på DAPUG omkring datoformat og om hvordan man sikrede sig at en streng-værdi blev konverteret til en dato - uanset hvilken landeindstiling den pågældende Windows har.

Efter som min blog er på dansk, må jeg formode at flertallet af mine læsere har dansk som deres sprog i Windows, så mit eksempel vil omhandle håndtering af en streng, der som udgangspunkt er formateret efter den engelske standard for datoformatering.

Det første vi har brug for, er en implementering af C++ macroen MAKELANGID. Den findes i nyere versioner af Delphi (2009 og frem ,tror jeg), men her kommer den for en sikkerheds skyld:


function MAKELANGID(Language : Word; SubLanguage: WORD = SUBLANG_DEFAULT): WORD; inline;
begin
  Result := WORD(SubLanguage shl 10) or Language;
end;

Derefter skal vi have fat i den "engelske version" af TFormatSettings og have den brugt. I mit lille eksempel viser jeg blot resultatet med en ShowMessage:

procedure TFormMain.FormCreate(Sender: TObject);
var
  aDate: TDateTime;
  Locale: TFormatSettings;
begin;
{$IF CompilerVersion > 18.5} // Delphi 2007
  Locale := TFormatSettings.Create(MAKELANGID(LANG_ENGLISH));
{$ELSE}
  GetLocaleFormatSettings(MAKELANGID(LANG_ENGLISH), Locale);
{$IFEND}
  aDate := StrToDate('10/28/2011', Locale);
  ShowMessage(FormatDateTime(FormatSettings.LongDateFormat, aDate, Locale));
end;


Her er resultatet så :

Variable "Locale: TFormatSettings;" kan således bruges i alle Delphis funktioner til at konvertere tal og datoer til- og fra en streng.

Jens Borrisholt

Velkommen


Velkommen til Delphi på Dansk

Jeg vil i min blog forsøge at beskrive forskellige ting omkring Delphi. Dels smarte måder at gøre forskellige ting på, men også noget om hvordan de "nye" features virker i Delphi. Med de "nye" features, tænker jeg på alt det der er kommet ind i sproget siden Delphi 2009, og senest med Delphi XE 2.

Jeg skriver på dansk, fordi det er mit sprog. Der findes rigtig mange andre, der skriver på engelsk, og det er de helt sikkert bedre til end mig! Så derfor vil jeg forsøge mig med en dansk blog. Jeg håber i hvert fald på, at blive læst her i Skandinavien.

Koden og eksemplerne i denne blog vil være af lidt blandet karakter. I nogle af eksemplerne vil jeg benytte mig af Developer Express komponent pakken - i andre eksempler vil jeg benytte mig af Generics, som således kun vil rette sig mod Delphi 2009 og nyere. Jeg vil dog tilstræbe at min kode vil virke i hvert fald i Delphi 2007 og frem, eventuelt ved brug af compiler direktiver.

Tag godt imod min nye blog, og lad mig endelig høre fra jer der ude :o)

Jens Borrisholt