Programowanie w języku Delphi » Artykuły

Internetowe zastosowania Delphi cz. 2

Internetowe zastosowania Delphi cz. 2


Artykuł powstał na podstawie rozdziału 30. książki Delphi 4. Vademecum profesjonalisty (tom II). Zachowano także oryginalne fragmenty kodu źródłowego przykładowej aplikacji. W drugiej części artykułu skupimy się przed wszystkim na preparacji stron HTML oraz generowaniu strony na podstawie informacji z bazy danych.

W poprzedniej części artykułu pod właściwość Content parametru Response podstawialiśmy konkretny łańcuch stanowiący ostateczna postać strony HTML. HTTP dopuszcza jednak bardziej elastyczną obsługę, polegającą na traktowaniu pewnych fragmentów treści strony nie jako ostatecznego materiału do wyświetlenia, lecz jako komend, których interpretacja dostarcza dopiero dodatkowych fragmentów kodu strony. Proces ten, zwany niekiedy preparacja strony (ja wolę tą nazwę, gdyż to "fajnie brzmi"), jest w Delphi uosabiany przez abstrakcyjną klasę TCustomContentProducer. Z tej to klasy wywodzą się 3 komponenty, reprezentujące 3 odmiany preparacji:
  • TPageProducer - realizuje zastępowanie konkretną treścią tzw. znaczników HTML (ang. HTML tags),
  • TDataSetTableProducer - dostarcza tabelaryczną listę rekordów pochodzących ze zbioru bazy danych,
  • TQueryTableProducer - tworzy tabelaryczną listę rekordów na podstawie wynikowego zbioru parametryzowanego zapytania SQL.

TPageProducer


Zastępowanie konkretną treścią wybranych znaczników w treści strony HTML przypomina trochę proces makrogeneracji, charakterystyczny dla asemblera i C, choć nie jest aż tak zaawansowane. Znacznik podlegający zastępowaniu przypomina standardowy znacznik HTML, różni się jednak od niego początkowym znakiem #. Oto ogólna postać tego znacznika:
<#łańcuch parametry>


Łańcuch jest nazwą znacznika, parametrami zajmiemy się za chwilę. W charakterze nazwy znacznika może wystąpić dowolny poprawny identyfikator w sensie składni Pascala. Delphi wyróżnia kilka predefiniowanych nazw, klasyfikując na ich podstawie rodzaj znacznika. Nie ma ona jednak praktycznie żadnego znaczenia, oprócz niewątpliwej wygody dla użytkownika. Wybiegając niego naprzód: procedura zdarzeniowa, dokonująca zastąpienia znacznika konkretną treścią, otrzymuje jego typ, nazwę i parametry, przy czym informacja o typie stanowi informację redundantną (większą, niż potrzeba lub po prostu zbędną - bo trzeba nazwać rzeczy po imieniu), gdyż wynika ona jednoznacznie z nazwy znacznika. Typ znacznika reprezentowany jest przez następujący typ Object Pascala:

Znacznik

Typ pascalowy

Przykładowe zastosowanie

#LINKtgLinkŁącznik do hypertekstu lub zakładka (link) (<A>...</A>)
#IMAGEtgImageŁącznik do obrazka (<IMG SRC=...>...</IMG>)
#TABLEtgTableTabela HTML (<TABLE>...</TABLE>)
#IMAGEMAPtgImageMapŁącznik do wyróżnionych fragmentów grafiki (<MAP>...</MAP>)
#OBJECTtgObjectKod aktywujący kontrolkę ActiveX
#EMBEDtgEmbedZnacznik biblioteki DLL stanowiącej add-in dla Netscape


Każdy inny znacznik klasyfikowany jest przez Delphi jako tgCustom.

Parametry znacznika mają postać oddzielonych ciągiem spacji sekwencji:

nazwaparametru=treśćparametru


przy czym jeśli taka sekwencja posiada spacje, musi być ujęta w cudzysłów. Oto przykład parametryzowanego znacznika:

<#LINK Param1=katalog "Param2=/ksiazki/w przygotowaniu">


Zastępowanie znacznika treścią odbywa się w ramach zdarzenia OnHTMLTag komponentu TPageProducer - oto przykładowa procedura zdarzeniowa:

procedure TWebModule1.PageProducer1HTMLTag(Sender: TObject;
  Tag: TTag; const TagString: string; TagParams: TStrings;
  var ReplaceText: string);
begin
  case Tag of
    tgCustom:
      if TagString = 'NAME' then
        ReplaceText := 'Partner';
  end;
end;


