Rozdział 11. Migracja do .NET

Adam Boduch

Delphi 8 było o tyle rewolucyjne, iż umożliwiało pisanie programów tylko na platformę .NET. Niewątpliwie był to odważny krok. Teraz Delphi 2005 umożliwia pisanie programów zarówno dla .NET, jak i Win32.

Jednakże przejście do .NET wiąże się z przystosowaniem kompilatora i linkera do całkowicie innego środowiska niż dotychczasowe Win32. W ten sposób w Delphi 8 wprowadzono zupełnie nowe funkcje, natomiast kilka innych całkowicie zlikwidowano, co wiąże się z niekompatybilnością projektów napisanych we wcześniejszych wersjach Delphi. W tym rozdziale mam zamiar omówić kompatybilność języka Delphi dla .NET w porównaniu z jego poprzednikiem — Delphi dla Win32.

Zawartość tego rozdziału jest przeznaczona dla bardziej zaawansowanych programistów, którzy mieli już kontakt z poprzednimi wersjami Delphi. Jeżeli dla Czytelnika Delphi 2005 jest pierwszym środowiskiem programowania z rodziny Delphi — może ten rozdział spokojnie pominąć.

     1 Czy warto przechodzić do .NET?
     2 Ewolucja platform programistycznych
          2.1 WinFX
     3 Brakujące komponenty
          3.2 Zmiany we właściwościach
     4 Elementy języka
          4.3 Wszystko jest klasą!
          4.4 Przestrzenie nazw
          4.5 Kompilacja warunkowa
          4.6 Brakujące elementy
               4.6.1 Niebezpieczny kod
                    4.6.1.1 Komunikaty niebezpiecznego kodu
               4.6.2 Wskaźniki
               4.6.3 Asembler
               4.6.4 Pliki typowane
               4.6.5 Pliki tekstowe
               4.6.6 Przykład — migracja aplikacji do .NET, operacje na plikach
               4.6.7 Dyrektywa absolute
               4.6.8 Słowo kluczowe object
               4.6.9 Dynamiczna alokacja danych
               4.6.10 Dyrektywa exports
          4.7 Ciągi znakowe w Delphi
               4.7.11 Unikod w ciągach znakowych
               4.7.12 ShortString
               4.7.13 AnsiString
               4.7.14 WideString
               4.7.15 Ciąg znakowy z zerowym ogranicznikiem
               4.7.16 Klasa System.String
                    4.7.16.2 Split
                    4.7.16.3 Join
                    4.7.16.4 SubString
                    4.7.16.5 Klasa StringBuilder
               4.7.17 Typy liczbowe
          4.8 Komunikaty
     5 WinForms
          5.9 Brak pliku <em>.dfm/</em>.nfm
               5.9.18 Dynamiczne tworzenie komponentów w WinForms
               5.9.19 Zdarzenia dla komponentów
          5.10 VCL i WinForms
               5.10.20 Przykład — wykorzystanie formularza VCL
     6 Platform Invoke
          6.11 Wywołanie standardowe
          6.12 Użycie atrybutu DLLImport
          6.13 Parametry wyjściowe
          6.14 Dane wskaźnikowe
          6.15 Pobieranie danych z bufora
          6.16 Kod zarządzany i niezarządzany
          6.17 Używanie funkcji Win32
          6.18 Marshaling
               6.18.21 Atrybuty [in] oraz [out]
               6.18.22 Przekazywanie struktur
               6.18.23 Inne atrybuty marshalingu
               6.18.24 Wskaźnik na strukturę
                    6.18.24.6 Program korzystający z biblioteki Win32
               6.18.25 Przekazywanie struktur
          6.19 Wady PInvoke
     7 .NET a obiekty COM
          7.20 Terminologia COM
               7.20.26 Obiekt COM
               7.20.27 Interfejs COM
               7.20.28 GUID
               7.20.29 Serwer COM
               7.20.30 Biblioteki typu
          7.21 Mechanizm COM Callable Wrappers
          7.22 Przykładowy podzespół
          7.23 Utworzenie biblioteki typu
          7.24 Użycie biblioteki typu
          7.25 Korzystanie z klasy COM
               7.25.31 Aplikacja w C#
          7.26 Kontrolki COM w aplikacjach .NET
     8 Aplikacje sieciowe
     9 Test
     10 FAQ
     11 Podsumowanie

W rozdziale:
*napiszę parę słów o środowisku .NET,
*zaprezentuję elementy, które zostały wyeliminowane w Delphi dla .NET,
*przedstawię zagadnienia dotyczące ciągów znakowych w Delphi,
*opowiem o różnicach pomiędzy WinForms a VCL.NET,
*omówię P/Invoke,
*zaprezentuję sposoby korzystania z obiektów .NET w środowisku Win32.

Czy warto przechodzić do .NET?

Wielu programistów wciąż zastanawia się, czy warto zawracać sobie głowę platformą .NET, szczególnie właśnie teraz. W trakcie pisania tej książki technika programowania pod Win32 jest wciąż popularna i na pewno przez jakiś czas tak pozostanie. Z drugiej strony faktem jest, że największe firmy informatyczne (Microsoft i Borland) promują jak mogą model programowania pod .NET i, chcąc nie chcąc, za parę lat większość oprogramowania będzie tworzona właśnie dla tej platformy.

Skoro Czytelnik wziął do rąk tę książkę, zapewne jest zainteresowany platformą .NET i w jakiś sposób wiąże z nią swoją przyszłość. Książka ta traktuje o Delphi 2005, więc niejako także o platformie .NET. Wciąż zaznaczam, iż firma Borland jest nastawiona pozytywnie do platformy .NET, a dołączenie kompilatora dla Win32 do pakietu Delphi raczej ma na celu zachowanie kompatybilności (wciąż wielu projektantów pisze swoje programy dla Win32).

Bardzo dużą wadą wykorzystania .NET jest fakt, że aby uruchomić programy napisane dla platformy .NET, należy posiadać pakiet .NET Framework SDK. Obecnie większość komputerów z systemem Windows takowego nie posiada (.NET jest standardowo instalowany w systemach Windows 2003), co utrudnia uruchomienie aplikacji. Zdobycie takiego pakietu jest także kłopotliwe dla posiadaczy słabego łącza internetowego, gdyż cały pakiet zajmuje ponad 100 MB.

Pakiet .NET Framework SDK 1.1 można pobrać ze strony internetowej firmy Microsoft — http://microsoft.com.

Nie ma co ukrywać: .NET jest rewolucyjnym rozwiązaniem i większość autorytetów w dziedzinie programowania uważa, iż jest to platforma przyszłości. Obecne przewiduje się, że większość programistów Windows za parę lat całkowicie zrezygnuje z korzystania ze standardowego modelu programowania (Win32). Istotnym argumentem jest zapowiedź systemu Windows .NET, który powinien wejść na rynek w 2007 roku. System ten będzie w pełni obsługiwał .NET i może znacząco przyczynić się do popularyzacji samego .NET.

Ewolucja platform programistycznych

Przypomnijmy: w dotychczasowych najpopularniejszych systemach Windows (Windows 98/ME/XP) jest stosowany interfejs programistyczny zwany Win32 API (lub po prostu WinAPI), umożliwiający programistom tworzenie aplikacji dla systemu Windows. Interfejs ów, napisany w języku C, udostępnia wiele funkcji pozwalających na łatwe tworzenie okien, kontrolek itd.

API jest skrótem od słów Application Programming Interface.

Platforma .NET nieco zmienia strategię, na której opierały się setki tysięcy aplikacji tworzonych dla Windows. Oto udostępniono nam nowe biblioteki, klasy, funkcje oraz mechanizmy (m.in. CLR), jednak ewolucja .NET na tym się nie kończy. Wraz z wprowadzeniem w 2007 roku nowego systemu operacyjnego Windows .NET (nazwą kodową jest Longhorn) zmieni się całkowicie model projektowania — dotychczasowy interfejs Win32 zostanie wyeliminowany i zastąpiony nowym — WinFX. Rysunek 11.1 przedstawia ewolucję platform, jaka nastąpiła w ciągu ostatnich lat.

11.1.jpg
Rysunek 11.1. Ewolucja platform programistycznych

WinFX

W tym momencie należy wprowadzić kolejne pojęcie, o którym wcześniej nie pisałem. Chodzi o nowy interfejs programistyczny (API) tworzony na podstawie .NET Framework, który zostanie wprowadzony w systemie Longhorn. Ów tworzony całkowicie od nowa interfejs — WinFX — ma być obiektowy i pozbawiony wad poprzedniego Win32.

Dodatkowo, WinFX będzie zapewniał całkowitą zgodność z Win32, dzięki czemu na Longhornie będą mogły być uruchamiane aplikacje napisane w dotychczasowym środowisku Windows.

Nie będę się zbytnio rozpisywał na temat nowego systemu Longhorn, gdyż jest to temat na tyle obszerny i zaawansowany, że trzeba by było poświęcić mu całą odrębną książkę.

Brakujące komponenty

Mimo że Delphi 8 posiadało wersję biblioteki VCL, przystosowaną do .NET (VCL.NET), to jednak wydawało się, że było nieco ubogie. Wiele komponentów, które programiści znali z Delphi 7, po prostu nie znalazło się w VCL.NET. Wystarczy tutaj wspomnieć o zakładce Samples, która została całkowicie usunięta z VCL.NET w Delphi 8.

W Delphi 2005 sytuacja uległa radykalnej zmianie. Większość komponentów, które są obecne w VCL, są również dostępne w VCL.NET. Wyjątkiem może być komponent TChart, którego brak w VCL.NET. Do VCL.NET zostały również przeniesione komponenty IntraWeb.

Komponenty, których brakuje w VCL.NET i które równocześnie znalazły się w VCL, znajdują się na zakładce: InternetExpress, WebSnap, WebServices.

Zmiany we właściwościach

Generalnie z brakującymi właściwościami nie powinno być problemu. Największą różnicą jest zmiana typu właściwości @@Tag@@. W VCL właściwość ta jest typu Integer. W VCL.NET natomiast zmieniono jej typ na Variant. Jest to krok w dobrym kierunku, gdyż nie ograniczamy informacji (które mogą być przechowywane we właściwości @@Tag@@) jedynie do liczb.

W bibliotece WinForms właściwość @@Tag@@ jest typu System.Object.

Elementy języka

Firma Borland zrobiła wiele, aby dotychczasowe projekty napisane dla systemu Win32 działały poprawnie również w Delphi dla .NET, więc przy odrobinie szczęścia przenoszenie programów na platformę .NET udaje bez większych problemów. W nielicznych przypadkach od razu można skompilować aplikację w Delphi dla .NET i od tej pory taki program będzie pracował na platformie .NET. Wadą takiego rozwiązania jest to, że rozmiar aplikacji wykonywalnej będzie o wiele większy niż w przypadku, gdyby taki sam program kompilować w Delphi dla Win32. Sam program powinien jednak funkcjonować prawidłowo.

Wszystko jest klasą!

Cała platforma .NET opiera się na klasach. Bazowym typem w .NET jest typ System.Object. Ów typ jest klasą bazową nawet dla typów znanych już Czytelnikowi, takich jak System.String czy System.Char. Zagadnienia związane z tym tematem przytaczałem w rozdziale 8.

Przestrzenie nazw

Podstawową, rzucającą się w oczy zmianą jest nowa organizacja modułów w Delphi. Już Delphi 7 pozwalało na używanie w nazwach modułów znaku kropki, lecz teraz specyficzny zapis przestrzeni nazw jest charakterystyczną cechą.

W Delphi dla .NET zmienił się zapis modułów. W poprzednich wersjach Delphi wyglądało to tak:

  uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls,     
    Forms, Dialogs, StdCtrls;

Wraz z pojawieniem się .NET zmieniła się organizacja zapisu nazw modułów i teraz ten specyficzny zapis nosi nazwę przestrzeni nazw (ang. namespaces). Teraz na przykład moduł Windows nazywa się Borland.VCL.Windows i jest zapisany jako plik Borland.VCL.Windows.pas, a nie Windows.pas. Dzięki takiemu zapisowi można zachować pewną hierarchiczną organizację modułów. I tak w .NET głównym modułem jest moduł System (jest położony najwyżej w hierarchii), dalej są System.Security, później System.Security.Cryptography itd.

Choć nazwy modułów z Delphi dla Win32, uległy zmianie, to programista w projekcie Delphi dla .NET nie musi dokonywać poprawek z tym związanych. Kompilator dodaje bowiem w sposób automatyczny prefiks Borland.VCL. Opcję tę można zmienić w oknie opcji projektu (Project => Project Options), na zakładce Directories/Conditionals.

Należy pamiętać, iż podzespół .NET może posiadać wiele przestrzeni nazw, w których znajdują się klasy.

Kompilacja warunkowa

Mimo że większość kodu kompiluje się bez problemu w różnych wersjach Delphi, czasami zdarzają się sytuacje, gdzie dany program nie działa na starszym lub nowszym kompilatorze (szczególnie w przypadku Delphi dla .NET). W sytuacjach, gdy piszemy kod przeznaczony dla kilku wersji kompilatorów, istnieje możliwość umieszczania dyrektyw wskazujących, dla jakiej wersji kompilatora jest przeznaczony dany fragment.

Dzięki temu w jednym pliku źródłowym mogą znajdować się fragmenty przeznaczone dla Delphi 1 (platforma 16-bitowa) oraz dla wyższych wersji (platforma 32-bitowa). Delphi 7 oraz Delphi 2005 rozpoznają także wersję kodu przeznaczoną dla .NET.

Oto przykład kodu przeznaczanego dla kompilacji w Delphi 7:

{$IFDEF VER150}
procedure Grozna;
var
  P : PChar;
begin
  P := 'Łańcuch typu PChar';
  MessageBox(0, P, '', 0);
end;
{$ENDIF}

Normalnie kompilator wskazałby w takim kodzie błąd, gdyż typ PChar nie jest dozwolony w .NET. Ujmując odpowiedni fragment kodu dyrektywami {$IFDEF}, można spowodować, iż kompilator zignoruje ową procedurę (tak jak w powyższym przykładzie) i nie jest ona w ogóle wykonywana w programie skompilowanym w Delphi 2005.

Duże znaczenie w tej dyrektywie ma fragment VER150, który oznacza fragment kodu przeznaczony dla Delphi 7. W tabeli 11.1 znajdują się inne możliwe wartości.

Tabela 11.1. Symbole kompilacji warunkowej

Wersja Delphi Symbol
Delphi 1 VER80
Delphi 2 VER90
Delphi 3 VER100
Delphi 4 VER120
Delphi 5 VER130
Delphi 6 VER140
Delphi 7 VER150
Win32 WIN32
Win16 WINDOWS
.NET CLR

Należy pamiętać o dyrektywie {$ENDIF} kończącej blok warunkowy. Istnieje także dyrektywa {$ELSE}, która działa na identycznej zasadzie, co instrukcja else w Delphi. Przykładowo, można pisać program przeznaczony zarówno dla Delphi dla .NET, jak i Delphi dla Win32:

uses
  {$IFDEF CLR}System.Windows.Forms{$ELSE}Forms{$ENDIF};
</dfn>

Dobrym rozwiązaniem jest zadeklarowanie na początku programu instrukcji takiej jak pokazana poniżej, jeżeli na przykład kod źródłowy jest przeznaczony jedynie dla kompilatorów Win32:

{$IFNDEF MSWINDOWS}
{$MESSAGE FATAL 'Ten kod może być skompilowany jedynie dla środowiska Win32'}
{$ENDIF}

W takim przypadku przy próbie kompilacji w Delphi .NET kompilator wyświetli komunikat: Ten kod może być skompilowany jedynie dla środowiska Win32.

Brakujące elementy

Za sprawą przystosowania Delphi do .NET nie tylko można zaobserwować pojawienie się wielu nowych elementów języka, ale również — niestety — parę takich elementów uległo likwidacji. Oznacza to, że konieczna będzie w niektórych przypadkach edycja kodów źródłowych, tak aby kompilator Delphi nie pokazywał błędów.

Oto elementy, które zostały zlikwidowane w Delphi dla .NET:

*słowo kluczowe absolute,
*konstrukcja file of,
*typ PChar,
*typ Real48,
*typ File,
*TVarData,
*operator @,
*procedury GetMem, FreeMem, ReallocMem,
*funkcje BlockRead i BlockWrite,
*dyrektywy automated i dispid,
*wstawki asemblera,
*Addr, Hi, Lo, Swap,
*słowo kluczowe object.

Niektóre z tych konstrukcji można zastąpić innymi — np. zamiast typu Real48 można po prostu użyć Double. W miejsce PChar stosuje się String lub StringBuilder (o tym w dalszej części).

