Václav Kadlec 6.9.2005
V dnešním článku společně dokončíme vytváření ukázkové aplikace, na níž si demonstrujeme používání síťových (socketových) datových proudů – streamů. V milulém dílu seriálu jsme implementovali komunikační část aplikace, dnes se budeme zabývat zpracováním dat a vytvořením uživatelského rozhraní.
Vítejte u dalšího dílu našeho programátorského seriálu. Dnes bezprostředně navážeme na to, co jsme si řekli minule, kdy jsme se začali zabývat síťovými sockety a jejich souvislostmi s datovými proudy (streamy).
V minulém článku jsme společně začali vytvářet ukázkovou aplikaci, která uvedené problémy prakticky demonstruje. Připomeňme pouze účel a činnost aplikace:
Vzhledem k tomu, co od aplikace žádáme, a vzhledem k tomu, čím se v poslední době náš seriál zabývá, je patrné, že k realizaci aplikace jsme zvolili dva koncepty:
Pokud vám z nějakého důvodu unikl minulý díl, vřele doporučuji se k němu vrátit a nejdříve si jej přečíst. V dnešním článku totiž bezprostředně navážeme v místě, kde jsme před minule skončili, což je kdesi uprostřed procesu tvorby ukázkové aplikace. Pokud jste minule nezačali aplikaci vytvářet, nebudete dnes moci nikterak pokročit. Ještě jednou tedy prosím všechny čtenáře, aby neváhali a podívali se nejprve na minulý díl tohoto seriálu.
Minule jsme realizovali následující kroky ve vytváření naší ukázkové aplikace:
Máme za sebou tedy „komunikační“ část aplikace, tedy tu část, která se stará o získávání dat ze vzdáleného síťového (internetového) serveru. Jedná se také o tu část, která má co do činění s vlákny, se sockety a se streamy.
Druhá část aplikace, kterou vytvoříme dnes, bude mít za úkol data zpracovat a zobrazit. Budeme tedy muset vytvořit design formuláře. K tomu účelu bude nutné vložit na formulář několik komponent. Ještě předtím však nesmíme zapomenout změnit jméno (vlastnost Name) hlavního formuláře Form1 na MainForm (neboť toto jméno jsme používali ve zdrojovém kódu minule).
Poté vložte na formulář následující komponenty:

Úvodní vlastnosti a popisky komponent nastavíme jak je naším zvykem v obsluze události OnCreate hlavního formuláře (ještě jednou raději připomeňme, že hlavní formulář se dnes jmenuje MainForm, nikoliv tradičně Form1):
procedure TMainForm.FormCreate(Sender:
TObject);
begin
Caption := `Ukazka sitovych streamu`;
RadioButton1.Caption := `Muzska jmena`;
RadioButton1.Checked :=
True;
RadioButton2.Caption := `Zenska jmena`;
Button1.Caption := `Stahni data`;
Button2.Caption :=
`Hledej`;
Label1.Caption := ``; // nastavi se za behu
Label2.Caption := `Hledej: `;
Edit1.Text := `John`;
ListView1.Viewstyle := vsReport;
FNamesList := TStringList.Create; // pro pozdejsi vyuziti
end;
Pojďme dál. Ošetříme událost OnClick tlačítka Button1, které bude sloužit ke stažení dat ze serveru. V obsluze události vytvoříme objekt vlákna TSocketThread, které jsme definovali minule. Vlákno je spuštěno a začne svůj běh. V závislosti na nastavení tlačítek RadioButton dojde k nastavení vlastnosti Gender na mužské nebo ženské jméno.
procedure TMainForm.Button1Click(Sender:
TObject);
begin
Button1.Enabled := False;
// Vytvorime
vlakno
with TSocketThread.Create do
begin
if RadioButton1.Checked
then
Gender := `male`
else
Gender := `female`;
Resume;
end;
end;
Další věc, kterou musíme udělat, je zpracování dat poté, co jsou načtena ze serveru. Už víme, že vlákno poté, co načte všechna data, zavolá metodu ProcessData, která je zpracuje. Tuto metodu nyní musíme implementovat. Takže, nejprve přidejme hlavičku metody do definice třídy TMainForm. Použijeme sekci private, protože třída TSocketThread je definována v téže jednotce. Když už něco přidáváme do definice třídy, přidejme i atribut FNameList, který využijeme později:
private
{ Private
declarations }
FNamesList:
TStringList;
procedure ProcessData(Data:
string);
Nyní pojďme na samotné zpracování dat (metodu ProcessData). HTTP odezva nám vrátí hlavičku, která nás příliš nezajímá: začátek zajímavých dat bezprostředně následuje po dvou párech oddělovačů nové řádky (dvojice #10#13). Budeme tedy zpracovávat data až od této dvojice:
procedure TMainForm.ProcessData(Data:
string);
var
DataStart: Integer;
CurrentPos:
PChar;
Name: string;
ListItem: TListItem;
function ReadNextToken: string;
begin
Result := ``;
// Vse je
oddeleno bilymi znaky
// Nejdriv preskocie vsechny bile
znaky na zacatku
while (CurrentPos^ in [#10, #13, ` `])
do
Inc(CurrentPos);
// nacitame znaky, dokud nenajdeme bile
znaky
while not (CurrentPos^ in [` `, #0, #10, #13])
do
begin
Result :=
Result + CurrentPos^;
Inc(CurrentPos);
end;
end;
begin
// velmi jednoduchy parser
// nejprve preskocime http hlavicku (az ke dvema dvojicim CR LF)
DataStart := Pos(#13#10#13#10, Data);
if
DataStart = 0 then
raise Exception.Create(`HTTP odezva
neobsahuje pozadovana data`);
ListView1.Items.BeginUpdate;
try
ListView1.Items.Clear;
//
FNamesList je TStringList pouzity k uchovani seznamu jmen
FNamesList.Clear;
// zaciname za dvojicemi CR LF,
pouzijeme ukazatel
CurrentPos := PChar(@Data[DataStart +
4]);
while (CurrentPos <> nil) and (CurrentPos^
<> #0) do
begin
Name := ReadNextToken;
if Name <> ``
then
begin
// vytvorime
seznam
ListItem :=
ListView1.Items.Add;
ListItem.Caption := Name;
ListItem.SubItems.Add(ReadNextToken);
ListItem.SubItems.Add(ReadNextToken);
ListItem.SubItems.Add(ReadNextToken);
// pridame jmeno do seznamu - pozdeji bude snadne
vyhledavani
FNamesList.Add(Name);
// Budeme si
na polozku udrzovat ukazatel
FNamesList.Objects[FNamesList.Count - 1] :=
ListItem;
Label1.Caption :=
`Zpracovavam data...`;
Application.ProcessMessages;
end
else
Break;
end;
finally
ListView1.Items.EndUpdate;
end;
// setridime seznam
jmen
FNamesList.Sort;
Label1.Caption := `Hotovo`;
Button1.Enabled := True;
end;
V metodě OnCreate hlavního fomuláře jste si možná všimli vytvoření seznamu FListNames. Tento seznam, v němž uchováváme načtená jména, musíme při skončení práce zase uvolnit z paměti – ošetřeme tedy metodu OnDestroy hlavního formuláře:
procedure TMainForm.FormDestroy(Sender:
TObject);
begin
FNamesList.Free;
end;
Jsme skoro hotovi! Poslední věc, kterou musíme učinit, je naprogramování podpory vyhledávání jmen. Jinak řečeno, uživatel by měl mít možnost vyhledat jednoduše konkrétní jméno v seznamu. Ošetříme tedy událost OnClick tlačítka Button2. Obsluha bude velmi jednoduchá – využijeme s výhodou toho, že k uchování seznamu jsme zvolili StringList, který v sobě přímo integruje podporu vyhledávání:
procedure TMainForm.Button2Click(Sender:
TObject);
var
Index: Integer;
begin
Index :=
FNamesList.IndexOf(UpperCase(Edit1.Text));
if (Index >= 0)
then
begin
ListView1.Selected :=
TListItem(FNamesList.Objects[Index]);
ListView1.Selected.MakeVisible(False);
ListView1.SetFocus;
end
else
raise
Exception.Create(`Nasledujici jmeno nenalezeno: ` +
Edit1.Text);
end;
Toť vše, aplikace je hotova, nezbývá nežli ji vyzkoušet!

Závěrem si jako obvykle uvedeme kompletní zdrojový kód celého příkladu:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants,
Classes, Graphics, Controls, Forms,
Dialogs, ScktComp, ComCtrls,
StdCtrls;
type
TSocketThread = class(TThread)
private
FClientSocket:
TClientSocket;
FData: string;
FLastException: Exception;
FGender:
string;
procedure SetGender(const Value:
string);
protected
procedure Execute;
override;
procedure
HandleThreadException;
procedure ThreadDone;
public
constructor Create;
destructor Destroy; override;
property Gender: string read
FGender write SetGender;
end;
TMainForm = class(TForm)
RadioButton1: TRadioButton;
RadioButton2:
TRadioButton;
Button1: TButton;
Label1: TLabel;
Label2: TLabel;
Edit1: TEdit;
Button2: TButton;
ListView1: TListView;
procedure FormCreate(Sender:
TObject);
procedure Button1Click(Sender:
TObject);
procedure FormDestroy(Sender:
TObject);
procedure Button2Click(Sender:
TObject);
private
{ Private declarations
}
FNamesList: TStringList;
procedure
ProcessData(Data: string);
public
{ Public
declarations }
end;
var
MainForm: TMainForm;
implementation
constructor TSocketThread.Create;
begin
inherited Create(True); // Intially suspended
FreeOnTerminate :=
True;
FClientSocket := TClientSocket.Create(nil);
FClientSocket.Host := `www.census.gov`;
FClientSocket.Port := 80; //
HTTP port
FClientSocket.ClientType := ctBlocking; // Blocking, so it
is in a thread
end;
destructor TSocketThread.Destroy;
begin
FClientSocket.Free;
inherited;
end;
procedure TSocketThread.Execute;
var
Buffer: array[0..1024-1] of Char;
SockStream:
TWinSocketStream;
RequestString: string;
begin
try
FClientSocket.Active := True;
FData := ``;
FillChar(Buffer, SizeOf(Buffer),
#0);
// Create the socket stream with a 1
minute timeout
SockStream :=
TWinSocketStream.Create(FClientSocket.Socket, 60000);
try
RequestString := Format(`GET
/genealogy/names/dist.%s.first
HTTP/1.0`#13#10#13#10,
[FGender]);
// Send the HTTP
request
SockStream.Write(RequestString[1],
Length(RequestString));
// Read the response
in 1024 chunks
while (SockStream.Read(Buffer,
SizeOf(Buffer)) <> 0) do
begin
FData := FData +
Buffer;
FillChar(Buffer,
SizeOf(Buffer), #0);
// Check for
termination of the thread or closure of the
socket
if Terminated or not
FClientSocket.Active
then
Exit;
end;
finally
SockStream.Free;
if FClientSocket.Active
then
FClientSocket.Active :=
False;
end;
except
on E:
Exception do
begin
if
not(ExceptObject is EAbort) then
begin
FLastException :=
E;
Synchronize(HandleThreadException);
end;
end;
end;
// Tell the main form
that we are done
Synchronize(ThreadDone);
end;
procedure
TSocketThread.HandleThreadException;
begin
Application.ShowException(FLastException);
end;
procedure TSocketThread.SetGender(const Value:
string);
begin
if (Value = `male`) or (Value = `female`)
then
FGender := Value
else
raise Exception.CreateFmt(`Unknown gender (%s) to search
for`,
[Value]);
end;
procedure TSocketThread.ThreadDone;
begin
//
The VCL is not thread safe. Accessing MainForm from inside
// a thread
would be bad, unless if it is inside a procedure
// that was called
via Synchronize.
MainForm.ProcessData(FData);
end;
{$R *.dfm}
procedure TMainForm.FormCreate(Sender:
TObject);
begin
Caption := `Ukazka sitovych streamu`;
RadioButton1.Caption := `Muzska jmena`;
RadioButton1.Checked :=
True;
RadioButton2.Caption := `Zenska jmena`;
Button1.Caption := `Stahni data`;
Button2.Caption :=
`Hledej`;
Label1.Caption := ``; // nastavi se za behu
Label2.Caption := `Hledej: `;
Edit1.Text := `John`;
ListView1.Viewstyle := vsReport;
FNamesList := TStringList.Create; // pro pozdejsi vyuziti
end;
procedure TMainForm.Button1Click(Sender:
TObject);
begin
Button1.Enabled := False;
// Vytvorime
vlakno
with TSocketThread.Create do
begin
if RadioButton1.Checked
then
Gender := `male`
else
Gender := `female`;
Resume;
end;
end;
procedure TMainForm.ProcessData(Data:
string);
var
DataStart: Integer;
CurrentPos:
PChar;
Name: string;
ListItem: TListItem;
function ReadNextToken: string;
begin
Result := ``;
// Vse je
oddeleno bilymi znaky
// Nejdriv preskocie vsechny bile
znaky na zacatku
while (CurrentPos^ in [#10, #13, ` `])
do
Inc(CurrentPos);
// nacitame znaky, dokud nenajdeme bile
znaky
while not (CurrentPos^ in [` `, #0, #10, #13])
do
begin
Result :=
Result + CurrentPos^;
Inc(CurrentPos);
end;
end;
begin
// velmi jednoduchy parser
// nejprve preskocime http hlavicku (az ke dvema dvojicim CR LF)
DataStart := Pos(#13#10#13#10, Data);
if
DataStart = 0 then
raise Exception.Create(`HTTP odezva
neobsahuje pozadovana data`);
ListView1.Items.BeginUpdate;
try
ListView1.Items.Clear;
//
FNamesList je TStringList pouzity k uchovani seznamu jmen
FNamesList.Clear;
// zaciname za dvojicemi CR LF,
pouzijeme ukazatel
CurrentPos := PChar(@Data[DataStart +
4]);
while (CurrentPos <> nil) and (CurrentPos^
<> #0) do
begin
Name := ReadNextToken;
if Name <> ``
then
begin
// vytvorime
seznam
ListItem :=
ListView1.Items.Add;
ListItem.Caption := Name;
ListItem.SubItems.Add(ReadNextToken);
ListItem.SubItems.Add(ReadNextToken);
ListItem.SubItems.Add(ReadNextToken);
// pridame jmeno do seznamu - pozdeji bude snadne
vyhledavani
FNamesList.Add(Name);
// Budeme si
na polozku udrzovat ukazatel
FNamesList.Objects[FNamesList.Count - 1] :=
ListItem;
Label1.Caption :=
`Zpracovavam data...`;
Application.ProcessMessages;
end
else
Break;
end;
finally
ListView1.Items.EndUpdate;
end;
// setridime seznam
jmen
FNamesList.Sort;
Label1.Caption := `Hotovo`;
Button1.Enabled := True;
end;
procedure TMainForm.FormDestroy(Sender:
TObject);
begin
FNamesList.Free;
end;
procedure TMainForm.Button2Click(Sender:
TObject);
var
Index: Integer;
begin
Index :=
FNamesList.IndexOf(UpperCase(Edit1.Text));
if (Index >= 0)
then
begin
ListView1.Selected :=
TListItem(FNamesList.Objects[Index]);
ListView1.Selected.MakeVisible(False);
ListView1.SetFocus;
end
else
raise
Exception.Create(`Nasledujici jmeno nenalezeno: ` +
Edit1.Text);
end;
end.
Dnes jsme dokončili ukázkovou aplikaci, na níž jsme si prakticky
demonstrovali používání dalšího druhu streamů – socketových streamů. Věřím, že
jste se nenechali odradit větším rozsahem aplikace - její zajímavost
doufám tento drobný problém bohatě vynahradila.