Powoduje ona zamianę każdego znacznika <#NAME...> na "Partner"; testowanie wartości Tag, zawierającej typ znacznika, jest tu właściwie zbędne - znacznik o postaci <#NAME...> zawsze jest bowiem typu tgCustom. Parametr TagString zawiera nazwę znacznika, a parametr TagParams jest kolekcją, której elementami są kolejne sekwencje parametryczne znacznika. Pod parametr ReplaceText podstawić należy tekst, którym znacznik ma zostać zastąpiony.

Przekazanie komponentowi TPageProducer treści zawierającej znaczniki może odbywać się na dwa sposoby:
  • jeśli treść ta ma postać łańcucha znaków, łańcuch ten należy przypisać do właściwości HTMLDoc,
  • jeśli treść ta jest zawartością pliku tekstowego, nazwę tego pliku należy przypisać do właściwości HTMLFile.

Wygenerowanie tekstu z zastąpionymi treścią znacznikami odbywa się w wyniku wywołania metody Content, której wynikiem jest ostateczna postać tekstu. Każdy napotkany znacznik będzie generował zdarzenie OnHTMLTag. Oto przykład najbardziej typowej współpracy komponentów TWebModule i TPageProducer:

procedure TWebModule1.WebModule1ActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse;
  var Handled: Boolean);
begin
  PageProducer1.HTMLFile := 'Strona.html';
  Response.Content := PageProducer1.Content;
end;


Oczywiście, nic nie wyklucza sytuacji, w której kilka komponentów TPageProducer połączonych jest kaskadowo (ang. daisy-chain). Każdy z nich odpowiedzialny jest za zastępowanie znaczników określonej postaci, zaś tekst wyjściowy każdego z nich (z wyjątkiem ostatniego) jest wejściowym dla następnego. Oczywiście, nic nie może obyć się bez przykładu. Tak będzie i tym razem:

procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse;
  var Handled: Boolean);
begin
  PageProducer1.HTMLFile := 'Strona.html';
  PageProducer2.HTMLDoc := PageProducer1.Content;
  PageProducer3.HTMLDoc := PageProducer2.Content;
  Reponse.Content := PageProducer3.Content;
end;

TDataSetTableProducer i TQueryTableProducer


Komponenty te umożliwiają włączenie do stron HTML raportów tabelarycznych zawierających rekordy pochodzące ze zbiorów danych lub stanowiących wynik zapytania SQL. Postać tych raportów jest w pełni konfigurowalna - możliwe jest oddzielne określenie wyglądu każdej kolumny, wiersza lub komórki, co daje funkcjonalność zbliżoną do komponentu TDBGrid.

Komponenty TDataSetTableProducer i TQueryTableProducer różnią się od innych kontrolek bazodanowych dość istotnym szczegółem - nie czerpią one mianowicie swej zawartości z komponentów TDataSource, ale bezpośrednio z komponentów reprezentujących zbiór danych, wskazywanych przez swe właściwości - odpowiednio - TDataSetTableProducer.DataSet i TQueryTableProducer.Query (to jedyna różnica pomiędzy nimi; wszystkie inne właściwości i aspekty ich zachowania są identyczne dla obydwu).

Za ogólną postać generowanego raportu tabelarycznego odpowiedzialna jest właściwość TableAttributes. Właściwości Header i Footer określają postać - odpowiednio - nagłówka i stopki. Właściwość RowAttributes określa sposób wyświetlania wierszy tabeli.

Zawartość generowanego raportu jest w głównej mierze zależna od właściwości Columns. Jest ona obsługiwana przez specjalizowany edytor, umożliwiający zarządzanie zestawem i kolejnością kolumn w tabeli oraz określenie właściwości każdej kolumny z osobna (np. odpowiadającego jej pola zbioru danych, tytułu, koloru tła, wyrównania treści w poziomie i pionie itp.). Edytor ten uruchamiamy klikając dwukrotnie w komponent.

Właściwości Caption i CaptionAlign określają tytuł tabeli i jego wyrównanie.

Jako że zasady korzystania z obu komponentów są identyczne (za wyjątkiem wspomnianej wcześniej drobnej różnicy), przykład zaprezentuje drugi z nich - TQueryTableProducer. Przykład również pochodzi z rozdziału 30. książki Delphi 4. Vademecum profesjonalisty (tom II).

SELECT CUSTNO, ORDERNO, COMPANY, AMOUNTPAID, ITEMSTOTAL FROM CUSTOMER, ORDERS WHERE CUSTOMER.CUSTNO = ORDERS.CUSTNO AND ORDERS.AMOUNTPAID <> ORDERS.ITEMSTOTAL


To zapytanie definiuje tabelę zawierającą wykaz klientów, którzy zalegają z płatnością za zrealizowane zamówienia.