Zapewne dla wielu projektantów dużą stratą okazał się brak plików typowanych (inaczej zwanych rekordowymi) oraz funkcji BlockRead i BlockWrite. Należy w tym przypadku poszukać rozwiązania zastępczego — np. strumieni (klasa TStream w VCL.NET lub Stream w podzespole System.IO) lub baz danych.

O wykorzystaniu plików (m.in. typowanych) w Delphi pisałem w rozdziale 10.

Niebezpieczny kod

Środowisko .NET jest uważane za bezpieczne (ang. safety), więc niektóre elementy uważane za niebezpieczne zostały zlikwidowane — jak choćby wskaźniki. Istnieje jednak sposób, aby mimo to z nich skorzystać. Służy do tego odpowiednia dyrektywa {$UNSAFECODE ON}, która umożliwia korzystanie z potencjalnie niebezpiecznych elementów języka. Powyżej wypisałem zlikwidowane elementy języka, jednak przy odrobinie wysiłku z niektórych da się skorzystać właśnie po opatrzeniu kodu dyrektywą {$UNSAFECODE}.

Trzeba jednak zwrócić uwagę na pewien szczegół! Niekiedy konieczne jest utworzenie osobnej procedury, w której znajdzie się cały niebezpieczny kod. Taka należy postąpić w przypadku, gdy kompilator wskaże błąd: [Error] XYZ.dpr(11): Unsafe code only allowed in unsafe procedure. Przykładowo, w razie zastosowania wskaźników w programie w taki sposób jak poniżej, kompilator na to nie zezwoli:

program XYZ;

{$APPTYPE CONSOLE}

{$UNSAFECODE ON}
var
  P : ^String;
  S : String;
begin
  S := 'Boduch';
  P := @S;
  P^ := 'Adam';
{$UNSAFECODE OFF}
end.

W tym przypadku trzeba kod (wykorzystujący wskaźniki) umieścić w procedurze, którą dodatkowo należy opatrzyć klauzulą unsafe (patrz listing 11.1).

Listing 11.1. Przykład użycia wskaźników w Delphi dla .NET

program P11_1;

{$APPTYPE CONSOLE}

{$UNSAFECODE ON}
procedure UnsafeProc; unsafe;
var
  P : ^String;
  S : String;
begin
  S := 'Boduch';
  P := @S;
  P^ := 'Adam';

  Writeln(S); // wyświetli napis 'Adam'
  Readln;
end;
{$UNSAFECODE OFF}

begin
  UnsafeProc;
end.

Nie wolno zapominać o słowie kluczowym unsafe! Zarówno dyrektywa {$UNSAFECODE}, jak i słowo unsafe muszą stanowić całość — w razie pominięcia jednego z tych elementów program nie zostanie skompilowany.

Wskaźniki w Delphi są traktowane jako typ niebezpieczny i nie zaleca się ich używania. Więcej informacji na temat wskaźników znajduje się w rozdziale 9.

Komunikaty niebezpiecznego kodu

W przypadku wykrycia w czasie kompilacji niebezpiecznego kodu Delphi w oknie komunikatów (Message View) wyświetla ostrzeżenia. Istnieje możliwość wyłączenia ostrzeżeń za pomocą odpowiednich dyrektyw w kodzie. Delphi rozróżnia trzy komunikaty o błędach: unsafe type (niebezpieczny typ), unsafe code (niebezpieczny kod) oraz unsafe cast (niebezpieczne rzutowanie). Wyłączenie wyświetlania tych komunikatów następuje poprzez zastosowanie poniższych dyrektyw:

{$WARN UNSAFE_TYPE OFF}
{$WARN UNSAFE_CODE OFF}
{$WANR UNSAFE_CAST OFF}

Wskaźniki

Jak pokazałem w poprzednim przykładzie i na listingu 11.1, istnieje możliwość wykorzystania wskaźników (tj. operatora ^ oraz @, a także typu Pointer) dzięki zastosowaniu dyrektywy {$UNSAFECODE}.

Asembler

W poprzednich wersjach Delphi można było umieszczać w kodzie wstawek asemblerowych (zresztą dotyczy to nie tylko Delphi — taka możliwość istnieje także w językach Turbo Pascal, C/C++ i innych). Polegało to na umieszczeniu kodu Asemblera pomiędzy znacznikami asm i end:

asm
{ kod Asemblera }
end;

W Delphi dla .NET reakcją kompilatora na taki kod będzie wyświetlenie komunikatu o błędzie: [Error] P11_2.dpr(9): Unsupported language feature: 'ASM'.

Pliki typowane

Pliki typowane (inaczej zwane plikami rekordowymi) stanowią sposób na zapisanie całych, określonych porcji danych w pliku tekstowym. Dla przypomnienia dodam, że zapisanie do pliku danych w postaci rekordu wiązało się z zadeklarowaniem określonej struktury (rekordu), a następnie typu:

type
  TNowyPlik = file of TRekord;

W Delphi dla .NET konstrukcja typu file of jest uznawana za przestarzałą i w związku z tym — zabronioną. Nie zaleca się już używania tego typu zapisu. Technologią, jakiej powinni teraz używać programiści do przechowywania danych tekstowych, jest XML (o języku XML będę pisał w rozdziale 18.).

W przypadku próby zastosowania plików typowanych kompilator wyświetli komunikat o błędzie: [Error] P11_2.dpr(11): Unsupported language feature: 'typed or untyped file'.

Mechanizm plików typowanych można w dość prosty sposób zastąpić strumieniami! VCL.NET definiuje klasę TStream oraz klasy pochodne (np. TFileStream wykorzystywaną do pracy z plikami). Można także skorzystać z klasy Stream dostarczanej przez .NET (klasa ta znajduje się w przestrzeni nazw System.IO).

Pliki tekstowe

Pliki tekstowe nadal są dostępne w Delphi (typ TextFile), aczkolwiek do przechowywania informacji zaleca się korzystanie z baz danych lub XML.

Należy zauważyć, że w Delphi dla .NET brakuje funkcji BlockRead oraz BlockWrite, które często służyły do operacji na plikach. W celu manipulowania zawartością pliku można jednak w łatwy sposób skorzystać z mechanizmu strumieni (klasa TFileStream).

Funkcje BlockRead oraz BlockWrite można równie dobrze zastąpić mechanizmem baz danych. Warto się zastanowić, czy wykorzystanie plików tekstowych w tworzonym programie jest rzeczywiście konieczne. Bazy danych często okazują się prostszym i bardziej efektywnym sposobem przechowywania danych.

Przykład — migracja aplikacji do .NET, operacje na plikach

Na listingu 11.2 przedstawiłem kod źródłowy aplikacji napisanej w Delphi dla Win32. Ten prosty przykład pokazuje możliwości wykorzystania typu File oraz funkcji BlockWrite/BlockRead w celu skopiowania zawartości pliku.

Do kopiowania plików służy funkcja CopyFile, której deklaracja znajduje się w pliku Borland.VCL.Windows.pas i wygląda tak:

function CopyFile(lpExistingFileName, lpNewFileName: string; bFailIfExists: BOOL): BOOL;

Listing 11.2. Kod źródłowy programu napisanego w Delphi dla Win32

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls;

type
  TMainForm = class(TForm)
    OpenDialog: TOpenDialog;
    GroupBox1: TGroupBox;
    lblFile: TLabel;
    pbCopy: TProgressBar;
    btnCopy: TButton;
    procedure btnCopyClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnCopyClick(Sender: TObject);
var
  SrcFile, DstFile : File; { plik źródłowy i plik przeznaczenia }
  FSize : Integer;       { rozmiar kopiowanego pliku }
  Bytes : Integer;       { ilość odczytanych danych }
  Buffer : array[0..255] of byte;  { bufor przechowujący dane }
  TotalSize : Integer;     { liczba już skopiowanych bajtów }
begin
  if OpenDialog.Execute then
  begin
  { wyświetl na etykiecie ścieżkę kopiowanego pliku }
    lblFile.Caption := 'Plik ' + OpenDialog.FileName;
    AssignFile(SrcFile, OpenDialog.FileName);
    try
      Reset(SrcFile, 1); { otwórz plik }
      FSize := FileSize(SrcFile); { odczytaj rozmiar pliku }
      pbCopy.Max := FSize;  { maks. pozycja na pasku postępu }

      AssignFile(DstFile, 'C:\' + ExtractFileName(OpenDialog.FileName) + '~');
      try
      { utwórz plik }
        Rewrite(DstFile, 1);

        repeat
          Application.ProcessMessages;

          { odczytaj dane }
          BlockRead(SrcFile, Buffer, SizeOf(Buffer), Bytes);
          if Bytes > 0 then  { jeżeli liczba odczytanych bajtów jest większa od 0 }
          begin
          { przypisz odczytane dane do pliku }
            BlockWrite(DstFile, Buffer, Bytes);
            TotalSize := TotalSize + Bytes;
          end;
          { pozycja na pasku postępu }
          pbCopy.Position := TotalSize;
          
        until Bytes = 0;

      finally
        CloseFile(DstFile);
      end;

    finally
      CloseFile(SrcFile);
    end;
  end;
end;

end. 

Główną ideą programu jest kopiowanie pojedynczymi fragmentami pliku wskazanego przez użytkownika. Dzięki temu możemy w komponencie TProgressBar pokazać postęp kopiowania pliku. Całość realizuje się poprzez funkcje BlockRead oraz BlockWrite.

Program wykonujący tę samą czynność z zastosowaniem strumieni pokazałem na listingu 11.3. Delphi jest językiem na tyle elastycznym, że wystarczy otworzyć projekt napisany w Delphi dla Win32, zmodyfikować nieco kod, a następnie skompilować. Żadne inne zmiany w przypadku naszego przykładowego programu nie są konieczne.

Listing 11.3. Program realizujący kopiowanie przy użyciu strumieni

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, System.ComponentModel;

type
  TMainForm = class(TForm)
    OpenDialog: TOpenDialog;
    GroupBox1: TGroupBox;
    lblFile: TLabel;
    pbCopy: TProgressBar;
    btnCopy: TButton;
    procedure btnCopyClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

const
  BuffSize = 1023;

procedure TMainForm.btnCopyClick(Sender: TObject);
var
  SrcFile, DstFile : TFileStream; { plik źródłowy i plik przeznaczenia }
  FSize : Integer;       { rozmiar kopiowanego pliku }
  Bytes : Integer;       { ilość odczytanych danych }
  Buffer : array[0..BuffSize] of byte;  { bufor przechowujący dane }
  TotalSize : Integer;     { liczba już skopiowanych bajtów }
begin
  if OpenDialog.Execute then
  begin
  { wyświetl na etykiecie ścieżkę kopiowanego pliku }
    lblFile.Caption := 'Plik ' + OpenDialog.FileName;
    SrcFile := TFileStream.Create(OpenDialog.FileName, fmOpenRead);
    try
      FSize := SrcFile.Size; { rozmiar pliku }
      pbCopy.Max := FSize;  { maks. pozycja na pasku postępu }

      DstFile := TFileStream.Create('C:\' + ExtractFileName(OpenDialog.FileName) + '~', fmCreate);
      try
        repeat
          Application.ProcessMessages;

          Bytes := SrcFile.Read(Buffer, TotalSize, SizeOf(Buffer));

          if Bytes > 0 then  { jeżeli liczba odczytanych bajtów jest większa od 0 }
          begin
          { przypisz odczytane dane do pliku }
            DstFile.Write(Buffer, Bytes);
            TotalSize := TotalSize + Bytes;
          end;
          { pozycja na pasku postępu }
          pbCopy.Position := TotalSize;

        until Bytes = 0;

      finally
        DstFile.Free;
      end;

    finally
      SrcFile.Free;
    end;
  end;
end;

end.

Szczegółowy opis korzystania ze strumieni oraz plików znajduje się w rozdziale 10.

Celem powyższego programu było zaprezentowanie możliwości operowania na strumieniach wraz z pokazaniem postępu kopiowania pliku. To samo zadanie można bowiem zrealizować za pomocą funkcji CopyFrom klasy TFileStream:

DstFile.CopyFrom(SrcFile, SrcFile.Size);

A zatem program, który pokazałem wcześniej, można określić jako wyważanie otwartych drzwi dla celów edukacyjnych. Miałem zamiar pokazać, że czasami, aby zaimportować aplikację do .NET, należy pozbyć się pewnych nieaktualnych już elementów języka i wprowadzić jakieś rozwiązanie zastępcze (w tym przypadku — strumienie).

Dyrektywa absolute

Dyrektywa absolute jest dość archaiczna — istniała bowiem już w Turbo Pascalu, gdzie umożliwiała deklarowanie danej zmiennej pod określonym adresem w pamięci.
Później, w Delphi 6, można było jedynie zadeklarować daną zmienną pod takim samym adresem jak inna zmienna:

var
   Zmienna: string[32];
   Dlugosc: Byte absolute Zmienna;

Borland nigdy nie zalecał używania tej dyrektywy w Delphi, aż w końcu w Delphi dla .NET została ona zlikwidowana całkowicie. Należy więc zmienić kody źródłowe, tak aby nie zawierały słowa kluczowego absolute.

Słowo kluczowe object

W poprzednich wersjach Delphi słowem kluczowym alternatywnym dla class było object. Pozwalało ono na deklarowanie obiektów, podobnie jak przy użyciu słowa class:

type
  TFoo = object(TObject)
  { metody }
end;

W Win32 słowo kluczowe object zostało zachowane jedynie ze względów kompatybilności, a w .NET taka konstrukcja znikła całkowicie i nie jest akceptowana przez kompilator.

Dynamiczna alokacja danych

Do tej pory Czytelnik zapewne nieraz korzystał ze wskaźników oraz dynamicznego przydziału pamięci w trakcie działania programu. Chodzi mi tutaj o funkcje: GetMem, FreeMem i AllocMem. Teraz już niestety te funkcje nie mogą zostać zastosowane w programie.

Istnieje natomiast możliwość wykorzystania funkcji New, lecz jedynie w kontekście tablic dynamicznych:

type
  TSomeType = array of Byte;

var
  SomeType : TSomeType;

begin
  SomeType := New(TSomeType, 10);
  SomeType[0] := 0;
  SomeType[1] := 1;
end.

W razie próby wykorzystania funkcji New w innym kontekście kompilator wygeneruje komunikat: NEW standard function expects dynamic array type identifier.

Dyrektywa exports

Osoby, które wcześniej zajmowały się programowaniem dla platformy Win32, a w szczególności pisaniem bibliotek DLL, wiedzą zapewne, iż dyrektywa exports zawarta w bibliotece DLL umożliwia eksportowanie funkcji i procedur. Delphi dla .NET pozwala na tworzenie bibliotek, lecz sama dyrektywa exports jest traktowana przez Delphi jako kod potencjalnie niebezpieczny. Dlatego też wystarczy umieścić w kodzie biblioteki klauzulę:

{$UNSAFECODE ON}

Od tego momentu można pisać w Delphi .NET biblioteki DLL, które mogą być normalnie wykorzystywane przez aplikacje działające w Win32.

Ciągi znakowe w Delphi

Jedną z głównych zmian w Delphi dla .NET jest brak typu PChar. Jako że zarówno typ PChar, jak i PAnsiChar oraz inne typy potomne wywodzą się ze wskaźników, a wskaźniki są w .NET niedozwolone, nie jest możliwe korzystanie również z typu PChar. Oczywiście tak jak w przypadku wskaźników, tak i typ PChar jest uznany za niebezpieczny (unsafe), lecz aby go zastosować, można użyć dyrektywy {$UNSAFECODE}. Moim zdaniem, jako że całe VCL.NET korzysta już jedynie z typu String, warto w swoim programie również zamienić typ zmiennych znakowych.

W poprzednich wersjach Delphi typ PChar był wykorzystywany praktycznie jedynie w przypadku funkcji WinAPI — VCL już wówczas korzystało z typu String. Teraz .NET zrezygnował całkowicie z typu PChar na rzecz String.

Unikod w ciągach znakowych

Największą zmianą jest traktowanie zmiennych znakowych (typu String) jako unikodu.

Unicode (lub unikod) jest zestawem znaków, który z założenia ma zawierać wszystkie symbole używane na świecie. Ma to zapewnić rozwiązanie problemu wyświetlania i kodowania znaków.

Jak dotąd w Delphi istniały trzy typy zmiennych znakowych typu String (tabela 11.2).

Tabela 11.2. Typy zmiennych znakowych

Typ Maksymalna długość Rozmiar w pamięci
ShortString 255 znaków od 2 do 256 bajtów
AnsiString 231 znaków od 4 bajtów do 2 GB
WideString 230 znaków od 4 bajtów do 2 GB

ShortString

Typ ShortString jest podstawowym typem znakowym począwszy od Delphi 1. Zmienne tego typu mają ograniczoną długość — do 255 znaków. Stosowanie tego typu ciągu znakowego jest szybkie, właśnie z powodu ograniczonej długości. Zmienną typu ShortString można zadeklarować na dwa sposoby:

var
  S1 : ShortString; // długość – 255 znaków
  S2 : String[255]; // długość - 255 znaków

Obie zmienne będą w tym przypadku zmiennymi typu ShortString. W przypadku zmiennej @@S2@@ równie dobrze można zadeklarować zmienną o mniejszej długości, wpisując odpowiednią wartość w nawiasie.

Informacja o długości ciągu znakowego ShortString jest umieszczona jest w pierwszym bajcie — łatwo więc można odczytać rzeczywistą długość tekstu:

var
  S : ShortString;
  Len : Integer;
begin
  S := 'Hello World!';
  Len := Ord(S[0]);
end.

Zmienna @@Len@@ posiada wartość 12.

Funkcja Ord służy do zamiany (konwersji) znaku typu Char na wartość liczbową Integer. Odwrotną funkcję (zamianę wartości Integer na Char) realizuje funkcja Chr.

AnsiString

Typ AnsiString raz pierwszy pojawił się po w Delphi 2 — jest to typ nieposiadający ograniczenia w długości, przez co staje się on bardzo uniwersalny. Domyślne ustawienia Delphi nakazywały (w Delphi 2 – 7) traktować typ String tak samo jak typ AnsiString.

Delphi automatycznie zarządza pamięcią dla zmiennych typu AnsiString — programista nie musi się tym przejmować. Wadą tego typu zmiennych jest cokolwiek wolniejsze działanie niż w przypadku ShortString, ale zalecane jest jego stosowanie ze względu na brak limitów w długości ciągu znakowego.

Odczyt długości ciągu znakowego nie może tutaj odbywać się z użyciem znaków [], jak to ma miejsce w zmiennej typu ShortString. W tym przypadku można skorzystać z funkcji Length.

var
  S : AnsiString;
  Len : Integer;
begin
  S := 'Hello World!';
  Len := Length(S);
end.

WideString

Obecnie typy WideString oraz String są sobie równoważne i wskazują na klasę System.String — to jest jedna z większych zmian, jeśli chodzi o ciągi znakowe w Delphi .NET. Dzięki temu znika problem tworzenia aplikacji wielojęzycznych.

Ciąg znakowy z zerowym ogranicznikiem

Pod tą nazwą kryją się w rzeczywistości zmienne typu PChar lub tablice Char. Nazwa wzięła się od tego, że zmienne znakowe reprezentowane przez typ PChar są zakończone znakiem o kodzie 0. W języku C wszystkie ciągi znaków są w rzeczywistości tablicami Char, np.:

var
  S : array[0..255] of char;
begin
  S := 'Hello World!';
end.

Po deklaracji tablicy 255-elementowej typu Char można przypisywać do niej dane jak do zwykłego ciągu znakowego. Zmienna typu @@S@@ jest zakończona znakiem #0 informującym, że to jej koniec.

Typ PChar jest w rzeczywistości typem wskaźnikowych (pointers), który wskazuje na tablicę znaków.

Prawdopodobnie Czytelnik nieczęsto będzie miał okazję do zastosowania typu PChar, tym bardziej że jest to typ uznany za niebezpieczny (ang. unsafe), ale mimo to należało o tym wspomnieć.

Klasa System.String

Wspominałem wcześniej, że .NET jest całkowicie obiektowy. W rzeczywistości typ String wskazuje na klasę System.String, która udostępnia parę ciekawych funkcji służących do manipulacji ciągami znakowymi. Funkcje te są na pewno znane osobom, mającym już pewne doświadczenie z takimi językami jak PHP czy Perl. Bez nich nadal trzeba by posługiwać się standardowymi procedurami, takimi jak Copy, Pos czy Delete.

Główne funkcje opisałem w tabeli 11.3, a poniżej umieściłem parę przykładów ich wykorzystania.
Tabela 11.3. Główne procedury klasy System.String

Funkcja/Procedura Opis
Length Zwraca ilość znaków znajdujących się w ciągu znakowym.
Compare Funkcja na podstawie dwóch parametrów sprawdza, czy większy jest pierwszy, czy też drugi ciąg znaków.
Copy Tworzy nową instancję ciągu znakowego (kopiuje jego zawartość).
Insert Wstawia tekst w określone miejsce ciągu znakowego.
Join Funkcja łączy kilka elementów tekstowych w jeden.
Remove Funkcja umożliwia usunięcie kawałka ciągu znaków.
Replace Zamienia część ciągu znakowego.
Split Umożliwia rozdzielenie ciągu znaków na mniejsze fragmenty na podstawie podanego znaku.
SubString Wycina część ciągu znaków.
ToLower Zwraca ciąg znaków zapisany małymi literami.
ToUpper Zwraca ciąg znaków zapisany wielkimi literami.
Trim Usuwa wszystkie spacje z początku i z końca ciągu znaków.
TrimEnd Usuwa wszystkie spacje z końca ciągu znaków.
TrimStart Usuwa wszystkie spacje z początku ciągu znaków.
Split

Funkcja Split na pewno jest znana programistom PHP czy Perla. Warto dodać, że jest to funkcja bardzo przydatna i często używana, umożliwia bowiem rozdzielenie ciągu znakowego na mniejsze fragmenty, na podstawie podanego separatora. Listing 11.4 prezentuje kod służący do rozdzielenia ciągu znaków Ala ma kota na pojedyncze wyrazy. Znakiem separatora jest tu spacja.

Listing 11.4. Przykład wykorzystania funkcji Split z klasy System.String

program P11_4;

{$APPTYPE CONSOLE}

uses
  Windows,
  System; // <-- odpowiednie moduły

var
  S : String;
  Separator : array of Char;  // separator
  Result : array of String; // tablica po rozdzieleniu
  I : Integer;

begin
  SetLength(Separator, 1);
  Separator[0] := ' ';  // tylko jeden separator

  S := 'Ala ma kota'; // ciąg znaków do rozdzielenia
  Result := S.Split(Separator);

  for I := Low(Result) to High(Result) do
  begin
    Writeln('Pozycja nr ' + Convert.ToString(I) + ': ' + Result[i]);
  end;

  Readln;

end.

Skoro zmienna @@S@@ wskazuje na klasę System.String, można z takiej niej korzystać jak ze zwykłego obiektu.

Funkcja Split wymaga podania parametru w postaci tablicy typu Char. Tablica ta zawiera znaki mające posłużyć jako separator, na podstawie którego ciąg znaków zostanie rozdzielony.

Rezultatem działania funkcji jest tablica typu String, zawierająca rozdzielony tekst: po jednym wyrazie w każdym elemencie. Rezultatem działania takiego programu będzie pojawienie się w konsoli tekstu:

Pozycja nr 0: Ala
Pozycja nr 1: ma
Pozycja nr 2: kota
Join

Funkcja Join działa w sposób przeciwny w porównaniu ze Split — łączy dane z tablicy zamiast je rozdzielać. Pierwszym parametrem owej funkcji musi być także separator — znak lub ciąg znaków, który zostanie wstawiony pomiędzy elementy tablicy. Drugim parametrem musi być tablica typu String, zawierająca elementy do połączenia (patrz listing 11.5).

Listing 11.5. Przykład wykorzystania funkcji Join z klasy System.String

program P11_5;

{$APPTYPE CONSOLE}

uses
  Windows,
  System; // <-- odpowiednie moduły

var
  S : String;
  Words : array of String; // tablica po rozdzieleniu

begin
  SetLength(Words, 3); // określenie liczby elementów w tablicy
  Words[0] := 'Ala';
  Words[1] := 'ma';
  Words[2] := 'kota';    

  Writeln('Po złączeniu: ' + S.Join(' ', Words));

  Readln;

end.

Po uruchomieniu programu na konsoli zostanie wyświetlony napis Ala ma kota, czyli połączenie trzech elementów z tablicy.

SubString

Funkcja SubString pozwala na wycięcie części ciągu znaków począwszy od podanego znaku. Jej budowa i zastosowanie są proste. Pierwszym parametrem musi być pozycja, od której rozpocznie się wycinanie ciągu znaków, natomiast drugim parametrem jest liczba znaków do wycięcia. Listing 11.6 prezentuje prosty przykład wykorzystania funkcji SubString.

Listing 11.6. Wykorzystanie funkcji SubString z klasy System.String

program P11_6;

{$APPTYPE CONSOLE}

uses
  Windows,
  System; // <-- odpowiednie moduły

var
  S : String;
  Words : array of String; // tablica po rozdzieleniu

begin
  S := 'Delphi for .NET';

  Writeln(S.SubString(11, 4)); // wyświetli napis .NET

  Readln;

end.

Po uruchomieniu na ekranie zostanie wyświetlony napis .NET.

Dlaczego wspominam o tych funkcjach klasy System.String? Ja sam bowiem, programując jeszcze w środowisku Win32, zmuszony bywałem czasami do pisania własnych funkcji typu Split czy Join. Jeżeli ktoś napisał na potrzeby swoich projektów funkcje do operowania na ciągach znaków, posiadające działanie takie jak Join czy Split, zaleca się zastąpienie ich gotowymi z klasy System.String. Tym samym zwiększa się przejrzystość kodu i zyskuje pewność, że wykorzystana funkcja .NET jest możliwie najszybsza i pozbawiona błędów.

Również, patrząc przyszłościowo, należy w swoich projektach stopniowo pozbywać się poczciwych funkcji Delphi typu Pos, Copy czy Insert i zastępować je tymi z klasy System.String.

Klasa StringBuilder

Pragnę zwrócić uwagę na jeszcze jeden fakt związany z operacjami na ciągach znakowych w Delphi .NET. Mianowicie nadmierne manipulowanie zmiennymi typu String, np. dodawanie do nich nowych wartości (za pomocą operatora +) powoduje wyraźnie wolniejsze działanie programu. Spójrzmy na listing 11.7.

Listing 11.7. Dodawanie wartości do ciągu znakowego

program P11_7;

{$APPTYPE CONSOLE}

uses
  Windows,
  SysUtils;

var
  Str : String;
  I : Integer;
  Start, Stop : Integer;
begin
  Str := '';
  Start := GetTickCount;

  for I := 0 to 20000 do
    Str := Str + 'Ala ma kota';

  Stop := GetTickCount;
  
  Writeln('Czas wykonywania: ' + IntToStr(Stop - Start));
  Readln;

end.

Program jest dość prosty, napisany w taki sposób, aby mógł zostać skompilowany zarówno w Delphi dla .NET, jak i w Delphi dla Win32. Jego zadaniem jest dodawanie w pętli (20 000 iteracji) napisu Ala ma kota. Taki program, skompilowany dla środowiska Win32, poradzi sobie z takim zadaniem w 16 sek., natomiast ten skompilowany dla .NET — potrzebuje na to aż 40 sekund! Aby przyspieszyć działanie aplikacji, można skorzystać z klasy StringBuilder, która znajduje się w przestrzeni System.Text. Użycie tej klasy spowoduje przyspieszenie działania aplikacji do 15 sekund, czyli do wartości porównywalnej z Delphi dla Win32. Warto o tym wiedzieć, w sytuacjach, gdy projektowana aplikacja silnie korzysta z typu String (patrz listing 11.8).

Listing 11.8. Program napisany z użyciem klasy StringBuilder

program P11_8;

{$APPTYPE CONSOLE}

uses
  Windows,
  System.Text,
  SysUtils;

var
  Str : StringBuilder;
  I : Integer;
  Start, Stop : Integer;
begin
  Str := StringBuilder.Create;

  Start := GetTickCount;

  for I := 0 to 20000 do
    Str.Append('Ala ma kota');

  Stop := GetTickCount;
  Writeln('Czas wykonywania: ' + IntToStr(Stop - Start));
  Readln;

  Str.Free;

end.

W klasie StringBuilder korzystamy z metody Append, która jest po prostu szybsza niż operator +. W przypadku pracy z większą ilością danych, zalecam więc użycie klasy StringBuilder. Wadą takiego rozwiązania jest to, iż taki program nie będzie kompatybilny ze starszymi wersjami Delphi (klasa StringBuilder jest charakterystyczna dla .NET).

Wspomniane programy były testowane na komputerze z procesorem AMD 950 Mhz, 384 MB RAM.

Więcej przykładów wykorzystania klasy StringBuilder znajduje się w dalszej części rozdziału.

Typy liczbowe

Jeżeli chodzi o typy liczbowe w Delphi .NET, chyba największą zmianą okazał się brak 6-bajtowego typu Real48. Typ ten był dość specyficzny, jego używanie nie było zalecane już w poprzednich wersjach Delphi, teraz został całkowicie zlikwidowany.

Dodatkowo typ Comp, który nie był zalecany do stosowania już w poprzednich wersjach Delphi, teraz wskazuje na typ Int64. Tak więc próba wykorzystania typu Comp spowoduje wyświetlenie ostrzeżenia kompilatora: Symbol 'Comp' is deprecated.

Typ Extended w Delphi dla .NET wskazuje na typ System.Double, podobnie jak typ Real.

Komunikaty

Dla przypomnienia: komunikaty stanowią specyficzny element systemu Windows, pozwalający na wymianę informacji pomiędzy aplikacjami. Każde działanie użytkownika w systemie powoduje wysłanie komunikatu (ang. message) — przykładowo, ruch myszą czy naciśnięcie klawisza. W takich momentach do danego programu jest przekazywany komunikat z informacją, np. kod ASCII naciśniętego klawisza lub położenie wskaźnika myszy itp.

Komunikaty stanowią bardziej zaawansowaną metodę reakcji na określone zdarzenia. W Delphi prostszym sposobem jest zastosowanie zdarzeń. Tabela 11.4 zawiera listę komunikatów, których działanie uległo zmianie w nowej wersji Delphi.

Więcej informacji o komunikatach i ich wykorzystaniu w Delphi znajduje się w książce Delphi 7. Kompendium programisty, wydanej nakładem wydawnictwa Helion w 2003 roku.

Tabela 11.4. Komunikaty, które uległy zmianie w .NET

Komunikat Opis
CM_MOUSEENTER Parametr LPARAM uległ zmianie. Teraz dostarcza numer kontrolki, w której „zasięgu” znalazł się wskaźnik myszy. Wcześniej dostarczał uchwyt obiektu.
CM_MOUSELEAVE Parametr LPARAM uległ zmianie. Teraz dostarcza numer kontrolki, w której „zasięgu” znalazł się wskaźnik myszy. Wcześniej dostarczał uchwyt obiektu.
CM_WINDOWHOOK Nieużywany. Zaleca się użycie metod HookMainWindow oraz UnhookMainWindow z klasy TApplication.
CM_CONTROLLISTCHANGE Zastąpiony przez metodę ControlListChange z klasy TWinControl.
CM_CHANGED Parametr LPARAM został zmieniony. Teraz zawiera kod zmodyfikowanego obiektu [#]_ .
CM_DOCKCLIENT Zastąpiony przez metodę DockClient z klasy TWinControl.
CM_UNDOCKCLIENT Zastąpiony przez metodę UnDockClient z klasy TWinControl.
CM_FLOAT Zastąpiony przez metodę FloatControl z klasy TControl.

===Destruktory ===

Osoby, które kiedyś programowały w Delphi, C++ lub innym języku uwzględniającym wykorzystywanie obiektów, wiedzą, jak ważną rolę podczas programowania klas odgrywają destruktory. Pełnią one w zasadzie dwie funkcje: zwalniają zasoby zajmowane przez obiekt oraz zwalniają pamięć zarezerwowaną na potrzeby danego obiektu. Stosowanie destruktorów często było przyczyną pojawiania się błędów typu AccessViolation, spowodowanych odwołaniem do elementu klasy już po jej zwolnieniu. W .NET dzięki mechanizmowi garbage collection nie ma obowiązku używania destruktorów. Użycie destruktorów nie ma już więc takiego znaczenia jak wcześniej.

Po utworzeniu nowej klasy może wystąpić błąd kompilacji: unsupported language feature "destructor". W Delphi .NET nie można tworzyć destruktorów, lecz jedynie dziedziczyć je po klasie bazowej, tak więc destruktor w naszej klasie musi nosić nazwę Destroy oraz być opatrzony klauzulą override:

type
  TMyClass = class
  public
    constructor Create;
    destructor Destroy; override;
end;

Tylko w taki sposób można korzystać z destruktorów.

W .NET istnieją jednak inne sposoby kontrolowania zwalniania obiektów. Mam tu na myśli metody Finalize oraz Dispose. Wspominałem o nich w rozdziale 7. książki.

WinForms

Windows Forms jest biblioteką komponentów, zaprojektowaną na potrzeby .NET i dostępną dla każdego języka wykorzystującego ową platformę. Mimo wielu podobieństw pomiędzy WinForms a VCL.NET istnieje równie dużo różnic, co sprawia, iż proces migracji z VCL.NET do WinForms jest o wiele trudniejszy.

Oto krótka lista różnic pomiędzy WinForms a VCL.NET:

*nazwy komponentów różnią się (nie ma TButton — jest po prostu Button itp.),
*właściwość Location w miejsce Left i Top,
*właściwość Size zamiast Width i Height,
*inne nazwy zdarzeń,
*właściwość Text zamiast Caption,
*brak niektórych komponentów,
*inny sposób wyświetlania formularza,
inny sposób tworzenia obiektów na formularzu,
brak pliku formularza (**.dfm/.nfm
),
*brak właściwości @@Components@@ i @@ComponentCount@@.

Aby skorzystać z biblioteki WinForms, do listy uses należy dodać moduł System.Windows.Forms.

Brak pliku .dfm/.nfm

W przypadku WinForms żadne informacje na temat komponentów nie są kompilowane w wersję wynikową. Wszystkie informacje na temat komponentów (właściwości i zdarzenia) są zawarte w kodzie źródłowym w procedurze InitializeComponent:

procedure TWinForm2.InitializeComponent;
begin
  Self.Components := System.ComponentModel.Container.Create;
  Self.Size := System.Drawing.Size.Create(300, 300);
  Self.Text := 'WinForm2';
end;

Umieśćmy teraz na formularzu komponent Panel. Należy zwrócić uwagę na zawartość procedury InitializeComponent:

procedure TWinForm3.InitializeComponent;
begin
  Self.Panel1 := System.Windows.Forms.Panel.Create;
  Self.SuspendLayout;
  //
  // Panel1
  //
  Self.Panel1.Location := System.Drawing.Point.Create(32, 80);
  Self.Panel1.Name := 'Panel1';
  Self.Panel1.TabIndex := 0;
  //
  // TWinForm3
  //
  Self.AutoScaleBaseSize := System.Drawing.Size.Create(5, 13);
  Self.ClientSize := System.Drawing.Size.Create(292, 273);
  Self.Controls.Add(Self.Panel1);
  Self.Name := 'TWinForm3';
  Self.Text := 'WinForm2';
  Self.ResumeLayout(False);
end;

Nie należy zmieniać poszczególnych właściwości komponentów bezpośrednio w kodzie źródłowym — najpewniejszym sposobem jest skorzystanie z inspektora obiektów.

Komponent Panel (TPanel w VCL.NET) jest charakterystycznym komponentem służącym do przechowywania innych obiektów. Jego rola ogranicza się praktycznie tylko do pełnienia funkcji klasy rodzicielskiej dla innych komponentów.

Procedura InitializeComponent zawiera instrukcje potrzebne do tworzenia komponentów. Jest wywoływana przez konstruktor głównej klasy WinForms, w naszym przypadku — TWinForm3.

Dynamiczne tworzenie komponentów w WinForms

Czasami istnieje konieczność tworzenia komponentów w sposób dynamiczny, tj. podczas działania programu. Nie jest to trudne — należy jedynie wywołać konstruktor odpowiedniego obiektu i określić kilka jego podstawowych właściwości.

W ramach ćwiczeń umieśćmy na formularzu komponent Button. W tym przykładzie zaprezentuję sposób utworzenia innego przycisku na komponencie Panel. Oto fragment kodu:

procedure TWinForm3.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  MyButton : System.Windows.Forms.Button;
begin
  MyButton := System.Windows.Forms.Button.Create;
  MyButton.Location := System.Drawing.Point.Create(1, 1);
  MyButton.Name := 'MyButton';
  MyButton.Text := 'MyButton';
  Panel1.Controls.Add(MyButton);
end;

Na samym początku należy zadeklarować zmienną wskazującą na odpowiednią klasę — w tym przypadku System.Windows.Forms.Button (w VCL.NET jej odpowiednikiem jest po prostu klasa TButton).

W następnym kroku wywołujemy konstruktor klasy, a następnie określamy jego położenie (właściwość @@Location@@), nazwę oraz tekst wyświetlany na obiekcie.

Ostatnim krokiem jest wywołanie metody Add i podanie w parametrze nazwy obiektu, który ma zostać dodany do komponentu.

Odpowiednikiem powyższego kodu WinForms byłby następujący z VCL.NET:

var
  MyButton : TButton;
begin
  MyButton := TButton.Create(Panel1); // wywołanie konstruktora
  MyButton.Parent := Panel1;  // rodzic dla obiektu
  MyButton.Left := 1;  // położenie w poziomie
  MyButton.Top := 1; // położenie w pionie
  MyButton.Name := 'MyButton'; // nazwa
  MyButton.Caption := 'MyButton'; // tekst na obiekcie
end;

Zdarzenia dla komponentów

W WinForms przypisanie zdarzenia dla komponentu odbywa się zupełnie inaczej niż w przypadku VCL.NET. Otóż służy do tego funkcja Include, którą stosuje się w sposób następujący:

Include(MyButton.Click, Button1_Click);

Pierwszy parametr określa nazwę zdarzenia — w tym wypadku Click. Odpowiednikiem zdarzenia Click w VCL.NET jest OnClick.

Drugim parametrem jest nazwa procedury zdarzeniowej, która zostanie przypisana do określonego zdarzenia.

Na listingu 11.9 znajduje się kod przykładowego programu. Naciśnięcie przycisku spowoduje dynamiczne utworzenie kolejnego. Do każdego nowo tworzonego obiektu jest przypisywana ta sama procedura zdarzeniowa. Rezultat działania programu został przedstawiony na rysunku 11.2.

Listing 11.9. Dynamiczne tworzenie komponentów w WinForms

unit WinForm;

interface

uses
  System.Drawing, System.Collections, System.ComponentModel,
  System.Windows.Forms, System.Data;

type
  TWinForm = class(System.Windows.Forms.Form)
  {$REGION 'Designer Managed Code'}
  strict private
    /// <summary>
    /// Required designer variable.
    /// </summary>
    Components: System.ComponentModel.Container;
    Button1: System.Windows.Forms.Button;
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    procedure InitializeComponent;
    procedure Button1_Click(sender: System.Object; e: System.EventArgs);
  {$ENDREGION}
  strict protected
    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    procedure Dispose(Disposing: Boolean); override;
  private
    { Private Declarations }
  public
    constructor Create;
  end;

  [assembly: RuntimeRequiredAttribute(TypeOf(TWinForm))]

implementation

{$REGION 'Windows Form Designer generated code'}
/// <summary>
/// Required method for Designer support -- do not modify
/// the contents of this method with the code editor.
/// </summary>
procedure TWinForm.InitializeComponent;
begin
  Self.Button1 := System.Windows.Forms.Button.Create;
  Self.SuspendLayout;
  // 
  // Button1
  //
  Self.Button1.Location := System.Drawing.Point.Create(8, 416);
  Self.Button1.Name := 'Button1';
  Self.Button1.Size := System.Drawing.Size.Create(144, 23);
  Self.Button1.TabIndex := 1;
  Self.Button1.Text := 'Naciśnij mnie!';
  Include(Self.Button1.Click, Self.Button1_Click);
  // 
  // TWinForm
  // 
  Self.AutoScaleBaseSize := System.Drawing.Size.Create(5, 13);
  Self.ClientSize := System.Drawing.Size.Create(512, 453);
  Self.Controls.Add(Self.Button1);
  Self.Name := 'TWinForm';
  Self.Text := 'Tworzenie obiektów';
  Self.ResumeLayout(False);
end;
{$ENDREGION}

procedure TWinForm.Dispose(Disposing: Boolean);
begin
  if Disposing then
  begin
    if Components <> nil then
      Components.Dispose();
  end;
  inherited Dispose(Disposing);
end;

constructor TWinForm.Create;
begin
  inherited Create;
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent;
  //
  // TODO: Add any constructor code after InitializeComponent call
  //
end;

procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  MyButton : System.Windows.Forms.Button;
begin
  MyButton := System.Windows.Forms.Button.Create;

  Randomize;

  MyButton.Location := System.Drawing.Point.Create(Random(Size.Width), Random(Size.Height));
  MyButton.Size := System.Drawing.Size.Create(144, 23);
  MyButton.Text := 'Naciśnij mnie!';
  Include(MyButton.Click, Button1_Click); // przypisz zdarzenie

  Controls.Add(MyButton);
end;

end.

11.2.jpg
Rysunek 11.2. Rezultat działania programu

VCL i WinForms

Jako że przekształcenie kodu VCL do WinForms jest nieco kłopotliwym zadaniem, dobrym rozwiązaniem może okazać się korzystanie z obu bibliotek jednocześnie. Od strony kompilatora nie ma to najmniejszego znaczenia, a programistom pozwala na oszczędzanie czasu. Jedyną wadą takiego rozwiązania jest większy rozmiar aplikacji wynikowej niż ma to miejsce przy zastosowaniu jedynie WinForms.

Przykład — wykorzystanie formularza VCL

Napisałem kiedyś bardzo prosty, przykładowy program prezentujący działanie klasy TApplication. Aplikacja ta po prostu wyświetlała na etykiecie (komponent TLabel) ścieżkę do samego siebie. Została utworzona w Delphi dla Win32.

Zaprezentuję teraz, jak wykorzystać ten formularz w Delphi dla .NET.

#Utwórz nowy projekt Windows Forms.
#Zapisz projekt na dysku.
#Do katalogu z projektem skopiuj pliki formularza Delphi Win32 (**.pas* oraz *.dfm).

Kolejnym krokiem będzie dodanie modułu MainFrm do listy uses w formularzu głównym WinForms naszej aplikacji (pod warunkiem oczywiście, że — tak jak w moim przypadku — nazwą pliku z formularzem jest MainFrm.pas).

uses
  System.Drawing, System.Collections, System.ComponentModel,
  System.Windows.Forms, System.Data, MainFrm; // <-- nasz moduł

Teraz należy umieścić na formularzu komponent Button oraz wygenerować jego zdarzenie Click:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
begin
  TMainForm.Create(nil).ShowModal;
end;

Jedyna linia znajdująca się wewnątrz procedury zdarzeniowej powoduje utworzenie i wyświetlenie formularza TMainForm. Łatwo więc można dostrzec, że wykorzystywanie formularzy VCL nie sprawia w Delphi dla .NET żadnych problemów.

Platform Invoke

.NET jest nową platformą programistyczną. Minie jeszcze sporo czasu, zanim programiści przystosują swoje aplikacje do nowej platformy oraz obdarzą ją zaufaniem. Mimo iż .NET udostępnia dziesiątki klas umożliwiających łatwiejsze programowanie, w Delphi .NET nadal będziemy zapewne nieraz korzystali ze standardowych funkcji Win32. Wystarczy spojrzeć na zawartość modułu Borland.Vcl.Windows.pas, który co prawda został zmodyfikowany w celu dostosowania do .NET, ale nadal zawiera funkcje znane z wcześniejszych wersji Delphi:

[...]
[DllImport(kernel32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetWindowsDirectory')]
function GetWindowsDirectory; external;
[DllImport(advapi32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetUserName')]
function GetUserName; external;
[...]

Jak widać, zmienił się nieco sposób ich importu w aplikacji. W wielu przypadkach użycie funkcji Win32 stanie się wręcz niezastąpione, tak więc w tej części rozdziału zajmiemy się wykorzystaniem bibliotek Win32 DLL w aplikacjach .NET.

W tym celu będziemy korzystać z mechanizmu zwanego Platform Invocation Service, czyli Platform Invoke, zwanego w skrócie PInvoke (lub P/Invoke). Mechanizm ów pozwala na importowanie funkcji z bibliotek Win32 DLL za pomocą atrybutu [DllImport].

W przypadku Delphi sprawa jest nieco ułatwiona, gdyż programiści Borlanda sami zadbali o modyfikację modułu Windows, tak aby można było korzystać z funkcji Win32 za jego pomocą.

Mówiąc o module Windows, mam w rzeczywistości na myśli moduł Borland.Vcl.Windows. Dla Delphi nie stanowi różnicy, czy do listy uses dodamy moduł Windows, czy też Borland.Vcl.Windows. Ułatwia to proces przenoszenia projektu z wcześniejszych wersji Delphi.

Wywołanie standardowe

Jeżeli chodzi o importowanie procedur z bibliotek DLL, to w Delphi nie uległo ono zmianie. Nadal istnieje słowo kluczowe external, które umożliwia załadowanie procedury w sposób statyczny. Poniższy fragment kodu to biblioteka DLL skompilowana w wersji Delphi dla Win32:

library SimpleDLL;

uses
  Windows;

procedure About; stdcall;
begin
  MessageBox(0, 'Hello World!', 'Hello', MB_OK + MB_ICONINFORMATION);
end;

exports
  About name 'About';

begin
end.

Jest to bardzo prosta biblioteka, zawierająca jedną funkcję About, która po prostu wyświetla tekst Hello World. Aby użyć tej funkcji z biblioteki DLL w programie pisanym w Delphi dla .NET, można zastosować poniższy kod:

program DotNet;

procedure About; stdcall external 'SimpleDLL.dll' name 'About';

begin
  About;
end.

Taki kod działa dobrze, a wywołanie funkcji jest proste w użyciu ze względu na brak parametrów tekstowych. Wiele funkcji API (a także tych występujących w zwykłych bibliotekach, napisanych dla systemu Win32) korzysta ze wskaźników i typów PChar, które są niedozwolone w .NET. Aby więc można było skompilować kod w Delphi dla .NET, trzeba rozwiązać pojawiający się problem konwersji typów.

Poprzedni przykładowy fragment kodu, który importuje funkcję DLL, można zapisać nieco inaczej, z wykorzystaniem atrybutów:

program DotNet;

{$APPTYPE CONSOLE}

uses Windows, System.Runtime.InteropServices;

[DllImport('SimpleDLL.dll')]
procedure About; external;     

begin
  About;
end.

Użycie atrybutu DLLImport

Przed skorzystaniem z atrybutu DllImport trzeba dodać moduł System.Runtime.InteropServices do listy uses. Użycie tego atrybutu w najprostszym wydaniu prezentuje poprzedni przykład. Jego budowa jest dość prosta, gdyż w takim przypadku należy podać jedynie nazwę biblioteki DLL:

[DllImport('Nazwa biblioteki DLL')];

Taki atrybut nie posiada żadnych dodatkowych parametrów. Normalnie jest możliwe określenie konwencji wywołania parametrów (cdecl, stdcall itp.), nazwy ładowanej procedury bądź funkcji oraz kodowania ciągów znakowych (Unikod, ANSI String). Parametry atrybutu DllImport można określać tak:

[DllImport('SimpleDLL.dll', CallingConvention = CallingConvention.Stdcall, EntryPoint='About')]

W powyższym przykładzie sprecyzowałem sposób wywołania parametrów funkcji (parametr CallingConvention) oraz określiłem dokładnie nazwę importowanej funkcji (EntryPoint).

Istnieje jeszcze jeden parametr używany tylko w przypadku, gdy w parametrze funkcji lub procedury znajduje się ciąg znaków (String). Tym parametrem jest CharSet, który określa kodowanie:

*Ansi — ciąg znakowy ANSI,
*Unicode — ciągi znakowe Unikodu,
*None — oznacza to samo co parametr Ansi.

W przypadku Win32 API wiele funkcji miało dwie odmiany — jedną z parametrem typu PAnsiChar, a drugą z PWideChar. Dla przypomnienia: wszystkie ciągi znakowe w .NET są zapisane w Unikodzie, także typ String jest teraz równoważny typowi WideString.

Jeżeli więc importujemy funkcję z biblioteki, która posiada parametry AnsiString, możemy to zapisać następująco:

[DllImport('nazwa.dll', CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
function DoSomethingA(const Msg: String): Boolean;
external;

Parametry wyjściowe

Windows był pisany w języku C, stąd też przykłady w dokumentacji WinAPI są zapisane w tym języku. Wiele funkcji zwracało rezultaty do parametru referencyjnego — warto tu wspomnieć chociażby o funkcjach GetUserName czy GetComputerName. Ich deklaracja w C wyglądała tak:

BOOL GetComputerName(
    LPTSTR lpBuffer,    // address of name buffer 
    LPDWORD nSize       // address of size of name buffer 
   );
BOOL GetUserName(
    LPTSTR lpBuffer,    // address of name buffer 
    LPDWORD nSize       // address of size of name buffer 
   );

Po przekształceniu tego zapisu do języka Delphi w środowisku Win32 owe funkcje zostały zadeklarowane następująco:

function GetComputerName(lpBuffer: PChar; var nSize: DWORD): BOOL; stdcall;
function GetUserName(lpBuffer: PChar; var nSize: DWORD): BOOL; stdcall;

Niestety, w .NET nie można użyć typu PChar, tak więc zmieniono zapis tych funkcji w module Windows:

function GetComputerName(lpBuffer: StringBuilder; var nSize: DWORD): BOOL;
function GetUserName(lpBuffer: StringBuilder; var nSize: DWORD): BOOL;

Jak widać, typ PChar został tutaj zastąpiony klasą StringBuilder, co może sprawić trudności w użyciu tej funkcji. Na listingach 11.10 oraz 11.11 zaprezentowałem kod służący do pobierania nazwy użytkownika systemu oraz ścieżki katalogu Windows w Delphi .NET.

Listing 11.10. Pobranie nazwy użytkownika

program DotNet;

{$APPTYPE CONSOLE}

uses Windows, System.Text, System.Runtime.InteropServices;

var
  Buffer : StringBuilder;
  dwSize : DWORD;
begin
  Buffer := StringBuilder.Create(64);
  dwSize := 64; // długość bufora
  GetUserName(Buffer, dwSize);

  Writeln(Buffer.ToString); // wyświetlenie nazwy
  Buffer.Free;
  Readln;
end.

Listing 11.11. Pobranie ścieżki katalogu Windows

program DotNet;

{$APPTYPE CONSOLE}

uses Windows, System.Text, System.Runtime.InteropServices;

var
  Buffer : StringBuilder;
begin
  Buffer := StringBuilder.Create(128);

  GetWindowsDirectory(Buffer, Buffer.Capacity);

  Writeln(Buffer.ToString); // wyświetlenie ścieżki
  Buffer.Free;
  Readln;
end.

Na listingu 11.11 specjalnie zaprezentowałem pobieranie ścieżki katalogu Windows, gdyż to wywołanie różni się nieco od pobierania nazwy użytkownika. W przypadku funkcji GetUserName oba parametry muszą być zmiennymi, a w przypadku GetWindowsDirecory można podać wartość właściwości @@Capacity@@, która zwraca długość bufora.

Funkcje GetUserName i GetWindowsDirectory zostały zachowane ze względów kompatybilności. Nastąpiła pewna zmiana, jeśli chodzi o sposób ich używania, dlatego pokazałem Czytelnikowi, w jaki sposób przekształcać napisane wcześniej programy, tak aby nadal mogły korzystać ze starych i sprawdzonych funkcji. W przyszłości prostsze okaże się zastosowanie klasy Environment, która posiada funkcje zwracające ścieżkę systemu, nazwę zalogowanego użytkownika systemu itp. Przykładowo, odczytanie nazwy użytkownika realizuje taki kod:

Console.WriteLine(Environment.get_UserName);
</dfn>

Zacznijmy jednak od początku. Klasa StringBuilder służy w dużym uogólnieniu do manipulowania danymi tekstowymi. Jako że jest to klasa (podobnie jak String), na początku wywołujemy jej konstruktor, podając jako parametr maksymalną długość tekstu, jaki może pomieścić dany ciąg znaków.

Dane wskaźnikowe

Wskaźniki zostały uznane w .NET za typ niebezpieczny, lecz istnieje sposób na zastąpienie typowych wskaźników innymi rozwiązaniami, takimi jak typ IntPtr oraz klasa Marshal (z przestrzeni nazw System.Runtime.InteropServices), służąca do zarządzania, alokacji i kopiowania bloków pamięci. W tym miejscu można się posłużyć przykładem wysyłania komunikatów, gdzie czasami konieczne było użycie operatora @, aby przekazać parametr do funkcji SendMessage i PostMessage. Posłużę się przykładem wysyłania komunikatu WM_SETTEXT. W Win32 należałoby skorzystać z takiego kodu:

var
  hWindow : HWND;
begin
  hWindow := FindWindow(nil, 'Program testowy');
  SendMessage(hWindow, WM_SETTEXT, 0, Longint(PChar('Nowy tekst')));
end;

Mimo że użycie operatora @ nie było konieczne, trzeba było użyć niejako wskaźnika — typu PChar. W .NET kod realizujący to samo zadanie będzie wyglądał tak:

uses System.Runtime.InteropServices;

procedure TForm1.Button1Click(Sender: TObject);
var
  hWindow : HWND;
  Buffer : IntPtr;
begin
  Buffer := Marshal.StringToHGlobalAuto('Nowy tekst');
  hWindow := FindWindow(nil, 'Program testowy');
  SendMessage(hWindow, WM_SETTEXT, 0, Buffer.ToInt32);

  Marshal.FreeHGlobal(Buffer);
end;

Dwie zmienne zadeklarowane w tej procedurze zapewniają przechowanie uchwytu do okna, do którego zostanie skierowany komunikat (hWindow) oraz jako wskaźnik (Buffer) typu IntPtr.

Na samym początku, korzystając z klasy Marshal, umieszczamy w pamięci nowe dane, które zostaną przekazane komunikatem — w tym przypadku jest to napis* Nowy tekst*. Po odnalezieniu uchwytu okna wysyłamy komunikat WM_SETTEXT, jako drugi parametr podając wartość Buffer.ToInt32. Taka kombinacja jest konieczna z tego względu, iż drugi parametr w funkcji SendMessage musi być typu Integer, a nie IntPtr. Sposób na ominięcie tego ograniczenia jest prosty — funkcja ToInt32 z klasy IntPtr konwertuje dane do liczby 32-bitowej typu Integer.

Pobieranie danych z bufora

Poprzedni przykład bazował na wysyłaniu komunikatu WM_SETTEXT. Sprawdźmy teraz, jak ma się sprawa z odbieraniem danych z wykorzystaniem klasy Marshal oraz typu IntPtr. Do wybranego okna wyślemy komunikat WM_GETTEXT, nakazujący zwrócenie tytułu okna. W tym celu, korzystając z klasy Marshal, konieczne będzie zadeklarowanie 128 bajtów pamięci na potrzeby naszej procedury:

uses System.Runtime.InteropServices;

procedure TForm1.Button1Click(Sender: TObject);
var
  hWindow : HWND;
  Buffer : IntPtr;
begin
  Buffer := Marshal.AllocHGlobal(128);
  hWindow := FindWindow(nil, 'Program testowy');
  SendMessage(hWindow, WM_GETTEXT, 128, Buffer.ToInt32);

  MessageBox(0, Marshal.PtrToStringAuto(Buffer), '', 0);

  Marshal.FreeHGlobal(Buffer);
end;

Alokację pamięci zapewnia procedura AllocHGlobal klasy Marshal, w której jako parametr należy podać wartość określającą liczbę bajtów do alokacji. W naszym przypadku zaalokowaliśmy 128 bajtów pamięci, zakładając, że tytuł okna nie przekroczy tej długości.

Po wysłaniu komunikatu tytuł okna powinien zostać umieszczony w strukturze @@Buffer@@. Odkodowaniem otrzymanej informacji zajmuje się funkcja PtrToStringAuto, która zwraca dane w postaci typu String.

Oczywiście podany przeze mnie przykład raczej nie znajdzie praktycznego zastosowania, ponieważ skoro znamy tytuł okna (bo używamy go w funkcji FindWindow), niepotrzebnie wysyłamy komunikat WM_GETTEXT. Jednak sama idea odczytywania danych z komunikatów się nie zmienia, przykład ten można więc uznać za wartościowy.

Jako ciekawostkę mogę powiedzieć, że taki sam rezultat można uzyskać, stosując funkcję GetWindowText oraz klasę StringBuilder:

uses System.Runtime.InteropServices, System.Text;

procedure TForm1.Button1Click(Sender: TObject);
var
  hWindow : HWND;
  Buffer : StringBuilder;
begin
  Buffer := StringBuilder.Create(128);
  hWindow := FindWindow(nil, 'Program testowy');
  GetWindowText(hWindow, Buffer, 128);

  MessageBox(0, Buffer.ToString, '', 0);

  Buffer.Free;
end; 

Funkcja GetWindowText jest standardową funkcją Win32 API (zadeklarowaną w module Windows.pas). Dla przypomnienia powiem, iż aby skorzystać z klasy StringBuilder, należy do listy uses dodać przestrzeń nazw System.Text.

Kod zarządzany i niezarządzany

Platforma .NET definiuje dwa nowe rozwiązania: kod zarządzany (managed code) oraz niezarządzany (ang. unmanaged code), które są istotne z punktu widzenia CLR. Pojęcie kod niezarządzany oznacza zwykły kod zarządzany poza środowiskiem .NET, czyli stare aplikacje kompilowane dla środowiska Win32. Natomiast, jak nietrudno się domyślić, kod zarządzany jest kodem wykonywanym pod kontrolą CLR.

Zmiany w Delphi dla .NET są na tyle duże w porównaniu z Win32, że problemem staje się współdziałanie obu rodzajów aplikacji (aplikacji .NET oraz Win32), a także korzystanie z zasobów starszych aplikacji Win32 — np. bibliotek DLL. Dlatego też w .NET wprowadzono mechanizm marshalingu [#]_, który jest związany z określeniem, w jaki sposób dane mają być przekazywane z kodu niezarządzanego do zarządzanego.

Używanie funkcji Win32

Importując do aplikacji funkcje z bibliotek Win32, należy dokonać pewnych zmian, co jest związane z obecnością wskaźników czy innych niedozwolonych danych w parametrach procedur i funkcji. W niektórych przypadkach zmiany nie będą konieczne lub będą minimalne — najczęściej wiążą się z zastąpieniem typu PChar klasą StringBuilder lub wskaźników typem IntPtr. Przykłady użycia typów StringBuilder oraz IntPtr zawarłem we wcześniejszych fragmentach książki. Tabela 11.5 prezentuje rodzaje parametrów, które występują w Win32 oraz w .NET. Pierwsza kolumna zawiera rodzaje typów, które nie są obsługiwane w .NET. Dwie kolejne kolumny wymieniają typy .NET, którymi należy zastąpić nieobsługiwane już rodzaje danych. Należy zwrócić uwagę na parametry wejściowe oraz wyjściowe. Mam tu na myśli parametry przekazywane np. przez wartość lub przez referencję.

Tabela 11.5. Typy Win32 oraz ich odpowiedniki w .NET

Kod niezarządzany Kod zarządzany (Parametr wejściowy) Kod zarządzany (Parametr wyjściowy)
Typ PChar String StringBuilder
Wskaźnik na strukturę (PRect) const TRect; var TRect;
Wskaźnik na typ prosty (PByte) const Byte; const Byte;
Wskaźnik na typ wskaźnikowy (^PInteger) IntPtr IntPtr

Na przykład w bibliotece DLL skompilowanej w Win32 znajduje się taka oto procedura:

  procedure SendChar(var lpPChar : PChar); stdcall;
  begin
    lpPChar := 'Ala ma kota';
  end;

Parametrem owej procedury jest @@lpPChar@@ należący do typu PChar. Teraz w aplikacji .NET typ PChar należy zastąpić typem StringBuilder. Co prawda Borland zaleca w takich sytuacjach użycie typu StringBuilder, ale zwykły String także będzie działał:

[DLLImport('Win32DLL.dll', EntryPoint = 'SendChar', CallingConvention = CallingConvention.Stdcall)]
  procedure SendChar(var lpPChar : String); external;

var
  S : String;
begin
  SendChar(S);
  Console.WriteLine(S); // wyświetl wartość otrzymaną z DLL-a
  Console.ReadLine;
end.

Marshaling

Niedługo Czytelnik się przekona, że istotą marshalingu jest dokładne określenie, z jakim rodzajem danych mamy do czynienia. Dzięki temu dajemy kompilatorowi informację, w jaki sposób ma traktować dane w pamięci. Podstawowym narzędziem określania danych są atrybuty.

Atrybuty [in] oraz [out]

Atrybuty [in] oraz [out] są zadeklarowane w przestrzeni nazw System.Runtime.InteropServices i informują o rodzaju przekazania parametru (przez wartość czy przez referencję). Atrybuty [in] oraz [out] są opcjonalne — niosą jednak dodatkową informację o rodzajach parametru. Atrybuty należy wstawić tuż przed deklaracją rzeczywistego parametru:

procedure SendChar([out] var lpPChar : String); external;

Atrybut [in] mówi, iż wartość danego parametru jest przekazywana przez wartość lub stałą. Atrybut [out] informuje o przekazaniu parametru przez referencję.

Przekazywanie struktur

Największe różnice przynosi przenoszenie struktur (czyli np. rekordów) z kodu niezarządzanego do zarządzanego i odwrotnie (przekazywanie rekordów do procedur z kodu niezarządzanego). Jeżeli np. rekord zawiera tablice i typy PChar, to konieczne będzie przekształcenie owych typów na dane akceptowane przez kompilator Delphi .NET. To nie wszystko — potrzebna będzie także usługa marshalingu w celu szczegółowego określenia zawartości rekordu.

Spójrzmy na następujący rekord, znajdujący się w bibliotece DLL skompilowanej w Delphi 2:

  type
    TWin32Rec = record
      Buffer : array[0..127] of Char;
      lpPChar : PChar;
      Numbers : array[1..2] of Byte;
    end;

Przede wszystkim biblioteka zawiera parametr @@Buffer@@, który jest zbiorem znaków (Char). Dodatkowo zawiera ona pole @@lpPChar@@, które jest typu PChar — je także trzeba zastąpić typem String, ale to już wiemy po lekturze poprzednich podrozdziałów. W Delphi .NET taki rekord trzeba będzie przekształcić do następującej postaci:

  type
    TWin32Rec = record
      Buffer : String;
      lpPChar : String;
      Numbers : array[1..2] of Byte;
    end;

Zarówno typ PChar, jak i tablicę Char zastąpiłem typem String. Uwaga! W rekordach nigdy nie będzie potrzebne użycie typu StringBuilder — zawsze należy zastosować typ String.

Zasada zastępowania typu PChar przez String nie jest sztywna. Czasami będzie możliwe lub konieczne użycie IntPtr, o czym Czytelnik przekona się podczas lektury dalszej części rozdziału.

Jak widać, z tego samego rekordu w Delphi 2 oraz Delphi 2005 identyczne zostało jedynie pole @@Numbers@@, które jest dwuelementową tablicą typu Byte. Tabela 11.6 zawiera typy z kodu niezarządzanego i ich odpowiedniki z kodu zarządzanego, które należy zmienić w polach struktur.

Tabela 11.6. Typy Win32 oraz ich odpowiedniki w .NET
Kod niezarządzany | Kod zarządzany (Parametr wejściowy) | Kod zarządzany (Parametr wyjściowy)
Tablica znaków | String | String
Dowolna tablica typu liczbowego (array[0..1] of Byte) | array[0..1] of Byte | array[0..1] of Byte
Tablica dynamiczna | IntPtr | IntPtr
Wskaźnik do struktury (np. PRect) | IntPtr | IntPtr
Wskaźnik do typu prostego (np. PByte) | IntPtr | IntPtr
Wskaźnik do typu wskaźnikowego (np. ^PInteger) | IntPtr | IntPtr

Inne atrybuty marshalingu

Oprócz oczywistego tłumaczenia rekordu w Delphi dla .NET należy użyć także dodatkowych atrybutów określających wcześniejszą budowę rekordu w Win32. Po zastosowaniu atrybutów nasz rekord wygląda następująco:

  type
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    TWin32Rec = record
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
      Buffer : String;
      [MarshalAs(UnmanagedType.LPStr, SizeConst = 30)]
      lpPChar : String;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
      Numbers : array[1..2] of Byte;
    end;

Pierwszy atrybut, StructLayout, określa warstwę rekordu: położenie elementów oraz kodowania (w tym przypadku ANSI). Pierwsza wartość, LayoutKind, nie był wcześniej omawiany — może przybierać następujące wartości:

*LayoutKind.Automatic — zezwala CLR na porządkowanie elementów rekordu według własnego uznania, tak aby było to jak najbardziej efektywne.
*LayoutKind.Sequential — położenie poszczególnych rekordów musi być identyczne z tym, jak je zadeklarowano.
*LayoutKind.Explicit — zaawansowany parametr. Daje możliwość samodzielnego określania konkretnego pola za pomocą innego atrybutu — FieldOffset.

Kolejny atrybut, MarshalAs, służy do szczegółowego określania konkretnego pola — jego typu oraz długości. Szczególną rolę odgrywa tu parametr UnmanagedType, którego zadaniem jest szczegółowe określanie typu pola w rekordzie. Parametr ten może przybrać wartości takie jak w tabeli 11.7.

Tabela 11.7. Możliwe wartości parametru UnmanagedType
UnmanagedType.LPStr | Znakowy typ wskaźnikowy — np. PChar (typu ANSI) lub wskaźnik na tablicę.
UnmanagedType.LPWStr | Znaki typu Unikod lub wskaźnik na tablicę.
UnmanagedType.LPTStr | Wskaźnik na tablicę.
UnmanagedType.ByValTStr | Tablica znaków (Char) z ograniczoną długością.
UnmanagedType.ByValArray | Tablica dowolnego typu.

Oprócz wspomnianego wcześniej parametru UnmanagedType we wcześniejszym przykładzie użyłem także parametru SizeConst, który deklaruje długość konkretnego pola. Na przykład zmienna @@Buffer@@ w Win32 była tablicą 128-elementową, więc długość zmiennej na pewno nie przekroczy 128 znaków. Nieco kłopotliwe może być tylko określenie długości zmiennej @@lpPChar@@. Nie wiadomo bowiem dokładnie, jaką długość posiada zmienna, a pozostawiając atrybut MarshalAs bez parametru SizeConst, program nie będzie działał zgodnie z oczekiwaniami. Warto więc przyjąć sobie górną granicę, jakiej się spodziewamy — ja nadałem elementowi @@lpPChar@@ długość 30 znaków.

Listing 11.12 prezentuje przykładową bibliotekę DLL, która eksportuje rekord TWin32Rec. Na listingu 11.13 z kolei znajduje się kod aplikacji napisanej w Delphi dla .NET, odczytującej wartości eksportowanego rekordu.

Listing 11.12. Przykładowa biblioteka Win32 DLL

library Win32DLL;

{ kompilowane w Delphi 2 (Win32) }

{ Important note about DLL memory management: ShareMem must be the
  first unit in your library's USES clause AND your project's (select
  View-Project Source) USES clause if your DLL exports any procedures or
  functions that pass strings as parameters or function results. This
  applies to all strings passed to and from your DLL--even those that
  are nested in records and classes. ShareMem is the interface unit to
  the DELPHIMM.DLL shared memory manager, which must be deployed along
  with your DLL. To avoid using DELPHIMM.DLL, pass string information
  using PChar or ShortString parameters. }

uses Windows;

  type
    TWin32Rec = record
      Buffer : array[0..127] of Char;
      lpPChar : PChar;
      Numbers : array[1..2] of Byte;
    end;

  procedure SendBuf(var Win32Rec : TWin32Rec); stdcall;   
  begin
  { eksport rekordu }
    with Win32Rec do
    begin
      Buffer := 'Znaki typu Char';
      lpPChar := 'Ciąg znakowy PChar';
      Numbers[1] := 1;
      Numbers[2] := 2;
    end;
  end;

exports
  SendBuf name 'SendBuf';

begin
end.

Listing 11.13. Przykładowy program .NET współpracujący z biblioteką DLL

program DotNetDemo;

{$APPTYPE CONSOLE}

uses
  System.Runtime.InteropServices,
  System.Text;
                 
  type
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    TWin32Rec = record
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
      Buffer : String;
      [MarshalAs(UnmanagedType.LPStr, SizeConst = 30)]
      lpPChar : String;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
      Numbers : array[1..2] of Byte;
    end;

  [DLLImport('Win32DLL.dll', EntryPoint = 'SendBuf', Charset = CharSet.Ansi, CallingConvention = CallingConvention.Stdcall)]
  procedure SendBuf([out] var Win32Rec : TWin32Rec); external;

var
  Win32Rec : TWin32Rec;

begin
  SendBuf(Win32Rec);
  Console.WriteLine('Tablica Char: ' + Win32Rec.Buffer);
  Console.WriteLine('Zmienna PChar: ' + Win32Rec.lpPChar);
  Console.WriteLine('Tablica Numbers: [' + Convert.ToString(Win32Rec.Numbers[1]) + '][' + Convert.ToString(Win32Rec.Numbers[2]) + ']');
  Console.ReadLine;
end.

Po uruchomieniu programu importuje on funkcję SendBuf z biblioteki Win32DLL.dll. Funkcja SendBuf wysyła zawartość rekordu do zmiennej @@Win32Rec@@, a ten na końcu zostaje wyświetlony na konsoli.

Myślę, że po tym omówieniu budowa tak aplikacji, jak i biblioteki DLL powinna być zrozumiała.

Wskaźnik na strukturę

Do tej pory nie pokazywałem jeszcze rozwiązania problemu parametru procedury, który jest wskaźnikiem na rekord. Rozwiązanie tego problemu zaprezentuję na przykładzie programu odczytującego tag ID3v1 z pliku mp3.

Niegdyś napisałem bibliotekę DLL, która odczytywała informacje na temat pliku mp3 (wykonawca, tytuł itp.). Biblioteka działa znakomicie w środowisku Win32. Teraz chciałbym ją wykorzystać w programie .NET. Biblioteka (listing 11.14) eksportowała rekord z informacjami o pliku mp3 w postaci wskaźnika na rekord, zatem powstaje pytanie: jak zastąpić ten wskaźnik w programie w Delphi .NET? Wbrew pozorom rozwiązanie jest bardzo proste.

Listing 11.14. Kod źródłowy biblioteki DLL Win32

{
  Copyright (c) 2003 by Adam Boduch
}
library mp3DLL;

uses Windows;


type
{ rekord, który będzie eksportowany do aplikacji }
  TMp3 = packed record
    ID: String[3]; // czy tag istnieje?
    Title : String[30]; // tytuł
    Artist : String[30]; // wykonawca
    Album : String[30]; // album
    Year : String[4]; // rok wydania
    Comment : String[30]; // komentarz
    Genre : String[30]; // typ - np. pop, techno, jazz itp.
  end;
  PMp3 = ^TMp3;



const
{  oto tablica zawierająca typy utworów }
  GenreArray : array[0..79] of ShortString = (
  ('Blues'), ('Classic Rock'), ('Country'), ('Dance'), ('Disco'),
  ('Funk'), ('Grunge'), ('Hip-Hop'), ('Jazz'), ('Metal'), ('New Age'),
  ('Oldies'), ('Other'), ('Pop'), ('R&B'), ('Rap'), ('Reggae'),
  ('Rock'), ('Techno'), ('Industrial'), ('Alternative'), ('Ska'),
  ('Death Metal'), ('Pranks'), ('Soundtrack'), ('Euro-Techno'), ('Ambient'),
  ('Trip-Hop'), ('Vocal'), ('Jazz+Funk'), ('Fusion'), ('Trance'),
  ('Classical'), ('Instrumental'), ('Acid' ), ('House'), ('Game'),
  ('Sound Clip'), ('Gospel'), ('Noise'), ('AlternRock'), ('Bass'),
  ('Soul'), ('Punk'), ('Space'), ('Meditative'), ('Instrumental Pop'),
  ('Instrumental Rock'), ('Ethnic'), ('Gothic'), ('Darkwave'),
  ('Techno-Industrial'), ('Electronic'), ('Pop-Folk'), ('Eurodance'),
  ('Dream'), ('Southern Rock'), ('Comedy'), ('Cult'), ('Gangsta'),
  ('Top 40'), ('Christian Rap'), ('Pop/Funk'), ('Jungle'), ('Native American'),
  ('Cabaret'), ('New Wave'), ('Psychadelic'), ('Rave'), ('Showtunes'),
  ('Trailer'), ('Lo-Fi'), ('Tribal'), ('Acid Punk'), ('Acid Jazz'),
  ('Polka'), ('Retro'), ('Musical'), ('Rock & Roll'), ('Hard Rock')
  );

procedure LoadTag(const lpFileName : PChar; Tag : PMp3); stdcall;
var
  F : File;
  Buffer : array[1..128] of char;
begin
  AssignFile(F, String(lpFileName));
  try
    Reset(F, 1);
    { przesunięcie pozycji na 128 bajt od końca }
    Seek(F, FileSize(F) - 128);
    BlockRead(F, Buffer, 128); // odczytanie zawartości bufora

    { do rekordu przypisz informacje odczytane z pliku mp3 }
    with Tag^ do
    begin
      ID := Copy(Buffer, 1, 3);
      Title := Copy(Buffer, 4, 30);
      Artist := Copy(Buffer, 34, 30);
      Album := Copy(Buffer, 64, 30);
      Year := Copy(Buffer, 94, 4);
      Comment := Copy(Buffer, 98, 30);
      Genre := GenreArray[Ord(Buffer[128])];
    end;
    
  finally
    CloseFile(F);
  end;
end;

exports
  LoadTag name 'LoadTag';

begin
end.

Działanie biblioteki jest w gruncie rzeczy proste. Dzięki znajomości budowy pliku mp3 wiadomo, gdzie znajdują się informacje dotyczące tagu — w ostatnich 128 bajtach pliku. Teoretycznie proces odczytu tejże informacji jest następujący:

#Otwieramy plik MP3 na podstawie ścieżki podanej przez użytkownika.
#Znajdujemy ostatnie 128 bajtów w pliku.
#Odczytujemy ostatnie 128 bajtów informacji z pliku.
#Rozdzielamy odczytane informacje na poszczególne pola z rekordu.
#Utworzony rekord wraz z zawartością eksportujemy na zewnątrz biblioteki.

Praktyczne rozwiązanie tego problemu znajduje się na listingu 11.14.

Program korzystający z biblioteki Win32

Program, który teraz pokażę, będzie aplikacją konsolową wywoływaną z poziomu wiersza poleceń. Użytkownik, uruchamiając program, będzie musiał jako parametr podać ścieżkę do pliku mp3:

getMp3 C:\mp3.mp3

Odczyt parametru przekazanego do programu jest możliwy dzięki klasie Envrionment:

Args := Environment.GetCommandLineArgs;

Zmienna @@Args@@ jest tablicą dynamiczną typu String. Funkcja GetCommandLineArgs zwraca parametry przekazane do programu w formie tablicy. Zmienna @@Args[1]@@ będzie zawierać pierwszy parametr (w naszym przypadku ścieżkę do analizowanego pliku mp3), @@Args[2]@@ drugi itd.

Struktura rekordu, jaki jest zwracany przez funkcję LoadTag z biblioteki DLL, jest znana:

type
[StructLayout(LayoutKind.Sequential, Pack = 1, Charset = Charset.Ansi)]
TMp3 = packed record
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
  ID: String[3];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Title : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Artist : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Album : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
  Year : String[4];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Comment : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Genre : String[30];
end;

Oczywiście, za pomocą odpowiednich atrybutów trzeba dokładnie określić elementy struktury i ich rozmiar.

Nie wspominałem wcześniej o parametrze Pack atrybutu StructLayout. Jest on związany z pamięcią zajmowaną przez rekord. Normalnie użycie samego słowa record w trakcie deklarowania rekordu powoduje, iż rozmiar elementów owego rekordu zajmowany w pamięci jest zaokrąglany. Dopiero użycie słowa kluczowego packed „kompresuje” rekord. Parametr Pack określa właśnie zaokrąglanie pamięci w rekordzie (w tym przypadku do jednego bajta). Listing 11.15 zawiera pełny kod źródłowy programu importującego bibliotekę mp3DLL.dll z funkcją LoadTag.

Listing 11.15. Kod źródłowy programu wykorzystującego bibliotekę Win32

program P11_15;

{$APPTYPE CONSOLE}

uses
  System.Text,
  SysUtils, { <-- wymagany przez funkcję FileExists }
  System.Runtime.InteropServices;

type
[StructLayout(LayoutKind.Sequential, Pack = 1, Charset = Charset.Ansi)]
TMp3 = packed record
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
  ID: String[3];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Title : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Artist : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Album : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
  Year : String[4];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Comment : String[30];
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)]
  Genre : String[30];
end;


  [DllImport('mp3DLL.dll', EntryPoint = 'LoadTag', CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
  procedure LoadTag(const lpFileName : String; var Tag : TMp3); external;

var
  Tag : TMp3;
  Args : array of String;
begin
  try
  { odczyt argumentów (parametrów) przekazanych do programu }
    Args := Environment.GetCommandLineArgs;

  { sprawdzenie, czy użytkownik podał poprawną ścieżkę do pliku mp3 }
    if ((Length(Args[1]) = 0) or (FileExists(Args[1]) = False)) then
    begin
      Console.WriteLine('Podaj ścieżkę do pliku mp3');
    end else
    begin
    { wywołanie funkcji z biblioteki DLL }
      LoadTag(Args[1], Tag);

    { jeżeli plik MP3 posiada znacznik - wyświetlenie zawartości rekordu }
      if Tag.ID = 'TAG' then
      begin
        Console.WriteLine('Plik: {0}', Args[1]);
        Console.WriteLine('---------------------------');
        Console.WriteLine('Tytuł: ' + Tag.Title);
        Console.WriteLine('Wykonawca: ' + Tag.Artist);
        Console.WriteLine('Album: ' + Tag.Album);
        Console.WriteLine('Rok produkcji: ' + Tag.Year);
        Console.WriteLine('Komentarz: ' + Tag.Comment);
        Console.WriteLine('Gatunek: ' + Tag.Genre);
      end else
      begin
        Console.WriteLine('Brak informacji o utworze!');
      end;
    end;
  except
  { obsługa wyjątków }
    on E : TypeLoadException do
      Console.WriteLine('Nie można załadować biblioteki: ' +
        E.Message);
    on E : Exception do
      Console.WriteLine(E.Message);
  end;
end.

Warto porównać nagłówek procedury LoadTag z nagłówkiem z biblioteki DLL. Typ PChar został zastąpiony typem String, a zamiast wskaźnika na rekord (PMp3) wpisałem typ zwykły — TMp3.

Na samym początku działania programu trzeba sprawdzić, czy użytkownik wywołał aplikację z parametrem oraz czy ścieżka do pliku jest prawidłowa. Dopiero później można użyć procedury LoadTag i wyświetlić zawartość rekordu. Oto rezultat działania programu:

Plik: D:\A.Boduch - Club House.mp3
---------------------------
Tytuł: Club House
Wykonawca: Adam Boduch
Album:
Rok produkcji: 2004
Komentarz: Club-House DJ's Set
Gatunek:

Dobrym zwyczajem w przypadkach takich jak ten jest implementowanie obsługi wyjątków. W opisywanym programie obsłużyłem standardowy wyjątek TypeLoadException, który występuje w sytuacjach, gdy niemożliwe jest załadowanie biblioteki (np. plik został usunięty). Druga obsługa wyjątku nie jest związana z żadnym konkretnym typem — wyświetla standardowy komunikat błędu.

Przekazywanie struktur

W ostatnim przykładzie pokażę, w jaki sposób można najpierw przekazać strukturę do funkcji z biblioteki DLL, aby następnie ją odzyskać. W praktyce takie rozwiązanie jest rzadko spotykane. Wyobraźmy sobie jednak, że kod w bibliotece DLL realizuje jakieś operacje na przekazanym rekordzie — np. szyfruje pola rekordu typu String. Po zaszyfrowaniu tych pól zwraca taki rekord z powrotem do aplikacji.

Na samym początku w aplikacji .NET raz zadeklarowany rekord zostanie przekazany do biblioteki DLL. Biblioteka DLL po otrzymaniu rekordu umiejscowi go w pamięci. Ta sama biblioteka będzie posiadała również funkcję GetRecord, która zwróci wskaźnik na ten sam rekord. Listing 11.16 zawiera kod źródłowy przykładowej biblioteki DLL.

Listing 11.16. Przykładowy kod źródłowy biblioteki DLL

library Win32DLL;

{ Należy kompilować w Delphi dla Win32 } 

uses
  Windows;

  type
  { deklaracja przykładowego rekordu }
    TWin32Rec = record
      Buffer : array[0..127] of Char;
      lpPChar : PChar;
      Numbers : array[1..2] of Byte;
    end;
    PWin32Rec = ^TWin32Rec; // wskaźnik na strukturę

  var
  { zmienna będzie przechowywała dane rekordu w pamięci na czas działania
    biblioteki }
    Rec : TWin32Rec;

  procedure SetRecord(Win32Rec : TWin32Rec); stdcall;
  begin
  { przekazane dane zapisz do zmiennej Rec }
    Rec := Win32Rec;
  end;

  function GetRecord : PWin32Rec; stdcall;
  begin
  { zwróć wskaźnik na rekord }
    Result := @Rec;
  end;

exports
  SetRecord name 'SetRecord',
  GetRecord name 'GetRecord';


begin
end.

Laik mógłby powiedzieć, że istnienie takiej biblioteki jest bez sensu. W gruncie rzeczy ma rację, ponieważ zawarte w niej funkcje odbierają, a następnie wysyłają rekord do aplikacji.

W programie .NET korzystającym z takiej biblioteki trzeba będzie użyć typu IntPtr oraz klasy Marshal. Wszystko dlatego, że funkcja GetRecord zwraca wskaźnik na rekord, a nie jego kopię. Dzięki temu w pamięci nie jest tworzona kopia danych, która miałaby być przesyłana do aplikacji.

Dodatkowo, po zapisaniu rekordu TWin32Rec w programie .NET zamiast typu String użyłem IntPtr:

  type
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    TWin32Rec = record
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
      Buffer : String;
      lpPChar : IntPtr;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
      Numbers : array[1..2] of Byte;
    end;

Pozostałe pola rekordu pozostają niezmienione — rekord jest identyczny jak ten z programu pokazanego na listingu 11.17 z wyjątkiem, że pole @@lpPChar@@ jest typu IntPtr. Listing 11.17 prezentuje cały kod źródłowy programu.

Listing 11.17. Kod źródłowy przykładowego programu

program P11_17;

{$APPTYPE CONSOLE}

uses System.Runtime.InteropServices;


  type
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    TWin32Rec = record
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
      Buffer : String;
      lpPChar : IntPtr;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
      Numbers : array[1..2] of Byte;
    end;


  [DLLImport('Win32DLL.dll', CharSet = CharSet.Ansi, EntryPoint = 'SetRecord', CallingConvention = CallingConvention.Stdcall)]
  procedure SetRecord([in] Win32Rec : TWin32Rec); external;

  [DLLImport('Win32DLL.dll', CharSet = CharSet.Ansi, EntryPoint = 'GetRecord', CallingConvention = CallingConvention.Stdcall)]
  function GetRecord : IntPtr; external;

var
  Win32Rec : TWin32Rec;
  Buffer : IntPtr;
begin
  Console.WriteLine('Przekazywanie rekordu: kod zarządzany do niezarządzanego...');

{ przypisanie wartości do rekordu }
  Win32Rec.Buffer := 'Znaki typu Char';
  Win32Rec.lpPChar := Marshal.StringToHGlobalAnsi('Ciąg znakowy PChar');
  Win32Rec.Numbers[1] := 1;
  Win32Rec.Numbers[2] := 2;

{ wysłanie rekordu do biblioteki DLL }
  SetRecord(Win32Rec);
  Console.WriteLine('Przekazano...');

  Console.WriteLine('Odbieranie rekordu z kodu niezarządzanego');

{ odebranie rekordu i przypisanie do zmiennej typu IntPtr }
  Buffer := GetRecord;

{ przekształcenie bufora otrzymanych danych na normalny rekord }
  Win32Rec := TWin32Rec(Marshal.PtrToStructure(Buffer, TypeOf(TWin32Rec)));

  Console.WriteLine('Tablica Char: ' + Win32Rec.Buffer);
  Console.WriteLine('Zmienna PChar: ' + Marshal.PtrToStringAnsi(Win32Rec.lpPChar));
  Console.WriteLine('Tablica Numbers: [' + Convert.ToString(Win32Rec.Numbers[1]) + '][' + Convert.ToString(Win32Rec.Numbers[2]) + ']');
  
end.

Na samym początku pola rekordu muszą zostać wypełnione, aby mogły zostać przesłane do funkcji SendRecord. W celu przypisania danych tekstowych do pola @@lpPChar@@ należało skorzystać z funkcji StringToHGlobalAnsi z klasy Marshal. Przekazanie rekordu do kodu niezarządzanego jest prostsze niż jego odebranie.

Funkcja GetRecord zwraca dane w postaci zmiennej IntPtr, więc jej odczytanie nie stanowi problemu:

Buffer := GetRecord;

Zmienna @@Buffer@@ także jest typu IntPtr. Od tego momentu posiadamy już dane, które chcemy odczytać. Problemem pozostaje konwersja danych do postaci rekordu. Z tego względu konieczne było użycie funkcji PtrToStructure z klasy Marshal:

Win32Rec := TWin32Rec(Marshal.PtrToStructure(Buffer, TypeOf(TWin32Rec)));

Pierwszym parametrem funkcji PtrToStructure musi być zmienna typu IntPtr, drugim typ (rekord), na który odbędzie się „rzutowanie” danych. Po takim zabiegu pola rekordu zostaną umiejscowione we właściwych pozycjach zmiennej @@Win32Rec@@.

Wady PInvoke

Oprócz oczywistych zalet zastosowania PInvoke (możliwość używania funkcji Win32) istnieją także wady takiego rozwiązania. Przede wszystkim biblioteka CLR nie jest w stanie zapewnić bezpieczeństwa niezarządzanego kodu. Funkcje z bibliotek DLL nie podlegają bezpośrednio CLR — nie ma gwarancji ich poprawnego wykonania.

Po drugie, użycie PInvoke jednocześnie ogranicza działanie aplikacji jedynie do systemu Windows. Co prawda, na dzień dzisiejszy platforma .NET działa jedynie w systemie Windows, ale niewykluczone jest, ba — nawet całkiem realne, iż powstaną implementacje dla innych platform (np. dla Windows CE czy Linuksa). Wówczas aplikacje korzystające z funkcji Win32 nie będą działały. To dlatego w trakcie użycia modułów typowych dla Windows, np. Windows i Messages, Delphi wyświetla komunikat ostrzegawczy mówiący o tym, że taki program może nie działać na innych platformach.

.NET a obiekty COM

Obiekty COM w modelu programowania Win32 umożliwiały tworzenie komponentów wielokrotnego użytku. Raz napisane kontrolki mogły zostać użyte w każdym języku programowania obsługującym obiekty COM (czyli np. Delphi, C++ Builder czy Visual C++). W .NET operacje te zostały uproszczone za sprawą wprowadzenia kodu pośredniego (IL) oraz specyficznej budowy podzespołów. Użytkownik jest również zwolniony z obowiązku rejestracji kontrolki (tak jak to było w przypadku COM), a współpraca pomiędzy poszczególnymi podzespołami jest prosta.

W rozdziale 8. pokazałem, w jaki sposób poszczególne podzespoły mogą komunikować się ze sobą oraz wykorzystywać klasy z innych podzespołów. Jednak cały ten proces odbywał się w obrębie kodu zarządzanego (managed code). W tym podrozdziale zaprezentuję sposób stosowania kodu znajdującego się w podzespole .NET przez aplikację Win32, działającą w obrębie kodu niezarządzanego (unmanaged code).

Terminologia COM

Co prawda, już w rozdziale 8. wspominałem o technologii COM i dałem Czytelnikowi pewne ogólne pojęcie, czym jest COM. Była to jednak czysta teoria. W niniejszej książce nie będę opisywał sposobu tworzenia obiektów COM, gdyż owa technologia została uznana za przestarzałą. Platforma .NET Framework posiada jednak możliwość używania kontrolek COM i udostępniania podzespołów aplikacjom COM. Nim zaczniemy, powinienem jednak wprowadzić kilka pojęć związanych z technologią Component Object Model.

Obiekt COM

Obiekt COM jest fragmentem kodu binarnego, który wykonuje pewną funkcję. Obiekt COM ujawnia aplikacji pewne metody, umożliwiające dostęp do jej funkcjonalności. Dostęp do tych metod odbywa się poprzez tzw. interfejsy COM.

W dalszej części rozdziału słów obiekt i kontrolka w rozumieniu obiektu COM będę używał zamiennie.

Interfejs COM

Interfejs COM w najprostszym rozumieniu pozwala na używanie obiektu COM. Jest pośrednikiem pomiędzy aplikacją a kodem znajdującym się w kontrolce. Z punktu widzenia Delphi interfejs jest obiektem przypominającym klasę (deklarowany z użyciem słowa kluczowego interface).

Jedna kontrolka COM może zawierać jeden lub więcej interfejsów.

GUID

GUID jest akronimem słów Global Unique Identifier. Jest to 128-bitowa liczba określająca unikalny identyfikator kontrolki. GUID jest wykorzystywany m.in. przez system do identyfikacji kontrolki (kontrolka musi być zarejestrowana w systemie). Z naszego punktu widzenia GUID nie ma większego znaczenia. Wygenerowanie odpowiedniego numeru zapewni Delphi po użyciu kombinacji Ctrl+Shift+G.

Serwer COM

Mianem serwera COM określa się zwykły plik binarny (np. z rozszerzeniem *.dll) zarejestrowany w systemie. Serwer COM (inaczej mówiąc, kontrolka COM) zawiera wcześniej wspomniane interfejsy oraz klasy służące do „porozumiewania” się z klientem, czyli aplikacją, która korzysta z serwera COM.

Biblioteki typu

Biblioteka typu jest specjalnym plikiem, zawierającym informację o obiekcie COM. W skład tych informacji wchodzi lista właściwości i metod interfejsów. Biblioteki typu mogą być zawarte w obiekcie COM jako zasoby lub mogą stanowić oddzielny plik. Biblioteka typu jest niezbędna, aby klient mógł odczytać informację o kontrolce, o jej metodach i zwracanych typach. Biblioteka typu jest plikiem z rozszerzeniem nazwy *.tlb.

Mechanizm COM Callable Wrappers

Aplikacje Win32 są aplikacjami niezarządzanymi, zatem mają możliwości ingerencji w komponenty .NET. Aby uzyskać dostęp do komponentu .NET, trzeba zastosować obiekt COM. Skoro jednak obiekty COM również należą do kodu niezarządzanego, można wykorzystać mechanizm zwany COM Callable Wrappers (w skrócie CCW). Tak więc dostęp do obiektu .NET będzie uzyskiwany za pośrednictwem CCW. Z tego względu trzeba będzie zarejestrować podzespół, tak aby mógł być uwzględniany przez klientów COM.
Rysunek 11.3 prezentuje działanie mechanizmu CCW.

Rysunek 11.3. Działanie CCW
11.3.jpg

Podsumujmy: w Delphi dla Win32 można użyć specjalnych funkcji, które utworzą serwer COM, pozwalający na wykorzystanie kodu z podzespołu .NET.

Przykładowy podzespół

Kolejna przykładowa aplikacja, napisana w Delphi dla .NET, ma udostępniać interfejs szyfrujący tekst. Algorytm szyfrowania będzie prostą, aczkolwiek wydajną pętlą.

Funkcja szyfrująca będzie umożliwiała zakodowanie wybranego tekstu przekazanego parametrem @@lpString@@. Kodowanie odbędzie się metodą tzw. xorowania, która jest metodą dość prostą w implementacji, a do tego całkiem wydajną i dobrze nadającą się do szyfrowania prostych tekstów.

Metoda kodowania, zwana potocznie xorowaniem, swoją nazwę wzięła od operatora xor, który umożliwia operowanie na bitach. Wykorzystanie operatora xor może wyglądać tak:

Result := 20 xor 5;

Systemy operacyjne pracują na dwójkowym systemie liczbowym — same zera lub jedynki. W powyższym przykładzie liczba 20 może być liczbą do zakodowania, a 5 hasłem, tak więc liczby te w postaci binarnej mogą wyglądać następująco:

20 = 1110000 
5 = 0001111

Prezentowana metoda szyfrowania polega na zestawieniu tych danych i porównaniu cyfr (przedstawionych w systemie dwójkowym). Jeżeli dwie porównywane cyfry będą takie same (na przykład pierwszą cyfrą jest 0 i druga też jest 0), to wynikiem będzie 0, natomiast w przeciwnym przypadku — 1.

Cała funkcja będzie wyglądała następująco:

function TXorObject.Crypt(lpString, lpPassword: WideString): WideString;
var
  I : Integer;
  PassCount : Integer;
begin
  PassCount := 1;
  Result := lpString; // przypisz wartość początkową

  try

    for I := 1 to Length(lpString) do // wykonuj dla każdej litery osobno
    begin
      {
        Dla każdego osobnego znaku zamieniaj na wartość liczbową, a następnie
        xoruj z każdą literą hasła - powstaje wówczas unikalna kombinacja.
      }
      Result[i] := Chr(Ord(lpString[i]) xor Ord(lpPassword[PassCount]));
      Inc(PassCount);  // zwiększ licznik - kolejna litera hasła
      { Jeżeli licznik przekroczy długość hasła - wyzeruj }

      if PassCount > Length(lpPassword) then PassCount := 1;
    end;

  except
    raise Exception.Create('Błąd w trakcie szyfrowania');
  end;

end;

Kod ten jest w miarę prosty — pewnie Czytelnik spodziewał się wielu linii kodu, a tu wszystko ograniczyło się do jednej tylko funkcji. Funkcja ta wykonuje kodowanie każdego znaku z osobna.

Na samym początku funkcją Ord przekształcamy znak do postaci liczby kodu ASCII. To samo wykonujemy ze znakiem hasła, po czym stosujemy na tych liczbach operator xor. Liczbę zwróconą w rezultacie tego działania ponownie zamieniamy na znak (Char), stosując funkcję Chr.

Nasz podzespół w rezultacie udostępni taką klasę:

TXorObject = class(TObject)
  public
    function Crypt(lpString, lpPassword : String) : String;
    procedure About;
  end;

Pierwsza metoda (która została przedstawiona wcześniej) służy do szyfrowania. Druga z nich — wyświetla proste okno dialogowe z informacją o autorze.

W naszym przykładzie skorzystamy z kompilatora Delphi dla Win32. Wydaje mi się, że jest to niezły przykład prezentujący, w jaki sposób w kompilatorze Win32 można skorzystać z obiektu mieszczącego się w podzespole .NET. Przed użyciem opisywanego podzespołu trzeba użyć specjalnego narzędzia (programu dostarczanego wraz z pakietem .NET Framework), który utworzy z napisanego podzespołu bibliotekę typu.

Pełny kod źródłowy naszego podzespołu .NET jest pokazany na listingu 11.18.

Listing 11.18. Kod źródłowy podzespołu .NET

library XorAssembly;

{%DelphiDotNetAssemblyCompiler '$(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Windows.Forms.dll'}

uses
  System.Reflection,
  System.Runtime.InteropServices,
  System.Windows.Forms;

type
  IXorInterface = interface
  ['{47594C96-6A2E-464D-B64E-984203CE2FF4}']
    function Crypt(lpString, lpPassword : String) : String;
    procedure About;
  end;

  [ClassInterface(ClassInterfaceType.None)]
  TXorObject = class(TObject, IXorInterface)
  public
    function Crypt(lpString, lpPassword : String) : String;
    procedure About;
  end;

const
  AboutStr = 'Kontrolka Xor .NET ' + #13 + 'Copyright (c) 2004 by Adam Boduch';

{ TXorObject }

procedure TXorObject.About;
begin
{ wyświetla okno pokazujące dane autora kontrolki }
  MessageBox.Show(AboutStr, 'O programie')
end;

function TXorObject.Crypt(lpString, lpPassword: WideString): WideString;
var
  I : Integer;
  PassCount : Integer;
begin
  PassCount := 1;
  Result := lpString; // przypisz wartość początkową

  try

    for I := 1 to Length(lpString) do // wykonuj dla każdej litery osobno
    begin
      {
        Dla każdego osobnego znaku zamieniaj na wartość liczbową, a następnie
        xoruj z każdą litera hasła - powstaje wówczas unikalna kombinacja.
      }
      Result[i] := Chr(Ord(lpString[i]) xor Ord(lpPassword[PassCount]));
      Inc(PassCount);  // zwiększ licznik - kolejna litera hasła
      { Jeżeli licznik przekroczy długość hasła - wyzeruj }

      if PassCount > Length(lpPassword) then PassCount := 1;
    end;

  except
    raise Exception.Create('Błąd w trakcie szyfrowania');
  end;
end;

begin

end.

Na samym początku, korzystając z dyrektywy DelphiDotNetAssemblyCompiler, włączyłem do programu podzespół System.Windows.Forms.dll. Dzięki temu możemy w aplikacji skorzystać z przestrzeni nazw System.Windows.Forms:

{%DelphiDotNetAssemblyCompiler '$(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Windows.Forms.dll'}

Kolejną istotną sprawą jest interfejs. Udostępnia on metody umożliwiające interakcję z obiektem COM. Interfejs posiada metody Crypt oraz About, które następnie są dziedziczone w klasie TXorObject:

TXorObject = class(TObject, IXorInterface)

Istotne jest to, że interfejsy nie posiadają implementacji metod. W istocie każde użycie metody z interfejsu powoduje w rzeczywistości wywołanie metody z klasy TXorObject.

W .NET jest możliwe zachowanie tradycyjnej konwencji zapisu GUID, czyli takiej jak na listingu 11.18. Innym sposobem jest skorzystanie z atrybutu GUID:

[Guid('47594C96-6A2E-464D-B64E-984203CE2FF4')]
IDotNetInterface = interface
...
end;
</dfn>

Utworzenie biblioteki typu

Jak wspomniałem wcześniej, utworzenie biblioteki typu oraz jej rejestracja należy do programu RegAsm.exe. Program należy wywołać z poziomu wiersza poleceń z parametrem /tlb, podając również ścieżkę do podzespołu:

C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322>regasm /tlb C:\x\XorAssembly.dll
Microsoft (R) .NET Framework Assembly Registration Utility 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002.  All rights reserved.
Types registered successfully
Assembly exported to 'C:\x\XorAssembly.tlb', and the type library was registered successfully

Ostatni komunikat informuje, że podzespół został wyeksportowany do pliku C:\x\XorAssembly.tlb, a biblioteka typu została zarejestrowana pomyślnie. Oprócz tych komunikatów na konsoli mogą pojawić się ostrzeżenia związane z modułem Borland.Delphi.System, który jest automatycznie włączany do pliku wykonywalnego. Ostrzeżenia są związane z niekompatybilnością z Win32.

Użycie biblioteki typu

W celu skorzystania z biblioteki typu trzeba uruchomić projekt Delphi dla Win32. Środowisko Delphi 2005 zostało wzbogacone o polecenie Import Type Library, które umożliwia zaimportowanie biblioteki typu w celu późniejszego wykorzystania tworzonego podzespołu.

Z menu Component wybiera się Import Component, co spowoduje pojawienie się okna Import Components. Należy zaznaczyć opcję Import Type Library i nacisnąć przycisk Next, co spowoduje wyświetlenie kreatora takiego jak na rysunku 11.4.

11.4.jpg
Rysunek 11.4. Okno Import Type Library

W oknie, na liście należy znaleźć nazwę podzespołu i nacisnąć przycisk Next. W kolejnym kroku można określić kategorie palety narzędzi, na której pojawi się importowany komponent. Pozostawmy domyślne ustawienia. Należy więc kliknąć przycisk Next. W ostatnim kroku Delphi wyświetli zapytanie, czy należy utworzyć osobny moduł dla importowanej kontrolki, czy ewentualnie dołączyć moduł do aktywnego projektu (jeżeli aktualnie programista pracuje nad jakimś projektem). Można wybrać opcję Create Unit i nacisnąć Finish.

W tym momencie Delphi utworzy moduł, który zostanie zapisany w katalogu C:\Program Files\Borland\BDS\3.0\Imports (w przypadku pozostawienia domyślnych ustawień).

Nie wiadomo dlaczego w pliku źródłowym XorAssembly_TLB.pas Delphi generuje następującą konstrukcję, której poprawne skompilowanie bywa niemożliwe:

  TDateTime = packed record
    FValue: TDateTime;
  end;

W takich sytuacjach kompilator wyświetli komunikat o błędzie: Type 'TDateTime' is not yet completely defined. Rozwiązaniem będzie doprowadzenie powyższego rekordu do następującej postaci:

  TDateTime = packed record
    FValue: DateTime;
  end;

To już ostatnia czynność. Teraz można już wykorzystać utworzony moduł we własnej aplikacji.

Korzystanie z klasy COM

Nadszedł czas, aby wykorzystać nowo napisany komponent COM. Uruchommy więc nowy projekt VCL Forms Application — Delphi for Win32. Do listy uses należy dodać następujące moduły:

XorAssembly_TLB, ComObj;

Listing 11.19 prezentuje pełny kod źródłowy modułu głównego aplikacji Delphi dla Win32.

Listing 11.19. Główny moduł aplikacji wykorzystującej obiekt COM

unit MainFrm;

{ Należy kompilować w Delphi Win32 }

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, XorAssembly_TLB, ComObj, StdCtrls;

type
  TMainForm = class(TForm)
    GroupBox1: TGroupBox;
    Label1: TLabel;
    memText: TMemo;
    Label2: TLabel;
    edtPassword: TEdit;
    btnCrypt: TButton;
    btnAbout: TButton;
    procedure btnCryptClick(Sender: TObject);
    procedure btnAboutClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  { zmienna wskazująca na obiekt }
    XorCrypt : IXorInterface;
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnCryptClick(Sender: TObject);
begin
{ jeżeli do zmiennej zostały przypisane jakieś dane -
  wykonaj kod. W tym przypadku wywołuje on metodę Crypt.

  Tekst do zaszyfrowania oraz hasło pobierane jest z
  kontrolki TMemo raz TEdit. Zaszyfrowany tekst ponownie
  przekazywany jest do TMemo
}
  if Assigned(XorCrypt) then
  begin
    memText.Lines.Text := XorCrypt.Crypt(memText.Lines.Text, edtPassword.Text);
  end;
end;

procedure TMainForm.btnAboutClick(Sender: TObject);
begin
{
  jeżeli do zmiennej zostały przypisane jakieś dane -
  wykonaj kod. W tym przypadku wywołuje on metodę About.
}
  if Assigned(XorCrypt) then
  begin
    XorCrypt.About;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ utwórz instancję obiektu COM }
  XorCrypt := CreateComObject(CLASS_TXOROBJECT) as IXorInterface;
end;

end.

Utworzenie obiektu COM w programie z listingu 11.19 odbywa się w zdarzeniu OnCreate formularza. Funkcją, która tworzy obiekt COM jest CreateComObject. Parametrem tej funkcji jest tzw. CLASSID, czyli unikalny identyfikator danej klasy. Generowanie CLASSID dla każdej klasy odbywa się automatycznie, więc projektant nie musi się o to martwić. Działanie naszego przykładowego programu prezentuje rysunek 11.5.

11.5.jpg
Rysunek 11.5. Działanie przykładowej aplikacji

Aplikacja w C#

Prezentowany tu podzespół, który utworzyłem w Delphi, równie dobrze może zostać napisany w każdym języku obsługiwanym przez .NET. Proces wykorzystywania takiej kontrolki przez Delphi jest identyczny. Tak samo należy zarejestrować taki podzespół, używając narzędzia RegAsm.exe. Listing 11.20 przedstawia kod aplikacji, napisanej w C# i wykonującej identyczne zadanie.

Listing 11.20. Klasa szyfrująca napisana w C#

using System;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;



namespace XorNamespace
{

  public interface XorInterface
  {
  string Crypt(string lpString, string lpPassword);
  void About();
  }

  [ClassInterface(ClassInterfaceType.None)]     
  public class XorClass : XorInterface
  {

  public XorClass( )
  {
    // konstruktor
  }

  public void About()
  {
      // metoda About
  }
 
  public string Crypt(string lpString, string lpPassword)
  {
    int i;
    int PassCount;

    StringBuilder Temp = new StringBuilder(lpString);
    PassCount = 0;

    for (i = 0; i < Temp.Length; i++)
    {
         Temp[i] = Convert.ToChar(Convert.ToByte(Temp[i]) ^ Convert.ToByte(lpPassword[PassCount]));
         ++PassCount;

         if (PassCount >= lpPassword.Length)
           PassCount = 0;
    }


    return (Temp.ToString());

  }

  }
}

Należy pamiętać o skompilowaniu takiego programu jako biblioteki DLL:

csc /t:library /out:C:\dotnetassembly.dll C:\dotnetassembly.cs

Istotną sprawą jest fakt, iż taki podzespół musi być umieszczony w tym samym folderze co program Win32, który z niego korzysta! Innym sposobem jest zarejestrowanie napisanej biblioteki w przestrzeni GAC za pomocą narzędzia Gacutil.exe.

Kontrolki COM w aplikacjach .NET

Wykonanie odwrotnego zadania — tzn. import obiektu COM do aplikacji .NET — jest o wiele prostsze. Obiekty Win32 COM mogą zostać zaimportowane do .NET za pomocą narzędzia o nazwie Tlbimp.exe, dołączonego do .NET Framework.

Zasady importowania obiektów COM do .NET przedstawię na przykładzie kontrolki SAPI (Microsoft Speech API). Najpierw odszukajmy na dysku plik sapi.dll. Potem, z poziomu wiersza poleceń, trzeba uruchomić program Tlbimp.exe, który zaimportuje kontrolkę COM do .NET:

tlbimp "C:\Scieżka do pliku\sapi.dll" /verbose /out:C:\Interop.SAPI.dll

Takie użycie programu spowoduje utworzenie podzespołu Interop.SAPI.dll, który można w pełni wykorzystać w środowisku .NET. Normalne jest, że podczas konwersji na konsoli jest wyświetlana masa komunikatów ostrzegających o możliwej niekompatybilności owej kontrolki z .NET.

Po utworzeniu podzespołu trzeba skopiować go do katalogu, w którym następnie należy umieścić nowy projekt WinForms. W pliku *.dpr projektu umieścimy odwołanie do podzespołu:

{%DelphiDotNetAssemblyCompiler 'Interop.SAPI.dll'}

Interfejs aplikacji będzie się składał z komponentów RichTextBox oraz Button. Po klinięciu przycisku ciąg znakowy z pola tekstowego zostanie przekazany do biblioteki, co spowoduje wywołanie lektora, który przeczyta tekst. Rysunek 11.6 prezentuje interfejs programu.

11.6.jpg
Rysunek 11.6. Przykład użycia kontrolki COM w aplikacji .NET

Uruchomienie kodu znajdującego się w podzespole Interop.SAPI.dll wygląda następująco:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Voice: SpVoice;
begin
  Voice := SpVoiceClass.Create;
  Voice.Speak(RichTextBox1.Text, SpeechVoiceSpeakFlags.SVSFDefault);
end;

Nie należy zapomnieć o dołączeniu przestrzeni nazw na liście uses:

uses InterOp.Sapi;

Aplikacje sieciowe

W trybie pracy Delphi dla Win32 możemy wykorzystać wiele technologii, pozwalających na tworzenie aplikacji sieciowych. Wystarczy wspomnieć o IntraWeb czy WebSnap. Technologie te umożliwiały tworzenie programów CGI oraz ISAPI. W Delphi dla .NET (VCl.NET) zostały one zastąpione przez jeden standard — ASP.NET. Migracja do Delphi .NET będzie więc polegała na zmodyfikowaniu kodu w taki sposób, aby korzystał z ASP.NET. Technologiami, które zostały nieuwzględnione w Delphi .NET są: WebBroker, InternetExpress i WebSnap.

Jedną z nowości w Delphi 7 był IntraWeb — technologia pozwalająca na tworzenie w prosty sposób dynamicznych strony WWW. Język Delphi 7 był pierwszą wersją zawierającą IntraWeb. W obecnej wersji (Delphi 2005) również mamy możliwość tworzenia aplikacji IntraWeb, zarówno w VCL, jak i VCL.NET.

Test

  1. Których zakładek z komponentami, obecnych VCL, nie ma już w VCL.NET?
    a) IntraWeb,
    b) WebSnap,
    c) Samples.
  2. Który z niżej wymienionych elementów nie jest uznawany za niebezpieczny w języku Delphi for .NET:
    a) PChar,
    b) File,
    c) Real.
  3. W .NET klasa System.String jest odpowiednikiem typu:
    a) AnsiString,
    b) WideString,
    c) Żadnego z powyższych.
  4. W WinForms przypisanie procedury zdarzeniowej do zdarzenia komponentu jest zapewniane przez:
    a) Operator :=,
    b) Funkcja Include,
    c) Funkcja Import.