Ustaw właściwość SessionName komponentów TSession i TQuery na tę samą nazwę - np. Session1_2. Następnie przypisz właściwości Query komponentu TQueryTableProducer wskazanie na komponent TQuery. Poprzez ustawienie na True właściwości Active tego ostatniego dokonasz otwarcia skojarzonej z nim bazy.

UWAGA: Wszystkie aplikacje serwera korzystające z baz danych muszą posiadać na swym formularzu komponent TSession. Jest to związane z wymogami bezpiecznego dostępu do baz danych w warunkach pracy wielowątkowej. Zapewnienie odrębnej sesji sprowadza się do umieszczenia na formularzu komponentu TSession i skojarzenia go z odnośnym zbiorem danych - skojarzenie to polega na nadaniu identycznej nazwy właściwości SessionName obydwu komponentów. O resztę zadba Delphi. Z wątkowym charakterem żądań aplikacji wiąże się jeszcze jeszcze jedno zagadnienie - mianowicie optymalizacja wątkami poszczególnych żądań, nazywana w oryginale connection caching. Polega to w skrócie na tym, iż po zakończeniu wątku realizującego określone żądanie system nie likwiduje jego obiektu, lecz pozostawia go do ponownego wykorzystania. W ten sposób unika się kosztownych nieraz czynności inicjalizacyjnych. Zachowanie takie jest skutkiem ustawienia na True właściwości CacheConnections obiektu Application (klasy TWebApplication). Powtarzalne wykorzystywanie wątków niesie jednak ze sobą jeden bardzo istotny efekt uboczny. Otóż - uruchomieniom i kończeniom wątków nie towarzyszy każdorazowe tworzenie/zwalnianie (automatycznych) formularzy aplikacji (w tym modułu TWebModule), nie jest więc generowane zdarzenie OnCreate! Formularze te muszą mieć tzw. właściwość powtarzalności (ang. reusability) i nie mogą polegać na czynnościach inicjalizacyjnych wykonywanych w ramach tego zdarzenia. Jeśli takiej zależności nie da się uniknąć, należy koniecznie wyłączyć opisywaną optymalizację poprzez ustawienie na False właściwości Application.CacheConnections. Użytkownik może kontrolować optymalizację wątków żądań za pomocą właściwości ActiveCount, InactiveCount i MaxConnections obiektu Application. Pierwsza z nich zawiera liczbę bieżąco aktywnych połączeń, druga - liczbę zakończonych wątków czekających "w zapasie", ostatnia ogranicza liczbę jednocześnie aktywnych połączeń. Próba przekroczenia zakresu określanego przez tą właściwość spowoduje wystąpienie wyjątku.


Teraz ustaw tytuł raportu stosownie do swych upodobań - np. "Niesolidni klienci".

Kliknij dwukrotnie w komponent TQueryTableProducer, co spowoduje uruchomienie edytora kolumn. W jego sekcji Table Properties znajdują się pola określające ogólne właściwości tabeli wynikowej. Obok, w prawej części znajduje się lista kolejnych kolumn tabeli wynikowej. Ich dodawanie, usuwanie i zmianę kolejności umożliwiają kontrolki na pasku narzędziowym edytora.

Ustaw na 1 właściwość Border (szerokość obrzeża tabeli), na 2 właściwość CellPadding (odległość zawartości komórki od jej obrzeża) i na 1 właściwość CellSpacing (odległość pomiędzy komórkami). Jako domyślny kolor tła pozostaw standardową wartość Aqua (indywidualne ustawienie koloru wiersza ma wyższy priorytet, najwyższy priorytet posiada ustawienie indywidualnego koloru kolumny).

Prawa część edytora (zawierająca listę kolumn) jest jeszcze pusta. Kliknij pierwszą kontrolkę z lewej strony (Add), co spowoduje dodanie kolumny (jako obiektu klasy THTMLTableColumn). Jej podświetlenie spowoduje wyświetlenie (oraz możliwość edycji) jej właściwości w Inspektorze Obiektów - przypisz jej pole CUSTNO (właściwość FieldName). W analogiczny sposób dodaj jeszcze 3 kolumny i przypisz im pola OrderNo, Company i AmountOwned.

Domyślny tytuł kolumny to nazwa związanego z nią pola. Można to zmienić za pomocą rozbudowanej właściwości Title określajacej - oprócz tekstu tytułu - jego poziome i pionowe wyrównanie oraz kolor.

Klasa THTMLTableColumn - jak zresztą niektóre inne klasy związane z tabelą - posiada właściwość Custom. Umożliwia ona zdefiniowanie dodatkowych informacji tekstowych związanych z danym obiektem (komórką, wierszem, kolumną) interpretowanych przez niektóre przeglądarki - zazwyczaj maja one postać nazwa=wartość. Można określić kilka takich sekwencji, rozdzielając je spacjami.