FAQ

Czy dyrektywa {$UNSAFECODE} umożliwia wykorzystanie wszystkich elementów uznanych za niebezpieczne?

Niestety, nie. Dyrektywa {$UNSAFECODE} pozwala na wykorzystanie wskaźników, zabronionych operatorów, a także typów PChar, PAnsiChar itd. Nie uprawnia jednak do stosowania takich elementów jak pliki typowane czy funkcje BlockRead i BlockWrite, które fizycznie już w Delphi nie istnieją.

Dlaczego w .NET wprowadzono elementy unsafe?

Microsoft wprowadził elementy unsafe w dużej mierze ze względu na kompatybilność z innymi platformami, na których ma działać .NET. Należy zaznaczyć, że nie jest to wybryk programistów czy dowód ich złośliwości: skoro niektóre z tych elementów zostały zlikwidowane, to na pewno były ku temu powody. Przykładowo, konstrukcja file of (używana w przypadku plików typowanych) została zlikwidowana ze względu na różnice w zrzutach pamięci na różnych platformach.

Czy VCL ma przyszłość?

Wiele osób zapewne sądzi, że skoro biblioteka WinForms jest uniwersalna, wystarczy raz nauczyć się jej działania, aby później wykorzystać ją w Delphi, C# czy VB. Czy VCL zostało w Delphi jedynie ze względów kompatybilności ze wcześniejszymi wersjami? Moim zdaniem VCL było rozwijane przez tyle lat, że dojrzało do miana poważnej biblioteki używanej przez setki tysięcy programistów. Moim zdaniem VCL (teraz już pod nazwą VCL.NET) daje większe możliwości niż WinForms i nadal będzie używana w projektach Delphi, jej istnienie ma więc przyszłość! W końcu, wykorzystując VCL.NET, nasza aplikacja nadal poddaje się kompilacji do IL oraz pozostaje pełnoprawną aplikacją .NET.

VCL.NET jest pewnym pomostem pomiędzy czystym programowaniem pod Win32 API a WinForms. W końcu użycie WinForms nie wyklucza istnienia VCL.NET! Jeżeli ktoś nie zna np. odpowiednika funkcji IntToStr w .NET, to zawsze może dodać moduł SysUtils do listy uses i skorzystać z tej funkcji! Zauważmy, jak dogodna to jest pozycja — mamy możliwość korzystania jednocześnie ze starych funkcji Win32 API, funkcji charakterystycznych dla VCL (np. IntToStr, StringReplace, Pos itp.) oraz klas .NET (np. Convert czy Environment). Wybór odpowiednich rozwiązań zależy od projektanta, od jego upodobań i przyzwyczajeń.

Nie trzeba się martwić o prawidłowe działanie programów VCL.NET na przyszłej platformie Longorn, ponieważ ów nowy system będzie zapewniał zgodność z aplikacjami Win32, a tym bardziej z VCL.NET. Jak powiedział Danny Thorpe (architekt kompilatora Delphi): VCL przetrwał kilka migracji (z Win16 do Win32, z Win32 do Linuksa, a teraz do .NET) więcej niż WinForms, więc można podejrzewać, że łatwiej zaadaptuje się do zmian w systemie Longhorn.