Teraz zajmiemy się zdarzeniami komponentu TQueryTableProducer. Zdarzenie OnCreateContent generowane jest bezpośrednio przed rozpoczęciem tworzenia wynikowej strony HTML. Można przy tej okazji wykonać pewne czynności inicjalizacyjne. Zdarzenie to daje także szansę do powstrzymania generacji - decyduje o tym jeden z parametrów (Continue). Nadanie mu wartości False spowoduje, że wygenerowany zostanie pusty łańcuch. W naszym przykładzie zdarzenie to powoduje ustawienie właściwości MaxRows na pełny rozmiar wynikowego zbioru zapytania i pozycjonowanie tego ostatniego na pierwszym rekordzie.

procedure TWebModule1.QueryTableProducer1CreateContent(Sender: TObject;
  var Continue: Boolean);
begin
  QueryTableProducer1.MaxRows := Query1.RecordCount;
  Query1.First;
  Continue := True;
end;


Zdarzenie OnGetTableCaption umożliwia szczegółowe określenie postaci tytułu tabeli - w postaci języka HTML. Decydują o tym wynikowe parametry procedury zdarzeniowej.

procedure TWebModule1.QueryTableProducer1GetTableCaption(Sender: TObject;
  var Caption: string; var Alignment: THTMLCaptionAlignment);
begin
  Caption := '<B><FONT SIZE="+2" COLOR="RED">' +
    'Zadłużenie kontrahntów</FONT></B>';
  Alignment := caTop;
end;


Za ostateczną postać poszczególnych komórek odpowiedzialne jest zdarzenie OnFormatCell:

THTMLFormatCellEvent = procedure(Sender: TObject;
  CellRow, CellColumn: Integer; var BgColor: THTMLBgColor;
  var Align: THTMLAlign; var VAlign: THTMLVAlign;
  var CustomAttrs, CellData: string) of object;


Parametry CellRow i CellColumn zawierają (odpowiednio) numer wiersza i kolumny. Wierszem zerowym jest wiersz nagłówka, za zerową kolumnę uważa się pierwszą kolumnę tabeli. Pozostałe parametry umożliwiają określenie wyglądu komórki w tabeli - jej tło, wyrównania, atrybuty i treść. Zawartość komórki (CellData) jest typu znakowego, więc wielkości innego typu muszą zostać skonwertowane do postaci znakowej. Ich początkowe wartości zawierają domyślne ustawienia dla komórki. Oto postać tej procedury w naszym przykładzie:

procedure(Sender: TObject;
  CellRow, CellColumn: Integer; var BgColor: THTMLBgColor;
  var Align: THTMLAlign; var VAlign: THTMLVAlign;
  var CustomAttrs, CellData: string);
var
  Owed, Paid, Total: Currency;
begin
  if CellRow = 0 then
    Exit; // nie przetwarzaj nagłówka
  if CellColumn = 3 then // kolumna "Zadłużenie"
  begin
    // oblicz wartość zadłużenia
    Paid := Query1.FieldByName('AmountPaid').AsCurrency;
    Total := Query1.FieldByName('ItemsTotal').AsCurrency;
    Owed := Total - Paid;
    // sformatuj do postaci znakowej
    CellData := FormatFloat('$0.00', Owed);
    // jeśli należność jest większa od zera,
    // wyróżnij komórkę kolorem czerwonym
    if Owed > 0 then
      BgColor := 'RED';
    Query1.Next; // przejdź do następnego wiersza
  end;
end;


Musimy jeszcze tylko ustalić postać nagłówka (właściwość Header):

<HTML> <HEAD> <TITLE>Niesolidni kontrahenci</HTML> </HTML>
<BODY> <CENTER><H2>Wędki HTML i harpuny ActiveX</H2></CENTER> <P>Kolor czerwony oznacza zaległości płatnicze: <P>


oraz stopki (właściwość Footer):

<P>
<I>Powyższe informacje są ściśle tajne - harpuny są jednak bardzo niebezpieczne</I><P> <B><I>Copyright 1997 by BigShotWidgets</I></B><P> </BODY> </HTML>


Teraz wystarczy oprogramować jedyną akcję formularza, identyfikowaną jako /TestTable:

procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse;
  var Handled: Boolean);
begin
  Reponse.Content := QueryTableProducer1.Content;
end;


Po skompilowaniu aplikacji i umieszczeniu wynikowej biblioteki DLL w odpowiednim katalogu serwera, wpisanie w adresie przeglądarki internetowej adresu URL:

http://<adres>/tableex.dll/TestTable


spowoduje wyświetlenie raportu z informacjami o klientach i niezapłaconych przez nich zamówieniach.

To wszystko w drugiej części artykułu. Już niebawem ukaże się kolejna część. Ale na razie to wszystko.