Podsumowanie

Dla programistów piszących dotychczas dla Win32 migracja może być ważnym tematem. Platforma .NET niesie ze sobą nowe możliwości i całkowicie nowy model programowania. Mimo starań firmy Borland, by nowe Delphi było jak najbardziej kompatybilne ze wcześniejszymi wersjami, można natknąć się na pewne problemy z przeniesieniem aplikacji dla .NET. Czas przystosowywania danej aplikacji dla .NET zależy od złożoności projektu oraz wykorzystywanych w nim mechanizmów. Pocieszeniem może okazać się fakt, że VCL nadal jest i — mam nadzieję — będzie standardowym składnikiem Delphi. Gdyby biblioteka ta nie znalazła się w Delphi 2005, przenoszenie aplikacji na WinForms stałoby się niezmiernie trudną czynnością. Należałoby zaprojektować od nowa cały interfejs programu. Faktem jest jednak, że właściwie zawsze można przenieść swój projekt na platformę .NET — kwestią otwartą pozostaje tylko, jak długo to potrwa. Mam nadzieje, że niniejszy rozdział pomógł Czytelnikowi w zaznajomieniu się z różnicami programowania dla Win32 oraz .NET. Jako ciekawostkę mogę dodać jeszcze, iż kod źródłowy gry Quake II został w całości przeniesiony na platformę .NET i w tym przypadku programiści mieli jeszcze bardziej utrudnione zadanie, gdyż musieli przepisać kod źródłowy z C na C++…

.. [#] Chodzi tutaj o funkcję GetHashCode, która zwraca kod jakiegokolwiek obiektu w .NET.
.. [#] Niestety, nie znalazłem dobrego polskiego odpowiednika tego słowa, które w pełni oddawałoby istotę rzeczy.

[[Delphi/Vademecum|Spis treści]]

[[Delphi/Vademecum/Prawa autorskie|©]] Helion 2005. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

3 komentarzy

Zrobiłem prosty test, string jest szybszy jeśli dodajemy jakaś funkcje np. IntToStr(i), sprawdźcie sami:

var
str:TStringBuilder;
s:string;
i,
start,
stop:integer;
begin

start:=GetTickCount;
str:=TStringBuilder.Create;

for i:= 0 to 20000000 do
str.Append('Ala ma kota'+IntToStr(i));

str.Free;
stop:=GetTickCount;
stop:=stop-start;
Showmessage(IntToStr(stop));

start:=GetTickCount;
s:='';
for i:= 0 to 20000000 do
s:=s+'Ala ma kota'+IntToStr(i);
stop:=GetTickCount;
stop:=stop-start;
Showmessage(IntToStr(stop));

Oczywiscie, ze jest sens. .NET jest coraz popularniejszy, wraz z jezykiem C#. Delphi natomiast juz nie nalezy do Borland, a sam jezyk jest niemal na wymarciu :(

A jak ma się teraz rozwój .NET w stosunku do przewidywań z 2007 roku ?
Czy nadal jest sens przechodzić na to środowisko ?
Osobiście działam w TurboDelphi (czyli chyba BDS 2006) i nie korzystam z .NET (tak mi się wydaje), czy .NET nadal jest tak silnie promowane przez Borlanda i MS ?