Praktyczne programowanie, R 5c-04, Szablon dla tlumaczy


Wykorzystanie elementów Win32 API w Delphi. Część I

Przechodzimy obecnie do omówienia metod wykorzystywania interfejsu programisty w aplikacjach komunikacyjnych pisanych w Delphi. Zamierzeniem autora jest, by część książki traktująca o Object Pascalu nie była jedynie kalką tego, co zostało powiedziane wcześniej. Delphi ma swoje specyficzne właściwości, które postaramy się pokazać w kolejnych podrozdziałach.

Testowanie portu szeregowego — inaczej

Wszystko, co powiedzieliśmy do tej pory o strukturze kontroli portu szeregowego DCB, ogólnie rzecz biorąc będzie dalej aktualne. Być może niektóre z osób czytających rozdział poświęcony testowaniu łącza szeregowego za pomocą C++Buildera zastanawiały się, dlaczego przy okazji prezentacji struktury DCB w sposób bardzo wyraźny rozgraniczyliśmy opis zmiennych i pól bitowych wchodzących w jej skład. Mogłoby się wydawać, iż z poziomu Buildera był to zabieg dosyć sztuczny, niemniej jednak obecnie bardzo nam się przyda. Ktoś może być przekonany, że API jest czymś absolutnym (niezmiennym). Ogólnie jest to prawdą, poza małymi wyjątkami. Pierwszym tego typu przykładem może być właśnie sposób posługiwania się blokiem kontroli portu szeregowego w Delphi. Można np. skonfigurować wybrany port, odwołując się do zmiennych struktury DCB (w Object Pascalu deklaracja ta przybiera postać TDCB) w sposób analogiczny jak w Builderze i bez problemu osiągniemy ten cel. Trudności napotkamy jednak przy próbie skorzystania z jej pól bitowych. Powodem tego jest fakt, że Object Pascal korzysta ze specjalnego fragmentu Win32 API, gdzie DCB (TDCB) definiuje się następująco:

type

{$EXTERNALSYM _DCB}

_DCB = packed record

DCBlength: DWORD;

BaudRate: DWORD;

Flags: LongInt;

wReserved: Word;

XonLim: Word;

XoffLim: Word;

ByteSize: Byte;

Parity: Byte;

StopBits: Byte;

XonChar: CHAR;

XoffChar: CHAR;

ErrorChar: CHAR;

EofChar: CHAR;

EvtChar: CHAR;

wReserved1: Word;

end;

TDCB = _DCB;

{$EXTERNALSYM DCB}

DCB = _DCB;

PDCB = ^TDCB;

Użycie dyrektywy $EXTERNALSYM zapobiega jedynie pojawianiu się specyficznych symboli używanych przez Object Pascal w plikach nagłówkowych generowanych dla C++Buildera.

Patrząc na treść powyższej definicji, można się domyśleć, że dostęp do zmiennych DCB z poziomu Delphi nie powinien stwarzać większego problemu. Inaczej jest ze znacznikami — tutaj niestety należałoby już znać ich wartości. Oto one:

fBinary = $0001;

fParity = $0002;

fOutxCtsFlow = $0004;

fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

fDsrSensitivity = $0040;

fTXContinueOnXoff = $0080;

fOutX = $0100;

fInX = $0200;

fErrorChar = $0400;

fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

fAbortOnError = $4000;

Do tych znaczników odwołujemy się właśnie poprzez pole Flags, która to nazwa w tym kontekście jest zastrzeżona. Jeżeli chcielibyśmy w programie skonfigurować wybrany port szeregowy w ten sposób, aby sprawdzane były parzystość, sygnał CTS oraz np. by kontrola linii DTR była typu handshaking (patrz tabela 5.5), wystarczy napisać:

dcb.Flags := dcb_fParity or DTR_CONTROL_HANDSHAKE or dcb_fOutxCtsFlow;

lub, co jest równoważne:

dcb.Flags := $0002 or $0020 or $0004;

Widzimy więc, że do Flags należy po prostu wpisać konkretną kombinację bitów, tworząc tym samym odpowiednią maskę bitową. Jeżeli chcemy, by dany parametr komunikacyjny był nieaktywny, nie należy się do niego odwoływać.

Znacznik (ang. flag) oznacza stałą będącą szczególną opcją dla wybranej operacji. Może być on użyty pojedynczo lub z wykorzystaniem operatora OR jako kombinacji kilku znaczników — tworzy tym samym parametr w postaci --> unikalnej [Author:RG] maski bitowej.

Praktyczne wykorzystanie niektórych opisanych właściwości bloku kontroli portu szeregowego dostępnego w Windows z poziomu Delphi przedstawiono na wydruku 5.9 (dotyczy on modułu RS_11.pas projektu aplikacji \KODY\DELPHI\RS_11\p_RS_11.dpr, testującej wybrany port szeregowy). Formularz naszego projektu składa się ze standardowych komponentów. Wyboru numeru portu oraz prędkości transmisji dokonujemy, zaznaczając poszczególne komponenty typu TCheckBox. Naciskając przycisk Otwórz port wywołujemy procedurę obsługi zdarzenia OpenCommClick(). Przycisk Test uruchamia zdarzenie TestCommClick(). Zamknięcia portu i aplikacji dokonuje się po uaktywnieniu CloseCommClick().

Rysunek 5.13. Formularz główny projektu p_RS_11.dpr

0x01 graphic

Wydruk 5.9. Kod głównego modułu RS_11.pas aplikacji testującej port szeregowy

unit RS_11;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls;

type

TForm1 = class(TForm)

CloseComm: TButton;

CheckBox1: TCheckBox;

OpenComm: TButton;

Edit1: TEdit;

CheckBox2: TCheckBox;

CheckBox3: TCheckBox;

CheckBox4: TCheckBox;

TestComm: TButton;

Edit2: TEdit;

Edit3: TEdit;

Edit4: TEdit;

Edit5: TEdit;

Edit6: TEdit;

Label1: TLabel;

Label2: TLabel;

Label3: TLabel;

Label4: TLabel;

Label5: TLabel;

Label6: TLabel;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure TestCommClick(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

var

hCommDev : THANDLE; // identyfikator portu

lpFileName : PChar; // przechowuje nazwę portu

// lpFileName : LPCTSTR;

dcb : TDCB; // blok kontroli urządzeń

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName:='COM2';

if (CheckBox2.Checked = TRUE) then

lpFileName:='COM1';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE,0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

if (CheckBox4.Checked = TRUE) then

dcb.BaudRate := CBR_9600;

dcb.Parity := ODDPARITY;

dcb.StopBits := ONESTOPBIT;

dcb.ByteSize := 7;

// -- przykładowe ustawienia flag sterujących DCB --

dcb.Flags := dcb_fParity or $0020;

SetCommState(hCommDev, dcb);

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny ' ,

mtError, [mbOk], 0);

end;

end;

//--------------------------------------------------------------------

procedure TForm1.TestCommClick(Sender: TObject);

begin

// -- sprawdzenie zmiennych struktury DCB --

if (hCommDev > 0) then

begin

case dcb.BaudRate of

CBR_1200: Edit1.Text := IntToStr(CBR_1200);

CBR_9600: Edit1.Text := IntToStr(CBR_9600);

end;

case dcb.Parity of

EVENPARITY: Edit2.Text := ' Parzysta';

ODDPARITY: Edit2.Text := 'Nieparzysta';

end;

case dcb.XoffChar of

char(17): Edit3.Text := 'DC1';

char(19): Edit3.Text := 'DC3';

end;

// -- sprawdzenie znaczników sterujących struktury DCB --

if (dcb.Flags and DTR_CONTROL_ENABLE) = DTR_CONTROL_ENABLE

then

Edit4.Text := 'Aktywna'

else

Edit4.Text := 'Handshaking';

if (dcb.Flags and dcb_fOutxCtsFlow) = dcb_fOutxCtsFlow

then

Edit5.Text := 'Sprawdzany'

else

Edit5.Text := 'Nie sprawdzany';

if (dcb.Flags and dcb_fParity) = dcb_fParity

then

Edit6.Text := 'Sprawdzana'

else

Edit6.Text := 'Nie sprawdzana';

end // koniec nadrzędnego if

else

ShowMessage('Port szeregowy nie jest aktywny');

end;

//--------------------------------------------------------------------

end.

Zwróćmy uwagę, że wszystkie znaczniki sterujące DCB oraz ich wartości potraktowane zostały jako stałe. Ogólnie rzecz biorąc nie musimy tego robić w odniesieniu do stałych symbolicznych, czyli makr DTR_CONTROL_x lub RTS_CONTROL_x, gdyż są one zdefiniowane w części Win32 API dostępnej w Object Pascalu i bezpośrednie użycie ich w programie nie nastręcza żadnych problemów. W części deklaracyjnej programu umieściłem je jedynie po to, by zachować całość obrazu. Warto w tym miejscu wskazać też na możliwość pewnego odmiennego sposobu wywołania funkcji CreateFile(). W najprostszym wypadku prawidłowe wykorzystanie jej w programie napisanym w Delphi wymaga użycia w odpowiednim miejscu słowa zarezerwowanego NIL. Zmienna, która przechowuje nazwę portu musi być typu PChar lub właściwszego dla Windows typu LPCTSTR.

PRZYPOMNIJMY

Słowo zarezerwowane NIL (ang. Not In List — nie znajdujący się na liście) oznacza standardową stałą, która może być wartością każdego typu wskaźnikowego.

Standardowy typ PChar jest typem wskaźnikowym, wskazującym na C-łańcuchy.

Zdarzają się w praktyce sytuacje, w których musimy bardzo szybko zainicjować port szeregowy, nie wdając się w stosowanie jakiś bardzo skomplikowanych odwołań do DCB. Przedstawię teraz taki sposób. Być może nie jest on zbyt elegancki oraz podatny na szybkie modyfikacje i właściwszy jest raczej dla Basica, niemniej jednak można go z powodzeniem używać w prostszych programach, pisząc zarówno w Delphi, jak i C++Builderze. W tym celu posłużymy się funkcjami Win32 API, z których pierwsza jest postaci:

BOOL BuildCommDCB(LPCTSTR lpDef, LPDCB lpDCB);

Parametr lpDef jest wskaźnikiem do C-łańcucha, zawierającego kompletną informację o wybranych parametrach transmisji, lpDCB wskazuje na blok kontroli urządzeń. Przykładowo ustalimy: prędkość transmisji jako 9600 b/s, parzystość jako ODDPARITY, 8 bitów danych oraz 1 bit stopu. Wówczas należałoby funkcję tę wywołać w sposób prezentowany poniżej:

BuildCommDCB('baud = 9600 parity = O data = 8 stop = 1', dcb);

lub, co jest równoważne:

BuildCommDCB('9600, O, 8, 1', dcb);

Przy próbie ustalenia prędkości na 110 b/s funkcja automatycznie ustali dwa bity stopu. Dzięki temu zostanie zachowana kompatybilność w Windows NT. Ponadto niemożliwym jest ustalenie protokołu XON-XOFF (przyjmuje się, że jest on nieaktywny). Tym sposobem spowodujemy też wykonanie następujących przypisań:

fInX

FALSE

fOutX

FALSE

fOutxDsrFlow

FALSE

fOutxCtsFlow

FALSE

fDtrControl

DTR_CONTROL_ENABLE

fRtsControl

RTS_CONTROL_ENABLE

Jeżeli łańcuch lpDef uzupełnimy o znak p:

BuildCommDCB('9600, O, 8, 1, p', dcb);

to oznaczać będzie, że:

fInX

FALSE

fOutX

FALSE

fOutxDsrFlow

TRUE

fOutxCtsFlow

TRUE

fDtrControl

DTR_CONTROL_HANDSHAKE

fRtsControl

RTS_CONTROL_HANDSHAKE

Jeżeli z kolei łańcuch lpDef skonstruujemy według przepisu:

BuildCommDCB('9600, O, 8, 1, x', dcb);

uzupełniając go o znak x, wówczas należy spodziewać się następujących przypisań:

fInX

TRUE

fOutX

TRUE

fOutxDsrFlow

FALSE

fOutxCtsFlow

FALSE

fDtrControl

DTR_CONTROL_ENABLE

fRtsControl

RTS_CONTROL_ENABLE

Użycie omówionej funkcji nie powinno sprawić nam żadnego kłopotu. Procedura obsługi zdarzenia otwierającego wybrany port szeregowy do transmisji w jednym z możliwych wariantów może przybrać następującą postać:

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName:='COM2';

if (CheckBox2.Checked = TRUE) then

lpFileName:='COM1';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

// wywołanie SetupComm()

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

// dcb.Flags

BuildCommDCB('9600, O, 8, 1, p', dcb);

SetCommState(hCommDev, dcb)

// wywołanie GetCommMask()

// wywołanie SetCommMask()

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny' ,

mtError, [mbOk], 0);

end;

end;

Kolejna funkcja Win 32 API, za pomocą której można nie tylko błyskawicznie skonfigurować łącze szeregowe, ale również ustalić parametry czasów przeterminowania operacji zapisu i odczytu, ma następującą postać:

BOOL BuildCommDCBAndTimeouts(LPCTSTR lpDef, LPDCB lpDCB,

LPCOMMTIMEOUTS lpCommTimeouts);

gdzie lpCommTimeouts jest wskaźnikiem do omawianej w poprzednim podrozdziale struktury COMMTIMEOUTS ( w Object Pascalu odpowiednikiem jej będzie strukturalny typ danych TCOMMTIMEOUTS). Jeżeli ciąg znaków wskazywany przez lpDCB uzupełnimy o podciąg TO=ON, oznaczać to będzie, że omawiana funkcja uaktywni wszystkie parametry czasowe transmisji wyspecyfikowane zgodnie z TCOMMTIMEOUTS. Jeżeli z kolei lpDCB uzupełnimy go o podciąg TO=OFF, to parametry czasowe transmisji pozostaną nieaktywne. Poniżej prezentowany jest jeden z możliwych sposobów wywołania funkcji BuildCommDCBAndTimeouts():

CommTimeouts : TCOMMTIMEOUTS;

...

BuildCommDCBAndTimeouts('baud=1200 parity=O data=7 stop=1 TO=ON', dcb,

CommTimeouts);

Zajmijmy się teraz nieco dokładniejszym sposobem zdiagnozowania zasobów komunikacyjnych dostępnych w naszym PC. Sięgnijmy więc do dobrze nam już znanej struktury COMMPROP w części Win32 API, z którego korzysta Delphi. Definiowana jest ona jako typ TCOMMPROP. Poniżej zostały przedstawione wartości wszystkich dostępnych w TCOMMPROP stałych oferowanych przez Borland Delphi Run-Time Library Win32 API Interface Unit:

const

{ Serial provider type. }

SP_SERIALCOMM = $00000001;

{ Provider SubTypes }

PST_UNSPECIFIED = $00000000;

PST_RS232 = $00000001;

PST_PARALLELPORT = $00000002;

PST_RS422 = $00000003;

PST_RS423 = $00000004;

PST_RS449 = $00000005;

PST_MODEM = $00000006;

PST_FAX = $00000021;

PST_SCANNER = $00000022;

PST_NETWORK_BRIDGE = $00000100;

PST_LAT = $00000101;

PST_TCPIP_TELNET = $00000102;

PST_X25 = $00000103;

{ Provider capabilities flags }

PCF_DTRDSR = $0001;

PCF_RTSCTS = $0002;

PCF_RLSD = $0004;

PCF_PARITY_CHECK = $0008;

PCF_XONXOFF = $0010;

PCF_SETXCHAR = $0020;

PCF_TOTALTIMEOUTS = $0040;

PCF_INTTIMEOUTS = $0080;

PCF_SPECIALCHARS = $0100;

PCF_16BITMODE = $0200;

{ Comm provider settable parameters }

SP_PARITY = $0001;

SP_BAUD = $0002;

SP_DATABITS = $0004;

SP_STOPBITS = $0008;

SP_HANDSHAKING = $0010;

SP_PARITY_CHECK = $0020;

SP_RLSD = $0040;

{ Settable baud rates in the provider }

BAUD_075 = $00000001;

BAUD_110 = $00000002;

BAUD_134_5 = $00000004;

BAUD_150 = $00000008;

BAUD_300 = $00000010;

BAUD_600 = $00000020;

BAUD_1200 = $00000040;

BAUD_1800 = $00000080;

BAUD_2400 = $00000100;

BAUD_4800 = $00000200;

BAUD_7200 = $00000400;

BAUD_9600 = $00000800;

BAUD_14400 = $00001000;

BAUD_19200 = $00002000;

BAUD_38400 = $00004000;

BAUD_56K = $00008000;

BAUD_128K = $00010000;

BAUD_115200 = $00020000;

BAUD_57600 = $00040000;

BAUD_USER = $10000000;

{ Settable Data Bits }

DATABITS_5 = $0001;

DATABITS_6 = $0002;

DATABITS_7 = $0004;

DATABITS_8 = $0008;

DATABITS_16 = $0010;

DATABITS_16X = $0020;

{ Settable Stop and Parity bits }

STOPBITS_10 = $0001;

STOPBITS_15 = $0002;

STOPBITS_20 = $0004;

PARITY_NONE = $0100;

PARITY_ODD = $0200;

PARITY_EVEN = $0400;

PARITY_MARK = $0800;

PARITY_SPACE = $1000;

--> Object Pascal TCOMMPROP definiuje następująco[Author:RiAG] :

type

PCommProp = ^TCommProp;

{$EXTERNALSYM _COMMPROP}

_COMMPROP = record

wPacketLength: Word;

wPacketVersion: Word;

dwServiceMask: DWORD;

dwReserved1: DWORD;

dwMaxTxQueue: DWORD;

dwMaxRxQueue: DWORD;

dwMaxBaud: DWORD;

dwProvSubType: DWORD;

dwProvCapabilities: DWORD;

dwSettableParams: DWORD;

dwSettableBaud: DWORD;

wSettableData: Word;

wSettableStopParity: Word;

dwCurrentTxQueue: DWORD;

dwCurrentRxQueue: DWORD;

dwProvSpec1: DWORD;

dwProvSpec2: DWORD;

wcProvChar: array[0..0] of WCHAR;

end;

TCommProp = _COMMPROP;

{$EXTERNALSYM COMMPROP}

COMMPROP = _COMMPROP;

Nic nie stoi na przeszkodzie, abyśmy — posiadając już pewne doświadczenia w tym względzie — spróbowali samodzielnie, z poziomu Delphi, dokładniej zdiagnozować wybrany port szeregowy z poziomu Delphi. W tym celu zaprojektujemy aplikację, której projekt zamieszczony jest na dołączonym krążku CD \KODY\DELPHI\RS_12\p_RS_12.dpr. Jej wygląd nie będzie zasadniczo różnić się od odpowiednika napisanego w C++Builderze. Jako nowość wprowadzimy tylko możliwość wyboru interesującego nas łącza szeregowego. W skład przedstawionego na rysunku 5.14 formularza wchodzą podwójnie użyte komponenty TCheckBox, TButton, TEdit, TLabel oraz TTrackBar. Pełną informację o aktualnie dostępnym parametrze komunikacyjnym otrzymamy przez wzajemną, płynną regulację położeń suwaków uruchamiających procedury obsługi zdarzeń TrackBar1Change() oraz Track2BarChange(). Zastosowaliśmy tu omówiony już proces maskowania z wykorzystaniem operatora iloczynu bitowego and (bitowe i). Program odczytuje wartość wybranej zmiennej udostępnianej przez TCOMMPROP a następnie, wybierając kolejne maski, sprawdza, czy włączone są konkretne bity odpowiedzialne za wybrane atrybuty transmisji. Posiadając ściągawkę w postaci wyżej przedstawionych wartości reprezentujących typ dostępnego parametru danej usługi komunikacyjnej, Czytelnik bez problemu może tak rozbudować algorytm, by móc kompletnie zdiagnozować wybrane łącze komunikacyjne. Być może niektórym pewien kłopot sprawi zastosowany tu zapis liczb w postaci szesnastkowej (heksadecymalnej). Pamiętamy, że w Pascalu liczby takie poprzedzone są znakiem $. Jeżeli rzeczywiście tak jest, zawsze można użyć ustawionego w trybie profesjonalnym Kalkulatora Windows.

Rysunek 5.14. Formularz główny projektu p_RS_12.dpr

0x01 graphic

Wydruk 5.10. Kod głównego modułu RS_12.pas aplikacji testującej zasoby wybranego łącza komunikacyjnego

unit RS_12;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls, ComCtrls;

type

TForm1 = class(TForm)

CloseComm: TButton;

OpenComm: TButton;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

Edit1: TEdit;

Edit2: TEdit;

Label1: TLabel;

Label2: TLabel;

TrackBar1: TTrackBar;

TrackBar2: TTrackBar;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure TrackBar1Change(Sender: TObject);

procedure TrackBar2Change(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

var

hCommDev : THANDLE;

lpFileName : PChar;

//lpFileName : LPCTSTR;

dcb : TDCB;

CommProp : TCOMMPROP; // właściwości portu

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName:='COM2';

if (CheckBox2.Checked = TRUE) then

lpFileName:='COM1';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

CommProp.dwProvSpec1:=COMMPROP_INITIALIZED;

GetCommProperties(hCommDev, CommProp);

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny' ,

mtError, [mbOk], 0);

end;

end;

//--------------------------------------------------------------------

procedure TForm1.TrackBar1Change(Sender: TObject);

begin

case (TrackBar1.Position) of

1:

begin

TrackBar2.Max := 7;

Label1.Caption := 'dwSettableParams';

Edit1.Text := IntToStr(CommProp.dwSettableParams);

end;

2:

begin

TrackBar2.Max := 6;

Label1.Caption := 'wSettableData';

Edit1.Text := IntToStr(CommProp.wSettableData);

end;

3:

begin

TrackBar2.Max := 8;

Label1.Caption := 'wSettableStopParity';

Edit1.Text := IntToStr(CommProp.wSettableStopParity);

end;

end; // koniec case of

end;

//--------------------------------------------------------------------

procedure TForm1.TrackBar2Change(Sender: TObject);

begin

if (TrackBar1.Position = 1) then

begin

case (TrackBar2.Position) of

1:

begin

Label2.Caption := 'SP_PARITY';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_PARITY);

end;

2:

begin

Label2.Caption := 'SP_BAUD';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_BAUD);

end;

3:

begin

Label2.Caption := 'SP_DATABITS';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_DATABITS);

end;

4:

begin

Label2.Caption := 'SP_STOPBITS';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_STOPBITS);

end;

5:

begin

Label2.Caption := 'SP_HANDSHAKING';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_HANDSHAKING);

end;

6:

begin

Label2.Caption := 'SP_PARITY_CHECK';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_PARITY_CHECK);

end;

7:

begin

Label2.Caption := 'SP_RLSD';

Edit2.Text := IntToStr(CommProp.dwSettableParams

and SP_RLSD);

end;

end; // koniec case of

end; // koniec if

//*******************************

if (TrackBar1.Position = 2) then

begin

case (TrackBar2.Position) of

1:

begin

Label2.Caption := 'DATABITS_5';

Edit2.Text := IntToStr(CommProp.wSettableData

and DATABITS_5);

end;

2:

begin

Label2.Caption := 'DATABITS_6';

Edit2.Text := IntToStr(CommProp.wSettableData

and DATABITS_6);

end;

3:

begin

Label2.Caption := 'DATABITS_7';

Edit2.Text := IntToStr(CommProp.wSettableData

and DATABITS_7);

end;

4:

begin

Label2.Caption := 'DATABITS_8';

Edit2.Text := IntToStr(CommProp.wSettableData

and DATABITS_8);

end;

5:

begin

Label2.Caption := 'DATABITS_16';

Edit2.Text := IntToStr(CommProp.wSettableData

and DATABITS_16);

end;

6:

begin

Label2.Caption := 'DATABITS_16X';

Edit2.Text := IntToStr(CommProp.wSettableData

and DATABITS_16X);

end;

end; // koniec case of

end;// koniec if

//********************************

if (TrackBar1.Position = 3) then

begin

case (TrackBar2.Position) of

1:

begin

Label2.Caption := 'STOPBITS_10';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and STOPBITS_10);

end;

2:

begin

Label2.Caption := 'STOPBITS_15';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and STOPBITS_15);

end;

3:

begin

Label2.Caption := 'STOPBITS_20';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and STOPBITS_20);

end;

4:

begin

Label2.Caption := 'PARITY_NONE';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and PARITY_NONE);

end;

5:

begin

Label2.Caption := 'PARITY_ODD';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and PARITY_ODD);

end;

6:

begin

Label2.Caption := 'PARITY_EVEN';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and PARITY_EVEN);

end;

7:

begin

Label2.Caption := 'PARITY_MARK';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and PARITY_MARK);

end;

8:

begin

Label2.Caption := 'PARITY_SPACE';

Edit2.Text := IntToStr(CommProp.wSettableStopParity

and PARITY_SPACE);

end;

end; // koniec case of

end;// koniec if

end;

//--------------------------------------------------------------------

end.

Jak się zapewne domyślamy, dalsze manipulowanie ustawieniami parametrów transmisji użycia opisanych już wcześniej funkcji GetCommConfig() i SetCommConfig() oraz odwołania się do struktury COMMCONFIG, która w wersji akceptowanej przez Object Pascala przybiera postać rekordu TCOMMCONFIG:

PCommConfig = ^TCommConfig;

{$EXTERNALSYM _COMMCONFIG}

_COMMCONFIG = record

dwSize: DWORD;

wVersion: Word;

wReserved: Word;

dcb: TDCB;

dwProviderSubType: DWORD;

dwProviderOffset: DWORD;

dwProviderSize: DWORD;

wcProviderData: array[0..0] of WCHAR;

end;

TCommConfig = _COMMCONFIG;

{$EXTERNALSYM COMMCONFIG}

COMMCONFIG = _COMMCONFIG;

Nawiązanie połączenia

Analizując wszystko, co do tej pory zostało powiedziane, musieliśmy zauważyć, że konstrukcja części aplikacji, w której konfigurujemy port szeregowy oraz inicjujemy połączenie nie różni się zbyt drastycznie od jej odpowiednika napisanego w Builderze. Jedyną nowością jest trochę inny sposób odwoływania się do znaczników sterujących bloku kontroli portu szeregowego. Z tego powodu nie będę w tym miejscu szczegółowo omawiał budowy segmentu inicjalizująco-konfiguracyjnego. Przejdźmy od razu do przedstawienia konstrukcji części aplikacji wysyłającej i odbierającej komunikaty. W tym miejscu uwzględnimy możliwość ich budowy zarówno w postaci procedur jak i funkcji.

Segment wysyłający komunikaty

Zaczniemy od omówienia najprostszej z możliwych metod wysłania jakiegoś komunikatu poprzez łącze szeregowe z poziomu Delphi. W tym celu w sposób maksymalnie uproszczony skonstruujemy procedurę z zaledwie dwoma parametrami.

procedure TForm1.Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD);

var

NumberOfBytesWritten : DWORD;

begin

...

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

WaitCommEvent(hCommDev, fdwEvtMask, NIL);

...

end;

Ustaliliśmy tutaj, że identyfikator wybranego portu zostanie przekazany procedurze przez parametr globalny hCommDev, zaś liczba bajtów do wysłania przez parametr nNumberOfBytesToWrite, wołany przez wartość. Ten ostatni posłuży do rezerwacji miejsca przeznaczonego do zajęcia przez wartość przekazywanego mu parametru aktualnego. Nie trudno zgadnąć, że w przyszłości będzie nim liczba znaków przeznaczonych do wysłania. Używanie w procedurach zmiennych oraz stałych globalnych jest bardzo wygodną i często stosowaną techniką programowania. Przedstawiony sposób przekazywania danych procedurze w tej konkretnej sytuacji jest najprostszym z możliwych. Zobaczymy dalej, że jest również niezawodny. Trzeba pamiętać, że parametry wołane przez wartość służą jedynie do przekazywania wartości parametrów aktualnych procedurze. Wynika z tego, że zmiana w procedurze parametru wołanego przez wartość będzie widoczna tylko w obrębie danej procedury. Parametr nNumberOfBytesToWrite zawsze będzie miał przy wyjściu z procedury wartość taką samą jaką miał przy wejściu. Pisząc programy komunikacyjne musimy dążyć do tego, by nigdy nie zmieniać wartości liczby bajtów do wysłania! Użyliśmy procedury dwuparametrowej, gdyż z reguły interesują nas dwie rzeczy: co i gdzie mamy wysłać. Wnikanie w mniej istotne szczegóły w tym przypadku nie jest wymagane.

Alternatywnym sposobem zbudowania w Object Pascalu segmentu wysyłającego komunikaty do łącza szeregowego jest możliwość użycia funkcji. Wydaje się, że z praktycznego punktu widzenia wykorzystanie funkcji jest bardziej opłacalne. Wynika to z prostego faktu, mianowicie w odróżnieniu od procedur funkcje zawsze posiadają określony rezultat o typie jednoznacznie określonym przez typ wyniku funkcji. I w tym przypadku z powodzeniem wystarczy nam funkcja dwuparametrowa z liczbą bajtów do wysłania wołaną przez wartość:

function TForm1.Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD): Integer;

var

NumberOfBytesWritten : DWORD;

begin

...

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then

begin

...

Write_Comm := 1

end

else

Write_Comm := 0;

end;

Użycie funkcji umożliwi nam w przyszłości bardzo szybkie zdiagnozowanie rezultatu jej wykonania. Jeżeli w wyniku zaprogramowanego zdarzenia WaitCommEvent() otrzymamy wartość TRUE, wówczas rezultat wykonania całej funkcji będzie równy 1 (umówmy się, że wystąpienie jedynki traktować będziemy jako sukces). W przeciwny wypadku funkcja wysyłająca dane do portu szeregowego zwróci nam wartość 0.

Segment odbierający komunikaty

Projektując tę część aplikacji komunikacyjnej, podobnie jak poprzednio postaramy się wykorzystać wszystkie zalety Delphi. Jednak zanim zaczniemy pisać odpowiednie procedury i funkcje, przedstawię Czytelnikom pascalowy odpowiednik wielce użytecznej struktury COMSTAT. Będzie nim zdefiniowany w Borland Delphi Run-Time Library Win32 API Interface Unit strukturalny typ danych TCOMSTAT:

type

TComStateFlag = (fCtlHold, fDsrHold, fRlsHold, fXoffHold,

fXOffSent, fEof, fTxim);

TComStateFlags = set of TComStateFlag;

{$EXTERNALSYM _COMSTAT}

_COMSTAT = record

Flags: TComStateFlags;

Reserved: array[0..2] of Byte;

cbInQue: DWORD;

cbOutQue: DWORD;

end;

TComStat = _COMSTAT;

{$EXTERNALSYM COMSTAT}

COMSTAT = _COMSTAT;

PComStat = ^TComStat;

Użycie go w praktyce nie powinno nam sprawić jakichkolwiek problemów, gdyż odwołanie do jego poszczególnych elementów będzie analogiczne jak w rozdziale poświęconym Builderowi.

Tradycyjnie zaczniemy od przedstawienia bardzo prostej, dwuparametrowej procedury, dzięki której będziemy mogli odbierać komunikaty przychodzące do portu szeregowego. Patrząc na poniższy przykład, zauważymy, że Buf_Size uczyniliśmy parametrem wołanym przez wartość z takich samych powodów jak poprzednio, czyli po to, by procedura nie zmieniała jego wartości.

var Stat : TCOMSTAT;

...

procedure TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD);

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

end

else

Number_Bytes_Read := 0;

end;

Pewną ciekawostką jest również sposób wywołania funkcji ClearCommError():

ClearCommError(hCommDev, Errors, @Stat);

Korzystając ze środowiska Delphi w trakcie pisania naszych programów, nigdy do tej pory nie potrzebowaliśmy jawnie odwoływać się do adresów zmiennych, oferowanych przez poszczególne strukturalne typu danych. Nie musieliśmy tego robić dlatego, że były one dla nas w pewnym sensie „statyczne”. Jedynym celem odwołań do zawartości typów TDCB, TCOMMPROP czy TCOMMCONFIG było, jeżeli można tak powiedzieć, pewne sztywne ustalenie wybranych parametrów transmisji. Oznacza to, że w trakcie działania programu parametry te nie podlegają już fizycznie żadnym zmianom (przynajmniej jawnie). O takich parametrach w dużym uproszczeniu można powiedzieć, że są „statyczne”. Zupełnie inaczej sprawa będzie wyglądać w przypadku konieczności czytania stanu buforów komunikacyjnych w trakcie działania aplikacji. Naturą rzeczy zawartość buforów w trakcie transmisji musi się zmieniać, zarówno w sensie ich aktualnego stopnia wypełnienia jak i rodzaju znaków aktualnie tam przechowywanych. Naszym zadaniem jest odpowiednie uchwycenie momentu tej zmiany. Twórcy Win32 API bardzo dokładnie przewidzieli taką sytuację. Traktując parametr Stat operatorem adresowym @, bardzo szybko otrzymamy adres miejsca, pod którym zapamiętany jest aktualny stan poszczególnych elementów TCOMSTAT. Potem pozostaje nam już tylko umiejętne wydobycie przechowywanej tam informacji, ale o tym mówiliśmy wcześniej. Przypomnijmy też, że zamiast operatora adresu możemy z takim samym skutkiem użyć standardowej funkcji adresowej Addr():

ClearCommError(hCommDev, Errors, Addr(Stat));

Alternatywnym sposobem skonstruowania segmentu odbierającego przychodzące do portu komunikaty jest zaprojektowanie go w postaci dwuparametrowej funkcji:

function TForm1.Read_Comm(hCommDev: THANDLE;

Buf_Size: DWORD): Integer;

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

Read_Comm := 1;

end

else

begin

Number_Bytes_Read := 0;

Read_Comm := 0;

end;

end;

Przykładowe aplikacje

Przedstawię obecnie kompletne przykłady aplikacji napisanych w Delphi, wykorzystujących skonstruowane przez nas procedury oraz funkcje zapisu i odczytu danych z portu szeregowego. Na rysunku 5.15 pokazano wygląd zaprojektowanego przeze mnie formularza, którego projekty wykorzystujące odpowiednio procedury i funkcje znajdują się w katalogach \KODY\DELPHI\RS_13\p_RS_13.dpr oraz \KODY\DELPHI\RS_14\p_RS_14.dpr. Formularz ten składa się z dobrze nam już znanych komponentów TCheckBox, TRichEdit oraz TButton. Starając się postępować konsekwentnie, zachowałem obowiązujące w naszej książce nazewnictwo poszczególnych zdarzeń, więc Czytelnik nie powinien mieć żadnych trudności z ich rozszyfrowaniem. Korzystając z zaprezentowanych programów testowałem, podobnie jak poprzednio, transmisję z innym egzemplarzem kontrolera temperatury. Wykorzystałem standardowe zapytanie o jego identyfikację: *IDN?+#13+#10. Owo zapytanie zakończone parą znaków CR (#13) LF (#10) można potraktować jako ciąg znaków wskazywanych przez parametr query. Zgodnie z prezentowaną wcześniej metodą, łańcuch ten skopiowałem do obszaru pamięci identyfikowanego przez bufor danych wyjściowych Buffer_O. W tym celu została zastosowana standardowa funkcja StrCopy(), za pomocą której można skopiować pierwotny łańcuch znaków do łańcucha docelowego (w naszym przykładzie: Buffer_O). Funkcja ta w wyniku daje nam wskaźnik na początek łańcucha docelowego. Należy jednak zwrócić uwagę na to, żeby długość ciągu znaków wysyłanych nie przekraczała rozmiaru zadeklarowanego bufora danych wyjściowych. W momencie, kiedy nie będziemy w stanie przewidzieć długości łańcucha znaków przeznaczonych do wysłania, lepiej jest skorzystać z funkcji:

function StrLCopy(Dest: PChar; const Source: PChar; MaxLen: Cardinal):

PChar;

Kopiuje ona C-łańcuch Source do C-łańcucha Dest, zwracając wskaźnik na początek łańcucha Dest. Jednak w tym wypadku liczba kopiowanych znaków jest równa co najwyżej liczbie wyrażonej przez MaxLen, która może być określona za pomocą funkcji SizeOf().

Rysunek 5.15. Formularz główny projektów p_RS_13.dpr oraz p_RS_14.dpr

0x01 graphic

Wydruk 5.11. Kod głównego modułu RS_13.pas aplikacji korzystającej z procedur wysyłających i odbierających komunikaty z łącza szeregowego

unit RS_13;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ComCtrls;

type

TForm1 = class(TForm)

CloseComm: TButton;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

OpenComm: TButton;

Send: TButton;

Receive: TButton;

RichEdit1: TRichEdit;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure SendClick(Sender: TObject);

procedure ReceiveClick(Sender: TObject);

private

{ Private declarations }

procedure Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD);

procedure Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD);

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości flag sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 256;

cbOutQueue = 256;

var

query : PChar = '*IDN?'+#13+#10; // przykładowe zapytanie

Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

Number_Bytes_Read : DWORD;

hCommDev : THANDLE;

lpFileName : PChar;

fdwEvtMask : DWORD;

Stat : TCOMSTAT;

Errors : DWORD;

dcb : TDCB;

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD);

var

NumberOfBytesWritten : DWORD;

begin

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

WaitCommEvent(hCommDev, fdwEvtMask, NIL);

end;

//--------------------------------------------------------------------

procedure TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD);

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

end

else

Number_Bytes_Read := 0;

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM2';

hCommDev := CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE,0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox2.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

//-- przykładowe ustawienia znaczników sterujących DCB --

dcb.Flags := RTS_CONTROL_DISABLE or dcb_fParity;

dcb.Parity := ODDPARITY;

dcb.StopBits := ONESTOPBIT;

dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);

GetCommMask(hCommDev, fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny ',

mtError, [mbOk], 0);

end;

end;

//--------------------------------------------------------------------

procedure TForm1.SendClick(Sender: TObject);

begin

StrCopy(Buffer_O, query);

RichEdit1.Text := Buffer_O;

Write_Comm(hCommDev, StrLen(Buffer_O));

FlushFileBuffers(hCommDev);

end;

//--------------------------------------------------------------------

procedure TForm1.ReceiveClick(Sender: TObject);

begin

Read_Comm(hCommDev, SizeOf(Buffer_I));

RichEdit1.Text := Buffer_I;

end;

//--------------------------------------------------------------------

end.

W bardzo podobny sposób problem transmisji szeregowej realizowanej w Object Pascalu można rozwiązać, wykorzystując w tym celu odpowiednio skonstruowane funkcje, tak jak pokazuje to poniższy, kompletny wydruk.

Wydruk 5.12. Kod głównego modułu RS_14.pas aplikacji korzystającej z funkcji wysyłających i odbierających komunikaty z łącza szeregowego

unit RS_14;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics,

Controls, Forms, Dialogs, StdCtrls, ComCtrls;

type

TForm1 = class(TForm)

CloseComm: TButton;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

OpenComm: TButton;

Send: TButton;

Receive: TButton;

RichEdit1: TRichEdit;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure SendClick(Sender: TObject);

procedure ReceiveClick(Sender: TObject);

private

{ Private declarations }

function Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD): Integer;

function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 256;

cbOutQueue = 256;

var

query : PChar = '*IDN?'+#13+#10; // przykładowe zapytanie

Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

Number_Bytes_Read : DWORD;

hCommDev : THANDLE;

lpFileName : PChar;

fdwEvtMask : DWORD;

Stat : TCOMSTAT;

Errors : DWORD;

dcb : TDCB;

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

function TForm1.Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD): Integer;

var

NumberOfBytesWritten : DWORD;

begin

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then

Write_Comm := 1

else

Write_Comm := 0;

end;

//--------------------------------------------------------------------

function TForm1.Read_Comm(hCommDev: THANDLE;

Buf_Size: DWORD): Integer;

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

Read_Comm := 1;

end

else

begin

Number_Bytes_Read := 0;

Read_Comm := 0;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM2';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox2.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

//-przykładowe ustawienia znaczników sterujących DCB-

dcb.Flags := DTR_CONTROL_ENABLE or dcb_fParity;

dcb.Parity := ODDPARITY;

dcb.StopBits := ONESTOPBIT;

dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);

GetCommMask(hCommDev, fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny ',

mtError, [mbOk], 0);

end;

end;

//--------------------------------------------------------------------

procedure TForm1.SendClick(Sender: TObject);

begin

Repeat

StrCopy(Buffer_O, query);

RichEdit1.Text := Buffer_O;

FlushFileBuffers(hCommDev);

Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);

end;

//--------------------------------------------------------------------

procedure TForm1.ReceiveClick(Sender: TObject);

begin

if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then

begin

// Beep();

RichEdit1.Text := Buffer_I;

end

else

begin

RichEdit1.Text := 'Brak danych do odebrania';

Beep();

end;

end;

//--------------------------------------------------------------------

end.

Porównując przedstawione dwie techniki programowania, zgodzimy się chyba, że większe możliwości manewru będziemy mieli, wykorzystując funkcje. Możliwym było np. użycie w zdarzeniu wysyłającym dane instrukcji powtarzającej Repeat...Until, której koniec nastąpi w momencie wysłania ostatniego znaku z bufora wyjściowego. Również przy odczycie danych możemy bardzo prosto zdiagnozować, czy w ogóle jakikolwiek komunikat pojawił się w buforze wejściowym.

Być może słabym punktem przedstawionego rozumowania jest fakt, że w końcowym efekcie skonstruowane przez nas funkcje zapisu i odczytu wywołaliśmy w procedurach reprezentujących odpowiednie zdarzenia. Możliwe są oczywiście inne rozwiązania, które jednak nie mieszczą się w temacie tej pracy. Jeżeli jednak ktoś bardziej pomysłowy nieco inaczej podejdzie do tego zagadnienia, książka ta w stu procentach spełni swoje zadanie.

Podsumowanie

Głównym celem tego fragmentu książki było zaprezentowanie ogólnych zasad poruszania się w warstwie komunikacyjnej Borland Delphi Run-Time Library Win32 API Interface Unit. Zauważyliśmy, że korzystanie w Delphi z bloku kontroli portu szeregowego opiera się na nieco innych zasadach niż w C++Builderze. Jednak po zapoznaniu się z przedstawionymi tu przykładami nie powinno to stanowić już dla nas tajemnicy. Uważny Czytelnik mógł się też przekonać, że czasami warto rozróżniać elementy składowe struktur. Jeżeli rozróżnienia takiego dokonamy już na poziomie C++, w innych kompilatorach nie będziemy mieli żadnych kłopotów. Zostały również zaprezentowane różne możliwe sposoby konstrukcji programu komunikacyjnego właściwe dla Object Pascala. Nauczyliśmy się pisać zarówno odpowiednie funkcje, jak i procedury pomocne w transmisji ciągów znaków. Użycie tych elementów we właściwym miejscu aplikacji nie powinno sprawić nam już poważniejszych trudności.

Ćwiczenia

  1. Na podstawie projektu p_RS_12.dpr oraz jego kodu źródłowego RS_12.pas przetestuj i wyświetl wszystkie usługi komunikacyjne wraz z ich atrybutami, do których będziesz miał aktualnie dostęp.

  2. Zmodyfikuj kody RS_13.pas oraz RS_14.pas, tak aby można było odebrany komunikat zapisać na dysku.

Wykorzystanie elementów Win32 API w Delphi. Część II

Skoro posiadamy już pewne wiadomości na temat konfigurowania i diagnozowania łącza oraz wysyłania i odbierania komunikatów poprzez interfejs szeregowy z poziomu Delphi, pora abyśmy zapoznali się z metodami transmitowania pojedynczych znaków oraz plików jako całości. Mimo że wiadomości tu zaprezentowane nie będą zbytnio odbiegały od tego, co powiedzieliśmy w części poświęconej C++Builderowi, to jednak aplikacje przedstawione w tym podrozdziale postaram się w miarę możliwości uatrakcyjnić. Wszystkie omówione nowości z powodzeniem można wykorzystać w aplikacjach pisanych w Builderze.

Wysyłamy znak po znaku

Podstawowa funkcja Win32 API służąca temu celowi jest oczywiście dostępna z poziomu Object Pascala. Niemniej jednak postaram się zaprezentować pewne dodatkowe możliwości, jakie możemy osiągnąć, korzystając z TransmitCommChar(). Zaprojektujmy w tym celu aplikację, której formularz będzie nieco odbiegał od swojego odpowiednika stworzonego w C++Builderze. Postaramy się skonstruować prosty terminal, za pomocą którego będzie można wysyłać pojedyncze znaki już w trakcie ich wpisywania z klawiatury. Dla zachowania całości obrazu uwzględnimy też możliwość wysłania pliku, którego rozmiar będzie dla nas nieistotny. Rysunek 5.16 przedstawia wygląd naszego formularza, którego projekt został umieszczony w katalogu \KODY\DELPHI\RS_15\p_RS_15.dpr. Składa się on z czterech doskonale znanych nam komponentów, za pomocą których w wersji podstawowej wybieramy numer portu oraz prędkość transmisji. Przyciskiem Otwórz port wywołujemy procedurę obsługi zdarzenia OpenCommClick(), zaś za pomocą Zamknij uaktywniamy CloseCommClick(). Naciskając przycisk Transmisja pliku, będziemy mogli wywołać zdarzenie, w którym wybrany przez nas plik zostanie w sposób nie buforowany przetransmitowany do otwartego wcześniej portu szeregowego. W dolnej części formularza umieściłem bardzo użyteczny komponent TStatusBar. W jego obszarze będziemy wyświetlać komunikaty o tym, czy wybrany port został otwarty oraz o końcu transmisji pliku. Podzieliłem go na dwie części — dwukrotnie klikając w nim, w edytorze statusów (Editing Status) umieściłem dwa panele: 0 — Status Panel oraz 1 — Status Panel. Wyświetlanie tam odpowiednich komunikatów jest rzeczą prostą, wystarczy w wybranym miejscu kodu napisać:

StatusBar1.Panels[0].Text := ' komunikat ';

W centralnej części formularza, w komponencie TGroupBox umieściłem obiekt typu TEdit. Tutaj właśnie będziemy wpisywali z klawiatury nasze komunikaty. Aby mogły być one transmitowane w trakcie pisania, należy okno edycji uczynić zdolnym do generowania zdarzeń. Postępujemy następująco: raz klikamy (tylko zaznaczamy) komponent Edit1 w inspektorze obiektów Object Inspector i przechodzimy od razu do karty zdarzeń Events. Zdarzeniu OnKeyPress przypiszemy Edit1KeyPress i naciśniemy Enter. W ten sposób dostaniemy się do wnętrza odpowiedniej procedury, która będzie generować interesujące nas zdarzenia. Jej nagłówek powinien wyglądać następująco:

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);

Pozostaje nam już teraz wypełnienie jej odpowiednią treścią. Kompletny kod zastosowanego przeze mnie algorytmu przedstawiony jest na wydruku 5.13.

Rysunek 5.16. Formularz główny projektu p_RS_15.dpr

0x01 graphic

Wydruk 5.13. Kod głównego modułu RS_15.pas aplikacji realizującej transmisję nie buforowaną

unit RS_15;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ComCtrls, ExtCtrls;

type

TForm1 = class(TForm)

CheckBox1: TCheckBox;

OpenComm: TButton;

CheckBox2: TCheckBox;

CheckBox3: TCheckBox;

CheckBox4: TCheckBox;

SendFile: TButton;

CloseComm: TButton;

StatusBar1: TStatusBar;

GroupBox1: TGroupBox;

Edit1: TEdit;

Label1: TLabel;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure SendFileClick(Sender: TObject);

procedure Edit1KeyPress(Sender: TObject; var Key: Char);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

var

hCommDev : THANDLE; // identyfikator portu

lpFileName : PChar; // wskaźnik do nazwy portu

//lpFileName : LPCTSTR;

dcb : TDCB; // blok kontroli urządzeń

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM2';

if (CheckBox2.Checked = TRUE) then

lpFileName := 'COM1';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

if (CheckBox4.Checked = TRUE) then

dcb.BaudRate := CBR_19200;

dcb.Parity := ODDPARITY;

dcb.StopBits := ONESTOPBIT;

dcb.ByteSize := 7;

// -- przykładowe ustawienia znaczników sterujących DCB --

dcb.Flags := dcb_fParity or DTR_CONTROL_HANDSHAKE ;

// dcb.Flags := $0002 or $0020 ;

SetCommState(hCommDev, dcb);

StatusBar1.Panels[0].Text := 'Port '+lpFileName+ ' jest'+

' otwarty';

end

else

case hCommDev of

IE_BADID:

Application.MessageBox('Niewłaściwa nazwa portu lub jest'+

' on aktywny ', 'Uwaga !',MB_OK);

end;

end;

//---------transmisja pliku------------------------------------------

procedure TForm1.SendFileClick(Sender: TObject);

var

InFile : TextFile;

Fname : String;

chTransmit : char;

begin

if (hCommDev > 0) then

begin

Fname := 'ala.dat';

AssignFile(InFile, Fname);

try

Reset(InFile);

try

while not EOF(InFile) do

begin

read(InFile, chTransmit);

TransmitCommChar(hCommdev, chTransmit);

sleep(1);

end;

finally

StatusBar1.Panels[1].Text := 'Koniec transmisji pliku';

CloseFile(InFile);

end

except

on EInOutError do

ShowMessage('Błąd otwarcia pliku. Sprawdź, czy plik'+

' istnieje.');

end;

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//---------wysłanie znaków z klawiatury-------------------------------

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);

begin

if (hCommDev > 0) then

begin

if (Key = #13) then // jeżeli naciśniemy Enter

begin

TransmitCommChar(hCommDev, char(10)); // CR

TransmitCommChar(hCommDev, char(13)); // LF

end;

case Key of

#7 : TransmitCommChar(hCommDev, char(7)); // BEL

#8 : TransmitCommChar(hCommDev, char(8)); // BS

#32 : TransmitCommChar(hCommDev, char(32)); // spacja

#97 : TransmitCommChar(hCommDev, char(97)); // a

#98 : TransmitCommChar(hCommDev, char(98)); // b

#99 : TransmitCommChar(hCommDev, char(99));

#100: TransmitCommChar(hCommDev, char(100));

#101: TransmitCommChar(hCommDev, char(101));

#102: TransmitCommChar(hCommDev, char(102));

#103: TransmitCommChar(hCommDev, char(103));

#104: TransmitCommChar(hCommDev, char(104));

#105: TransmitCommChar(hCommDev, char(105));

#106: TransmitCommChar(hCommDev, char(106));

#107: TransmitCommChar(hCommDev, char(107));

#108: TransmitCommChar(hCommDev, char(108));

#109: TransmitCommChar(hCommDev, char(109));

#110: TransmitCommChar(hCommDev, char(110));

#111: TransmitCommChar(hCommDev, char(111));

#112: TransmitCommChar(hCommDev, char(112));

#113: TransmitCommChar(hCommDev, char(113));

#114: TransmitCommChar(hCommDev, char(114));

#115: TransmitCommChar(hCommDev, char(115));

#116: TransmitCommChar(hCommDev, char(116));

#117: TransmitCommChar(hCommDev, char(117));

#118: TransmitCommChar(hCommDev, char(118));

#119: TransmitCommChar(hCommDev, char(119));

#120: TransmitCommChar(hCommDev, char(120)); // x

#121: TransmitCommChar(hCommDev, char(121)); // y

#122: TransmitCommChar(hCommDev, char(122)); // z

end;

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//--------------------------------------------------------------------

end.

Projektując zapis procedury obsługi ostatniego zdarzenia, na samym jej początku (oczywiście po upewnieniu się, czy port jest otwarty do transmisji), przewidziałem możliwość wysyłania znaków sterujących CR LF po naciśnięciu Enter. Mogę ponadto „zadzwonić” do sąsiedniego komputera, naciskając Ctrl+G, czyli transmitując znak dzwonienia lub na sąsiednim monitorze przejść na początek pisanego tekstu — BS (patrz tabela 2.1). Uwzględniłem transmisję jedynie małych liter alfabetu łacińskiego oraz tylko niektórych dostępnych nam znaków sterujących, jednak każdy może znacznie rozszerzyć możliwości tej aplikacji.

Zauważmy ponadto, że w procedurze obsługi zdarzenia SendFileClick() użyliśmy konstrukcji try...finally...end. W pierwszej kolejności zostaną wykonane instrukcje zawarte pomiędzy try oraz finally. Następnie zostaną wykonane instrukcje pomiędzy klauzulami finally oraz end, niezależnie od rezultatu wykonania pierwszej grupy instrukcji. Zewnętrzny blok try...except...end pokazuje ideę obsługi wyjątków. Dzięki zastosowaniu takiej konstrukcji dokonujemy rozdziału miejsca, w którym może wystąpić wyjątek (w naszym przykładzie będzie to próba otwarcia np. nieistniejącego pliku) od miejsca, w którym będzie on obsługiwany. Opisany mechanizm nosi nazwę strukturalnej obsługi wyjątków SEH (ang. Structural Exception Handling) i stosowany bywa wszędzie tam, gdzie żądamy, by aplikacja dalej funkcjonowała „w miarę normalnie” po wystąpieniu jakiegoś błędu.

Wysyłamy pliki

Wykonywanie w Delphi różnego rodzaju operacji plikowych w celu przesłania określonego zbioru danych w ogólnych zarysach nie różni się zbytnio od analogicznych czynności, które wykonywaliśmy z poziomu Buildera. Niemniej jednak pomiędzy tymi dwoma kompilatorami istnieją pewne różnice co do sposobu wywoływania niektórych funkcji API. Jedyną metodą na to, aby poznać te rozbieżności, jest napisanie odpowiedniego programu. Zaprojektujemy formularz wykorzystujący standardowe okna dialogowe Windows. Rysunek 5.16 pokazuje, jak może on wyglądać. W jego skład wchodzą pojedyncze komponenty TDirectoryListBox, TFileListBox, TDriveComboBox oraz TEdit. Uzupełniony jest ponadto o obiekt edycji TRichEdit, wskaźnik postępu wykonywanych operacji TProgressBar oraz po cztery komponenty TCheckBox i TButton. Projektując tego typu aplikacje, w pierwszej kolejności wszystkie obiekty dialogowe należy ze sobą powiązać, tak aby widziały się nawzajem. Można to zrobić za pomocą inspektora obiektów, jednak dużo prościej i czytelniej będzie, jeżeli postąpimy tak, jak w przypadku projektu \KODY\DELPHI\RS_16\p_RS_16.dpr. Wszystkie wymagane relacje ustalimy w procedurze:

procedure TForm1.FormCreate(Sender: TObject);

begin

DirectoryListBox1.FileList := FileListBox1;

DriveComboBox1.DirList := DirectoryListBox1;

FileListBox1.FileEdit := Edit1;

ProgressBar1.Step := 1;

end;

Następnym krokiem jest zawsze przetestowanie wykonanych przypisań. Najlepiej, jeżeli w tym celu otworzymy, przeczytamy i wyświetlimy wybrany plik. Już tutaj natrafimy na nieco odmienny sposób wywołania dobrze nam znanych funkcji _lopen() oraz _lread(). Z poziomu Object Pascala wymagane jest aby, właściwość FileName obiektu TFileListBox odczytać za pomocą operacji rzutowania jako typ PChar. Należy postąpić tak dlatego, gdyż w rzeczywistości właściwość FileName jest typu String, zaś funkcja API _lopen() niechętnie operuje na takich zmiennych. Jednak anachroniczność ta, którą odziedziczyliśmy po klasycznym Pascalu, nie powinna być dla nas przeszkodą. Na szczęście korzystając z Delphi spotykamy ich już coraz mniej. Następną właściwością Object Pascala jest konieczność użycia operatora adresowego @ przy próbie wczytania danych do bufora za pomocą funkcji _lread(). Delphi potrzebuje jawnego odzyskania adresu miejsca w pamięci, pod którym aktualnie zapamiętywany jest bufor danych. Nie powinno to nikogo dziwić, chociażby ze względu na sposób deklaracji bufora:

Buffer_O : ARRAY[0..cbOutQueue] of Char;

który jest równoznaczny z zadeklarowaniem pewnego C-łańcucha. Cóż, Pascal był zawsze bardziej restrykcyjny niż C, jednak muszę przyznać, że niekiedy ma to swoje dobre strony. Poniższy przykład ilustruje jedną z metod użycia w programie napisanym w Delphi przedstawionych funkcji.

procedure TForm1.FileListBox1Change(Sender: TObject);

...

begin

hfile_s := _lopen(PChar(FileListBox1.FileName), OF_READ);

if (hfile_s <> HFILE_ERROR) then

begin

...

_lread(hfile_s, @Buffer_O, cbOutQueue);

_lclose(hfile_s);

end;

end;

Rysunek 5.17. Wygląd formularza projektu p_RS_16.dpr

0x01 graphic

Wydruk 5.14. Kod modułu RS_16.pas

unit RS_16;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ComCtrls, FileCtrl;

type

TForm1 = class(TForm)

CloseComm: TButton;

CheckBox1: TCheckBox;

OpenComm: TButton;

CheckBox2: TCheckBox;

CheckBox3: TCheckBox;

CheckBox4: TCheckBox;

SendFile: TButton;

Receive: TButton;

RichEdit1: TRichEdit;

DriveComboBox1: TDriveComboBox;

DirectoryListBox1: TDirectoryListBox;

FileListBox1: TFileListBox;

Edit1: TEdit;

ProgressBar1: TProgressBar;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure SendFileClick(Sender: TObject);

procedure ReceiveClick(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure FileListBox1Change(Sender: TObject);

private

{ Private declarations }

function Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;

nNumberOfBytesToWrite: DWORD): Integer;

function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 1024;

cbOutQueue = 1024;

var

hfile_s : HFILE; // identyfikator pliku źródłowego

Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

Number_Bytes_Read : DWORD;

hCommDev : THANDLE;

lpFileName : LPCSTR;

fdwEvtMask : DWORD;

Stat : TCOMSTAT;

Errors : DWORD;

dcb : TDCB;

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.FormCreate(Sender: TObject);

begin

DirectoryListBox1.FileList := FileListBox1;

DriveComboBox1.DirList := DirectoryListBox1;

FileListBox1.FileEdit := Edit1;

ProgressBar1.Step := 1;

end;

//--------------------------------------------------------------------

procedure TForm1.FileListBox1Change(Sender: TObject);

var i : Integer;

begin

for i := 0 to cbOutQueue do

Buffer_O[i] := #0; // czyści bufor wyjściowy

hfile_s := _lopen(PChar(FileListBox1.FileName), OF_READ);

if (hfile_s <> HFILE_ERROR) then

begin

_lread(hfile_s, @Buffer_O, cbOutQueue);

RichEdit1.Text := Buffer_O;

_lclose(hfile_s);

end;

end;

//--------------------------------------------------------------------

function TForm1.Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;

nNumberOfBytesToWrite: DWORD): Integer;

var

NumberOfBytesWritten : DWORD;

begin

// EscapeCommFunction(hCommDev, SETRTS);

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then

begin

// EscapeCommFunction(hCommDev, CLRRTS);

Write_Comm := 1

end

else

Write_Comm := 0;

end;

//--------------------------------------------------------------------

function TForm1.Read_Comm(hCommDev: THANDLE;

Buf_Size: DWORD): Integer;

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

Read_Comm := 1;

end

else

begin

Number_Bytes_Read := 0;

Read_Comm := 0;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM2';

if (CheckBox2.Checked = TRUE) then

lpFileName := 'COM1';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

if (CheckBox4.Checked = TRUE) then

dcb.BaudRate := CBR_9600;

//-przykładowe ustawienia flag sterujących DCB-

dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;

dcb.StopBits := TWOSTOPBITS;

dcb.ByteSize := 8;

SetCommState(hCommDev, dcb);

GetCommMask(hCommDev, fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny ',

mtError, [mbOk], 0);

end;

end;

//-----------wysyłanie pliku--------------------------------------

procedure TForm1.SendFileClick(Sender: TObject);

var

i : Integer;

FileSizeHigh : DWORD;

begin

for i := 0 to cbOutQueue do

Buffer_O[i] := char(0); // czyści bufor wyjściowy

ProgressBar1.Max := 0;

if (hCommDev > 0) then

begin

if((_lopen(PChar(FileListBox1.FileName), OF_READ)) <>

HFILE_ERROR) then

begin

hfile_s := _lopen(PChar(FileListBox1.FileName),

OF_READ);

ProgressBar1.Max := GetFileSize(hfile_s,

@FileSizeHigh);

while (_lread(hfile_s, @Buffer_O, 1) > 0) do

begin

Write_Comm(hCommDev, Buffer_O, 1); // 1 bajt

ProgressBar1.StepIt();

end;

_lclose(hfile_s);

FlushFileBuffers(hCommDev);

end

else

Application.MessageBox('Nie wybrano pliku do'+

' transmisji ', 'Uwaga !',MB_OK);

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//------------odbiór danych-------------------------------------------

procedure TForm1.ReceiveClick(Sender: TObject);

begin

if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then

begin

RichEdit1.Text := Buffer_I;

end

else

begin

RichEdit1.Text := 'Brak danych do odebrania';

Beep();

end;

end;

//--------------------------------------------------------------------

end.

Poprawność zastosowanych przypisań sprawdziłem, nawiązując za pomocą przedstawionego programu współpracę z sąsiednim komputerem, na którym uruchomiony był Terminal dla Windows 3.11. Przesyłając pewien niewielki (ok. 3 kB) plik tekstowy, otrzymałem w pełni poprawną, obustronną transmisję. Można też zauważyć, że użyliśmy tutaj funkcji Write_Comm() o trzech (a nie jak poprzednio dwóch) parametrach. Uczyniłem tak dlatego, by uzyskać przede wszystkim pełną przejrzystość ale też i zgodność przypisań z _lread().

Zaprezentuję teraz trochę bardziej skomplikowaną w swojej budowie aplikację, za pomocą której możemy nadawać i odbierać oraz zapisywać na dysku zarówno pliki jak i dowolne, wpisywane z klawiatury lub kopiowane z innych edytorów (np. Word) ciągi znaków. Jest rzeczą bardzo ważną, że pisane przez nas programy komunikacyjne są w pełni kompatybilne z profesjonalnymi edytorami tekstu. Analogicznie jak w przypadku odpowiednika napisanego w C++Builderze zaprojektowałem w Object Pascalu uproszczony edytor IDE, którego obsługa ukryta jest w procedurach obsługi zdarzeń: CopyTextClick(), PasteTextClick()oraz CutTextClick(). Mając bardziej kompletny przykład w postaci formularza projektu p_RS_06.bpr, omówionego w części poświęconej Builderowi, bez problemu można samodzielnie uzupełnić takie miniśrodowisko edycyjne prezentowane na bieżącym przykładzie. Wygląd działającej aplikacji komunikacyjnej korzystającej z takiego prostego IDE pokazano na rysunku 5.18, zaś jej kod źródłowy RS_17.pas przedstawiono na wydruku 5.15. Odpowiedni projekt znajduje się na CD w katalogu \KODY\DELPHI\RS_17\p_RS_17.dpr. Zastosowałem tu tylko jedno okno edycji, w którym wyświetlamy zarówno pliki lub inne komunikaty przeznaczone do wysłania, jak też dane odbierane z portu szeregowego. W przedstawionej niżej aplikacji uwzględniłem ponadto możliwość wyświetlania tekstu w linii tytułowej formularza. Głównym zadaniem tej linii będzie wyświetlanie nazwy aktualnie edytowanego pliku. Dodatkowo będzie się tam pojawiać również nazwa działającej aplikacji. Wszystkie wymienione czynności zostaną wykonane dzięki procedurze FormCaption():

procedure TForm1.FormCaption(const sFile_s: String);

begin

sFile := sFile_s;

Caption := Format('%s - %s', [ExtractFileName(sFile_s),

Application.Title]);

end;

Testując przedstawiony program, zapoznamy się ponadto ze sposobami posługiwania się innego rodzaju oknami dialogowymi, za pomocą których możemy wczytać i ewentualnie zapisać różnego rodzaju dane. Projektując procedurę obsługi zdarzenia FileOpenClick(), wykorzystaliśmy standardowe właściwości dialogu TOpenDialog, za pomocą którego możemy wczytać większość dostępnych plików. Zdarzenie SaveAs1Click(), posługujące się dialogiem TSaveDialog, umożliwia zapisanie w postaci pliku aktualnej zawartości danego pola edycji.

Rysunek 5.18. Formularz projektu p_RS_17.dpr w trakcie działania

0x01 graphic

Wydruk 5.15. Kod modułu RS_17.pas

unit RS_17;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ComCtrls, FileCtrl, ToolWin, Buttons, Menus;

type

TForm1 = class(TForm)

CloseComm: TButton;

OpenComm: TButton;

SendFile: TButton;

Receive: TButton;

RichEdit1: TRichEdit;

ProgressBar1: TProgressBar;

OpenDialog1: TOpenDialog;

SaveDialog1: TSaveDialog;

CoolBar1: TCoolBar;

CopyText: TSpeedButton;

PasteText: TSpeedButton;

CutText: TSpeedButton;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

CheckBox3: TCheckBox;

CheckBox4: TCheckBox;

MainMenu1: TMainMenu;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure SendFileClick(Sender: TObject);

procedure ReceiveClick(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure CopyTextClick(Sender: TObject);

procedure PasteTextClick(Sender: TObject);

procedure CutTextClick(Sender: TObject);

procedure FileOpenClick(Sender: TObject);

procedure NewClick(Sender: TObject);

procedure SaveAs1Click(Sender: TObject);

procedure SendWrittenClick(Sender: TObject);

private

{ Private declarations }

sFile: String;

procedure FormCaption(const sFile_s: String);

procedure CheckFileSave;

procedure ShowFileOpen(const sFile_O: String);

function Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;

nNumberOfBytesToWrite: DWORD): Integer;

function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 1024;

cbOutQueue = 1024;

var

hfile_s : HFILE; // identyfikator pliku źródłowego

Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

Number_Bytes_Read : DWORD;

hCommDev : THANDLE;

lpFileName : LPCSTR;

fdwEvtMask : DWORD;

Stat : TCOMSTAT;

Errors : DWORD;

dcb : TDCB;

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

CheckFileSave;

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.FormCreate(Sender: TObject);

begin

OpenDialog1.InitialDir := ExtractFilePath(ParamStr(0));

SaveDialog1.InitialDir := OpenDialog1.InitialDir;

ProgressBar1.Step := 1;

end;

//--------------------------------------------------------------------

procedure TForm1.CopyTextClick(Sender: TObject);

begin

RichEdit1.CopyToClipboard;

end;

//-------------------------------------------------------------------

procedure TForm1.PasteTextClick(Sender: TObject);

begin

RichEdit1.PasteFromClipboard;

end;

//--------------------------------------------------------------------

procedure TForm1.CutTextClick(Sender: TObject);

begin

RichEdit1.CutToClipboard;

end;

//--------------------------------------------------------------------

procedure TForm1.FormCaption(const sFile_s: String);

begin

sFile := sFile_s;

Caption := Format('%s - %s', [ExtractFileName(sFile_s),

Application.Title]);

end;

//--------------------------------------------------------------------

procedure TForm1.CheckFileSave;

var

iCheckSave: Integer;

begin

if not RichEdit1.Modified then

Exit

else

begin

iCheckSave := MessageDlg(Format('Zawartość pliku lub okna'+

' została zmieniona. Zapisać zmiany %s? ', [sFile]),

mtConfirmation, mbYesNoCancel, 0);

case iCheckSave of

idYes: SaveAs1Click(Self);

idNo: {};

idCancel: Abort;

end;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.FileOpenClick(Sender: TObject);

begin

CheckFileSave;

if OpenDialog1.Execute then

begin

ShowFileOpen(OpenDialog1.FileName);

RichEdit1.ReadOnly := ofReadOnly in OpenDialog1.Options;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.NewClick(Sender: TObject);

begin

FormCaption('Bez nazwy');

RichEdit1.Lines.Clear;

RichEdit1.Modified := FALSE;

end;

//--------------------------------------------------------------------

procedure TForm1.SaveAs1Click(Sender: TObject);

begin

if SaveDialog1.Execute then

begin

if FileExists(SaveDialog1.FileName) then

if MessageDlg(Format('Plik zapisany ponownie %s',

[SaveDialog1.FileName]), mtConfirmation,

mbYesNoCancel, 0) <> idYes then Exit;

RichEdit1.Lines.SaveToFile(SaveDialog1.FileName);

FormCaption(SaveDialog1.FileName);

RichEdit1.Modified := FALSE;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.ShowFileOpen(const sFile_O: string);

begin

RichEdit1.Lines.LoadFromFile(sFile_O);

FormCaption(sFile_O);

RichEdit1.SetFocus;

RichEdit1.Modified := FALSE;

end;

//--------------------------------------------------------------------

function TForm1.Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;

nNumberOfBytesToWrite: DWORD): Integer;

var

NumberOfBytesWritten : DWORD;

begin

// EscapeCommFunction(hCommDev, SETRTS);

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then

begin

// EscapeCommFunction(hCommDev, CLRRTS);

Write_Comm := 1

end

else

Write_Comm := 0;

end;

//--------------------------------------------------------------------

function TForm1.Read_Comm(hCommDev: THANDLE;

Buf_Size: DWORD): Integer;

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

Read_Comm := 1;

end

else

begin

Number_Bytes_Read := 0;

Read_Comm := 0;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM1';

if (CheckBox2.Checked = TRUE) then

lpFileName := 'COM2';

hCommDev := CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

if (CheckBox4.Checked = TRUE) then

dcb.BaudRate := CBR_19200;

//-przykładowe ustawienia znaczników sterujących DCB-

dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;

dcb.StopBits := TWOSTOPBITS;

dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);

GetCommMask(hCommDev, fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

end

else

case hCommDev of

IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+

' lub jest on aktywny ',

mtError, [mbOk], 0);

end;

end;

//-----------wysyłanie pliku--------------------------------------

procedure TForm1.SendFileClick(Sender: TObject);

var

i : Integer;

FileSizeHigh : DWORD;

begin

for i := 0 to cbOutQueue do

Buffer_O[i] := char(0); // czyści bufor wyjściowy

ProgressBar1.Max:=0;

if (hCommDev > 0) then

begin

if((_lopen(PChar(OpenDialog1.FileName), OF_READ)) <>

HFILE_ERROR) then

begin

hfile_s := _lopen(PChar(OpenDialog1.FileName),

OF_READ);

ProgressBar1.Max:=GetFileSize(hfile_s,

@FileSizeHigh);

while (_lread(hfile_s, @Buffer_O, 1) > 0) do

begin

Write_Comm(hCommDev, Buffer_O, 1); // transmisja

// 1 bajtu

ProgressBar1.StepIt();

end;

_lclose(hfile_s);

FlushFileBuffers(hCommDev);

end

else

Application.MessageBox('Nie wybrano pliku do'+

' transmisji ', 'Uwaga !',MB_OK);

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//------------odbiór danych-------------------------------------------

procedure TForm1.ReceiveClick(Sender: TObject);

begin

ProgressBar1.Max := 0;

if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then

begin

RichEdit1.Text := Buffer_I;

end

else

begin

RichEdit1.Text := 'Brak danych do odebrania';

Beep();

end;

end;

//--------transmisja danych wpisanych---------------------------------

procedure TForm1.SendWrittenClick(Sender: TObject);

begin

if (hCommDev > 0) then

begin

StrCopy(Buffer_O, PChar(RichEdit1.Text));

ProgressBar1.Max := 0;

ProgressBar1.Max := SizeOf(PChar(RichEdit1.Text));

Write_Comm(hCommDev, Buffer_O, StrLen(Buffer_O));

ProgressBar1.StepIt();

FlushFileBuffers(hCommDev);

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//--------------------------------------------------------------------

end.

Ze względu na to, że korzystamy tu z TRichEdit, dane siłą rzeczy będą zapisane w tym formacie. Widoczne jest to przy próbie odczytu tak przetransmitowanego pliku. Jeżeli nie jest on wyświetlany w zgodnym formacie, tekst będzie poprzedzony specjalnymi znakami formatowania. Trudność tę można przezwyciężyć, jeżeli zastosujemy w zdarzeniach czytających i zapisujących pliki na dysku konstrukcje typowe dla Object Pascala. Procedury alternatywne do tych przedstawionych na wydruku 5.15, obsługujące typowe pliki tekstowe, z powodzeniem mogą przyjąć następującą budowę:

procedure TForm1.FileOpen_2Click(Sender: TObject);

var

InFile : TextFile;

sFname, sIndata : String;

begin

RichEdit1.Lines.Clear;

if OpenDialog1.Execute then

begin

sFname := OpenDialog1.Filename;

AssignFile(InFile, sFname);

Reset(InFile);

while not EOF(InFile) do

begin

ReadLn(InFile, sIndata);

RichEdit1.Lines.Add(sIndata);

end;

CloseFile(InFile);

Form1.Caption := 'Edycja [' + sFname + ' ]';

end;

end;

//--------------------------------------------------------------------

procedure TForm1.FileSave_2Click(Sender: TObject);

var

OutFile : TextFile;

sFname : String;

begin

if SaveDialog1.Execute then

begin

sFname := SaveDialog1.FileName;

AssignFile(OutFile, sFname);

Rewrite(OutFile);

WriteLn(OutFile, RichEdit1.Text);

CloseFile(OutFile);

Form1.Caption := 'Zapisany [ ' + sFname + ' ]';

end;

end;

//--------------------------------------------------------------------

Zauważmy, że po raz pierwszy mamy możliwość wykorzystania pętli, która do obiektu TRichEdit ładuje kolejne wiersze pobrane z pliku.

Wiele miejsca poświęciliśmy na przedstawienie różnych sposobów odczytu i zapisu plików. Wbrew pozorom jest to zawsze bardzo ważny element aplikacji komunikacyjnej, gdyż zajmując się szeroko rozumianym sterowaniem czy komunikacją komputerową, prędzej czy później należy liczyć się z koniecznością obsługi wysyłanych lub otrzymywanych zbiorów danych. Bardzo często zachodzi też potrzeba przedstawienia informacji zawartych w plikach w postaci różnego rodzaju wydruków. Jednak zagadnienia związane z obsługą drukarek w aplikacjach pisanych w Delphi lub C++Builderze znacznie wykraczają poza ramy tej książki.

Timer w Delphi

Sposób wykorzystania właściwości komponentu TTimer w Object Pascalu nie różni się w istocie od tego, co zaprezentowaliśmy wcześniej. Niemniej jednak omówimy obecnie nieco dokładniej pewne aspekty wykorzystywania Timera w programach pomiarowych. zaprezentujemy jeden z możliwych sposobów jego użycia w aplikacji obsługującej pewne bardzo nowoczesne urządzenie ,służące do stabilizacji i odczytu temperatury. Jest to najnowsza odmiana rodziny mierników, za pomocą których testowałem poprzednie programy. Obsługa tego konkretnego modelu wymaga stosowania w połączeniu jedynie linii TxD, RxD oraz przewodu masy. Koniec wysyłanych przez siebie danych znaczy on parą znaków CR LF. Wygląd działającej aplikacji pokazano na rysunku 5.19, zaś jej projekt dostępny jest w katalogu \KODY\DELPHI\RS_18\p_RS_18.dpr. Odczytywana temperatura (w stopniach Kelvina) wyświetlana jest w obiekcie edycji TRichEdit. Przy pomocy komponentu TTrackBar można prawie płynnie ustalać częstość odczytu wskazań przyrządu. Komponent ten uczyniłem zdolnym do generowania zdarzenia TrackBar1Change(), tak jak pokazuje to wydruk 5.16. Wykorzystane przeze mnie właściwości TTrackBar umożliwiają wykonanie w trakcie pomiaru płynnej synchronizacji generowanych przez aplikację zdarzeń ze zdarzeniami generowanymi przez miernik. Dzięki procedurze obsługi zdarzenia TimerONClick() skojarzonego z przyciskiem Rozpocznij pomiar uaktywniamy obsługę zdarzenia TimerOnTimer(), gdzie dokonuje się właściwy odczyt temperatury. Zauważmy, że dzięki zastosowaniu instrukcji Repeat...Until proces wysyłania zapytania będzie powtarzany, aż do momentu wysłania ostatniego znaku z bufora wyjściowego. Nie ma tu żadnych instrukcji opóźniających typu Sleep()! Jest jeszcze jedna, bardzo ważna zaleta takiej konstrukcji części algorytmu wysyłającego dane — nie musimy przy nadawaniu zbytnio przejmować się znacznikiem końca danych CR LF.

Rysunek 5.19. Wygląd formularza projektu p_RS_18.dpr

0x01 graphic

Wydruk 5.16. Kod modułu RS_18.pas aplikacji wykorzystującej komponent TTimer

unit RS_18;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ComCtrls, ExtCtrls;

type

TForm1 = class(TForm)

CloseComm: TButton;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

OpenComm: TButton;

TimerON: TButton;

TimerOFF: TButton;

RichEdit1: TRichEdit;

Timer1: TTimer;

TrackBar1: TTrackBar;

Edit1: TEdit;

Label1: TLabel;

Label2: TLabel;

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure TimerONClick(Sender: TObject);

procedure TimerOFFClick(Sender: TObject);

procedure TimerOnTimer(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure TrackBar1Change(Sender: TObject);

private

{ Private declarations }

function Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD): Integer;

function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

dcb_fOutxCtsFlow = $0004;

dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --

DTR_CONTROL_ENABLE = $0010;

DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;

dcb_fTXContinueOnXoff = $0080;

dcb_fOutX = $0100;

dcb_fInX = $0200;

dcb_fErrorChar = $0400;

dcb_fNull = $0800;

// -- fRtsControl --

RTS_CONTROL_ENABLE = $1000;

RTS_CONTROL_HANDSHAKE = $2000;

RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 16;

cbOutQueue = 16;

var

query : PChar = 'CDAT?'+#13+#10; // przykładowe zapytanie

// zakończone parą znaków CR LF

Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

Number_Bytes_Read : DWORD;

hCommDev : THANDLE;

lpFileName : PChar;

fdwEvtMask : DWORD;

Stat : TCOMSTAT;

Errors : DWORD;

dcb : TDCB;

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

begin

Timer1.Enabled := FALSE;

CloseHandle(hCommDev);

Application.Terminate();

end;

//--------------------------------------------------------------------

procedure TForm1.FormCreate(Sender: TObject);

begin

Timer1.Enabled := FALSE;

Timer1.Interval := 1000;

TrackBar1.Max := 1000;

TrackBar1.Min := 1;

TrackBar1.Frequency := 100;

end;

//--------------------------------------------------------------------

function TForm1.Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD): Integer;

var

NumberOfBytesWritten : DWORD;

begin

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then

Write_Comm := 1

else

Write_Comm := 0;

end;

//--------------------------------------------------------------------

function TForm1.Read_Comm(hCommDev: THANDLE;

Buf_Size: DWORD): Integer;

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

Read_Comm := 1;

end

else

begin

Number_Bytes_Read := 0;

Read_Comm := 0;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM2';

hCommDev := CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE,0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength = sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox2.Checked = TRUE) then

dcb.BaudRate := CBR_1200;

//-- przykładowe ustawienia znaczników sterujących DCB --

dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;

dcb.StopBits :=ONESTOPBIT;

dcb.ByteSize :=7;

SetCommState(hCommDev, dcb);

GetCommMask(hCommDev, fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

end

else

case hCommDev of

IE_BADID: Application.MessageBox('Niewłaściwa nazwa portu'+

' lub jest on aktywny ', 'Uwaga !',MB_OK);

end;

end;

//--------------------------------------------------------------------

procedure TForm1.TimerONClick(Sender: TObject);

begin

if (hCommDev > 0) then

begin

StrCopy(Buffer_O, query);

Timer1.Enabled := TRUE;

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//--------------------------------------------------------------------

procedure TForm1.TimerOFFClick(Sender: TObject);

begin

Timer1.Enabled := FALSE;

end;

//--------------------------------------------------------------------

procedure TForm1.TrackBar1Change(Sender: TObject);

begin

Timer1.Interval := TrackBar1.Position;

Edit1.Text := IntToStr(TrackBar1.Position);

end;

//--------------------------------------------------------------------

procedure TForm1.TimerOnTimer(Sender: TObject);

begin

Repeat

// wysłanie zapytania

FlushFileBuffers(hCommDev);

Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);

if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then // odbiór

// danych

RichEdit1.Text := Buffer_I

else

RichEdit1.Text := 'Brak danych';

end;

//--------------------------------------------------------------------

end.

Spoglądając na treść podobnych algorytmów napisanych w C++Builderze, możesz z powodzeniem użyć instrukcji do...while w celu płynnego dostrojenia się do miernika. Cóż, nie ukrywam, że pisząc programy w Delphi trochę ściągaliśmy z Buildera. Może warto dla zachowania równowagi czasami postąpić odwrotnie?

Aby wyłączyć generację zdarzenia cyklicznie wysyłającego i odbierającego dane wystarczy nacisnąć przycisk Zakończ pomiar, przez co wywołamy procedurę obsługi zdarzenia TimerOFFClick().

Musimy jeszcze zastanowić się nad tym, czy istnieje możliwość szybkiego stwierdzenia, z poziomu działającej aplikacji, że miernik rzeczywiście w jakiś sposób znaczy koniec wysyłanych przez siebie komunikatów. Możemy to stwierdzić, ale należy zdawać sobie sprawę z pewnych właściwości różnych komponentów edycyjnych. Spróbujemy wyświetlić rezultat pomiaru za pomocą trzech obiektów, których używaliśmy do tej pory: TRichEdit, TEdit oraz TMemo. Wyniki pokazane są na rysunku 5.20. O ile w przypadku dwóch skrajnych komponentów edycyjnych nie ma różnicy co do formatu wyświetlania wartości aktualnie mierzonej temperatury, to w środkowym typu TEdit pojawiły się dwa kwadraciki zaraz po odczytanej wartości. To jest właśnie owa para znaków CR LF. Wygląd symboli reprezentujących wymienioną parę znaków z reguły nie zależy w Windows od użytej przez programistę funkcji, zamieniającej liczbę na odpowiedni łańcuch. W tym przykładzie użyłem prostych przypisań:

RichEdit1.Text := Buffer_I;

Edit2.Text := Buffer_I;

Memo1.Text := Buffer_I;

Równie dobrze o tym fakcie można się przekonać, odczytując liczbę rzeczywiście odebranych bajtów. Z reguły będzie ona o dwa większa niż ilości znaków aktualnie wyświetlonych na ekranie. Osoby zaniepokojone faktem pojawiania się jakiś dodatkowych symboli w oknie edycji pragnę od razu uspokoić. W żadnym wypadku nie zostają one uwzględniane przy zapisie danych do pliku. Informacja, którą daje nam nowoczesny przyrząd, jest bardzo dyskretna.

Rysunek 5.20. Wygląd formularza projektu p_RS_19.dpr wyświetlającego dane w różnych komponentach edycyjnych

0x01 graphic

Spoglądając na ostatni rysunek, powróćmy jeszcze na chwilę do niezwykle ważnego tematu, czyli problemu synchronizacji stworzonego programu z urządzeniem pomiarowym. W przykładzie napisanym w Builderze funkcję taką pełnił komponent TCSpinEdit. Obecnie zastosowaliśmy prosty suwak, którego zakres zmienności podzieliłem na 1000 części. Używając obiektu TTimer mogę więc minimalną częstość jego wyzwalania określić w przybliżeniu na 1 milisekundę. Jest to oczywiście w odniesieniu do tego konkretnego urządzenia wartość znacznie przesadzona, niemniej jednak oddaje chyba ideę synchronizacji działania aplikacji z pracą miernika. Stosując tak prostą metodę, można z dobrym przybliżeniem określić w miarę optymalną częstość odpytywania przyrządu. Jest to wielka zaleta techniki programowania obiektowo-zdarzeniowego, które niewątpliwie zmienia podejście do sposobu pisania tego typu aplikacji. Projektując warstwę synchronizacyjną programu, musimy mieć przede wszystkim na uwadze jej optymalność. Nie ma wielkiego sensu próbkowanie łącza w czasie krótszym niż możliwości danego przyrządu. Musimy też zdawać sobie sprawę z przeznaczenia naszego programu. W omawianym przykładzie posłużyłem się urządzeniem, którego głównym zadaniem jest bardzo dokładna stabilizacja i odczyt temperatury. To, że jest w stanie odpowiadać co kilkaset milisekund nie stanowi dla mnie niespodzianki.

W praktyce producenci konkretnych przyrządów pomiarowych częstość próbkowania łącza, do którego urządzenie jest przyłączane, podają z pewną dość szeroką tolerancją. Dochodzimy więc do tego, że sam programista musi taki w miarę optymalny czas ustalić. Z reguły postępuje się w sposób następujący: wykorzystując jakiekolwiek zaprojektowane wcześniej zdarzenie, sprawdzamy moment, w którym przestajemy otrzymywać błędne (przekłamane) informacje pochodzące od urządzenia. Wcale to jednak nie znaczy, że jeżeli przy ustalonym przedziale próbkowania, np. 750 ms, otrzymuję w dość długim okresie prawidłowe wskazania przyrządu, to wszystko jest już w porządku. Może się na przykład okazać, że dla wartości 760 ms pomiar zacznie przebiegać bardziej miarowo. Dlatego należy mieć na uwadze dwie sprawy: bezbłędny odczyt w długim przedziale czasu oraz regularną jego powtarzalność co pewien (najkrótszy z możliwych) przedziałów czasowych. Z reguły pierwsze testy świeżo napisanej tego typu aplikacji trwają wiele godzin. Zobaczmy, jak to wygląda w praktyce. Na rysunku 5.21 zamieściłem testy programu sterującego miernikiem, odczytującym temperaturę pewnego układu fizycznego. Przyjąłem najprostsze z możliwych rozwiązań. Kawałek stali kwasoodpornej mający temperaturę początkową około 311 K szybko podgrzałem do temperatury około 326,5 K, następnie ochładzałem go, rejestrując wskazania przyrządu w funkcji czasu względnego (numeru pomiaru).

Rysunek 5.21. Pomiar temperatury za pomocą aplikacji wykorzystującej komponent TTimer

0x01 graphic

Widoczna na rysunku krzywa A prezentuje wynik odczytu przy zastosowaniu najmniejszej z możliwych częstości próbkowania łącza, przy której nie zauważyłem już żadnych błędów transmisji. Test B został wykonany przy trochę mniejszej częstości odczytu przyrządu, dając, jak widzimy, dużo lepsze rezultaty. Oba eksperymenty były przeprowadzane z nieco odmienną szybkością ochładzania materiału jedynie po to, by wyniki były rozróżnialne na wykresie, fakt ten nie wpływa w żadnym wypadku na wynik synchronizacji programu z urządzeniem. Zauważmy, że przy polepszającym się stopniu synchronizacji zmiana temperatury w funkcji kolejnego pomiaru widoczna jest w postaci schodków. Im regularniej będą one rozłożone oraz im mniejsza będzie ich szerokość, tym lepszy jest stopień synchronizacji miernika i programu zbierającego dane. Jeżeli dysponowalibyśmy bardzo szybkim przyrządem oraz poprawnie działającą aplikacją, tego typu wykres przedstawiałby linię gładką. Podobnie gładki wykres możemy otrzymać, przyjmując wystarczająco długi okres pomiędzy dwoma odczytami. Mówimy wówczas, że pomiary są uśredniane w czasie.

Czytając wszystko, co zostało napisane do tej pory o obiekcie typu TTimer, zapewne niejeden Czytelnik może lekko powątpiewać w celowość powoływania się na możliwości uzyskania tak dokładnego przedziału czasu próbkowania łącza. Panuje bowiem opinia, że dokładność i powtarzalność działania tego komponentu są, delikatnie mówiąc, średniej jakości. Zgadzam się, że Timer nie jest bynajmniej sztandarowym osiągnięciem technologii informatycznych XX wieku. Z drugiej jednak strony jest powszechnie dostępnym i wygodnym narzędziem. Jeżeli tylko używa się go w sposób rozsądny, program może działać poprawnie. Timer służy do generowania zdarzeń w mniej lub bardziej jednakowych odstępach czasu. Nic nie stoi na przeszkodzie, by porównać jego działanie z funkcjonowaniem zegara systemowego, który można znaleźć w Panelu sterowania. Zbudowałem naprawdę prostą aplikację, której jedynym celem jest cykliczne, wraz z działaniem Timera, wyświetlanie kolejnych liczb, tak jak pokazuje to rysunek 5.22.

Rysunek 5.22. Porównanie stopnia synchronizacji działającego zegara systemowego z aplikacją wykorzystującą komponent TTimer

0x01 graphic

Wykorzystany w tym przykładzie opis procedury obsługi zdarzenia TimerOnTimer() jest banalny:

var

intVar: int64;

...

intVar := 0;

...

procedure TForm1.TimerOnTimer(Sender: TObject);

begin

Inc(intVar);

RichEdit1.Text := IntToStr(IntVar);

Beep();

end;

Każda osoba, która zechce taką aplikację przetestować w sposób pokazany na powyższym rysunku, od razu zauważy, że w funkcjonowaniu tych dwóch zupełnie niezależnych programów występuje dość wyraźne podobieństwo. Powiedzmy, że właściwość Interval obiektu TTimer ustalimy na 1000 ms. Przyjrzyjmy się dokładnie — te dwa zegary po pewnym, nawet dość krótkim czasie, najzwyczajniej w świecie zaczną się zdudniać! Nic w tym dziwnego, korzystają z tych samych zasobów Windows. Oczywiście, że efektu takiego nigdy w 100% w tym systemie operacyjnym nie wyeliminujemy, możemy natomiast w pewnym stopniu go ograniczyć. Wystarczy, abyśmy czas próbkowania Timera ustalili trochę mniejszy, powiedzmy na 990 ms. Od razu zauważymy, że oba zegary zaczęły pracować bardziej stabilnie. Można w Windows znaleźć jeszcze parę podobnych ciekawostek, które dla osoby piszącej tego typu programy i znającej podstawy fizyki nie powinny stanowić żadnego wyzwania.

Należy również być świadomym faktu, że używanie Timera nie zapewni nam jakiejś super szybkiej możliwości odczytywania danych, jednak dla większości spotykanych przypadków może on być całkiem użyteczny. Spotyka się ultraszybkie urządzenia, mierzące z dokładnością nanosekund różne parametry (w tym temperaturę np. gazu w przepływie), jednak przyrządów takich nie obsługują PC, zaś stosowanych protokołów transmisji danych na próżno szukać w jakiejkolwiek literaturze.

Kończąc rozważania o Timerze, chciałbym przedstawić przykład kompletnej aplikacji obsługującej pewien woltomierz cyfrowy. Na rysunku 5.23 pokazano wygląd jej formularza, którego projekt zamieszczono w katalogu \KODY\DELPHI\RS_20\p_RS_20.dpr. Zastosowałem tu dobrze nam już znane komponenty. Konstrukcja algorytmu umożliwia zatrzymanie pomiaru w dowolnej chwili. Wówczas zawartość poszczególnych okien edycji można przekopiować do schowka (oczywiście klikając uprzednio w obszar jednego z nich). Wyniki takie bez problemu można już wstawić do dowolnego arkusza kalkulacyjnego czy innego programu graficznego akceptującego dane w postaci kolumn liczb. Aktywne własności ScrollBars komponentów TMemo umożliwiają ponadto wygodny przegląd całości pomiarów w trakcie działania programu. Dla wygody obsługi zdarzenia TimerOnTimer(), części programu wysyłające zapytanie do woltomierza oraz odczytujące jego wskazania zapisałem w oddzielnych funkcjach RS_Send() oraz RS_Receive(). Zastosowałem dodatkowo jeszcze jedną, dosyć ciekawą modyfikację — tuż po rozpoczęciu pomiaru przycisk Otwórz port staje się nieaktywny. Jest to jeden ze sposobów, w jaki można uchronić program przed nadmiernie dociekliwymi Użytkownikami, którzy w wolnych chwilach lubią zadawać sobie pytania będące niewątpliwie rezultatem głębszych przemyśleń, w stylu: Co się stanie, gdy w trakcie działania programu będę bez przerwy naciskał jakiś przycisk?

Rysunek 5.23. Wygląd formularza projektu p_RS_20.dpr obsługującego woltomierz cyfrowy

0x01 graphic

Wydruk 5.17. Kod modułu RS_20.pas aplikacji zbierającej dane z woltomierza cyfrowego

unit RS_20;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons;

type

TForm1 = class(TForm)

Start: TButton;

Suspend: TButton;

Resume: TButton;

CloseComm: TButton;

OpenComm: TButton;

Memo2: TMemo;

Timer1: TTimer;

Edit1: TEdit;

TrackBar1: TTrackBar;

Label1: TLabel;

Label2: TLabel;

Label3: TLabel;

Panel1: TPanel;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

Label4: TLabel;

Label5: TLabel;

SpeedButton1: TSpeedButton;

SpeedButton2: TSpeedButton;

Memo1: TMemo;

procedure StartClick(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure SuspendClick(Sender: TObject);

procedure ResumeClick(Sender: TObject);

procedure CloseCommClick(Sender: TObject);

procedure OpenCommClick(Sender: TObject);

procedure TimerOnTimer(Sender: TObject);

procedure SpeedButton1Click(Sender: TObject);

procedure SpeedButton2Click(Sender: TObject);

procedure TrackBar1Change(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

const

// -- wartości znaczników sterujących portu szeregowego --

dcb_fBinary = $0001;

dcb_fParity = $0002;

cbInQueue = 32; // rozmiary buforów danych

cbOutQueue = 32;

var

query : PChar = 'CDAT?'+#13+#10; // przykładowe zapytanie

Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

Number_Bytes_Read : DWORD;

hCommDev : THANDLE;

lpFileName : PChar;

fdwEvtMask : DWORD;

Stat : TCOMSTAT;

Errors : DWORD;

dcb : TDCB;

intVar: LongWord; // licznik pomiarów

//--------------------------------------------------------------------

procedure TForm1.CloseCommClick(Sender: TObject);

var

iCheckProcess: Integer;

begin

iCheckProcess := MessageDlg('Zakończenie pomiaru i zamknięcie'+

' aplikacji?', mtConfirmation, [mbYes, mbNo], 0);

case iCheckProcess of

idYes:

begin

Timer1.Enabled := FALSE;

CloseHandle(hCommDev);

Application.Terminate();

end;

idNo: Exit;

end;

end;

//--------------------------------------------------------------------

procedure TForm1.OpenCommClick(Sender: TObject);

begin

if (CheckBox1.Checked = TRUE) then

lpFileName := 'COM2';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,

NIL, OPEN_EXISTING, 0, 0);

if (hCommDev <> INVALID_HANDLE_VALUE) then

begin

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength := sizeof(dcb);

GetCommState(hCommDev, dcb);

if (CheckBox2.Checked = TRUE) then

dcb.BaudRate:=CBR_1200;

//-przykładowe ustawienia znaczników sterujących DCB-

dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;

dcb.StopBits := ONESTOPBIT;

dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);

GetCommMask(hCommDev, fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

end

else

case hCommDev of

IE_BADID:

begin

Application.MessageBox('Niewłaściwa nazwa portu lub'+

' jest on aktywny', 'Uwaga !',MB_OK);

lpFileName:='';

end;

end;

end;

//--------------------------------------------------------------------

function Write_Comm(hCommDev: THANDLE;

nNumberOfBytesToWrite: DWORD): Integer;

var

NumberOfBytesWritten : DWORD;

begin

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,

NumberOfBytesWritten, NIL);

if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then

Write_Comm := 1

else

Write_Comm := 0;

end;

//--------------------------------------------------------------------

function Read_Comm(hCommDev: THANDLE;

Buf_Size: DWORD): Integer;

var

nNumberOfBytesToRead: DWORD;

begin

ClearCommError(hCommDev, Errors, @Stat);

if (Stat.cbInQue > 0) then

begin

if (Stat.cbInQue > Buf_Size) then

nNumberOfBytesToRead := Buf_Size

else

nNumberOfBytesToRead := Stat.cbInQue;

ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,

Number_Bytes_Read, NIL);

Read_Comm := 1;

end

else

begin

Number_Bytes_Read := 0;

Read_Comm := 0;

end;

end;

//--------------------------------------------------------------------

function RS_Send: Integer;

begin

Repeat

FlushFileBuffers(hCommDev);

Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);

Inc(intVar); // zliczanie kolejnych pomiarów

Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));

Form1.Memo1.Lines.Add('');

Result := 0;

end;

//--------------------------------------------------------------------

function RS_Receive: Integer;

begin

if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then

begin

Form1.Memo2.Lines.Add(AnsiString(Buffer_I));

// Beep();

end

else

begin

Form1.Memo2.Lines.Add('x0');

Beep();

Form1.Memo2.Lines.Add('');

end;

Result:=0;

end;

//--------------------------------------------------------------------

procedure TForm1.StartClick(Sender: TObject);

begin

if (hCommDev > 0) then

begin

OpenComm.Enabled := FALSE;

Timer1.Enabled:=TRUE;

Label1.Caption := 'Pomiar';

end

else

Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+

' aktywny ', 'Uwaga !',MB_OK);

end;

//--------------------------------------------------------------------

procedure TForm1.FormCreate(Sender: TObject);

begin

Timer1.enabled := FALSE;

Timer1.Interval := 1000;

TrackBar1.Position := 1000;

TrackBar1.Max := 2000;

TrackBar1.Min := 1;

TrackBar1.Frequency := 100;

OpenComm.Enabled := TRUE;

intVar := 0;

end;

//--------------------------------------------------------------------

procedure TForm1.SuspendClick(Sender: TObject);

begin

Timer1.Enabled := FALSE;

Label1.Caption := 'Wstrzymanie';

end;

//--------------------------------------------------------------------

procedure TForm1.ResumeClick(Sender: TObject);

begin

Timer1.Enabled := TRUE;

Label1.Caption := 'Pomiar';

end;

//--------------------------------------------------------------------

procedure TForm1.TimerOnTimer(Sender: TObject);

begin

StrCopy(Buffer_O, query);

RS_Send;

RS_Receive;

end;

//--------------------------------------------------------------------

procedure TForm1.SpeedButton1Click(Sender: TObject);

begin

Form1.Memo2.SelectAll;

Form1.Memo2.CopyToClipboard;

end;

//--------------------------------------------------------------------

procedure TForm1.SpeedButton2Click(Sender: TObject);

begin

Form1.Memo1.SelectAll;

Form1.Memo1.CopyToClipboard;

end;

//--------------------------------------------------------------------

procedure TForm1.TrackBar1Change(Sender: TObject);

begin

Timer1.Interval := TrackBar1.Position;

Edit1.Text := IntToStr(TrackBar1.Position);

end;

//--------------------------------------------------------------------

end.

Podsumowanie

Pisząc ten rozdział, miałem na uwadze fakt, że z większością warstwy Win32 API, wykorzystywaną w programach posługujących się transmisją szeregową, zapoznaliśmy się już wcześniej. Na przykładzie konkretnych aplikacji omówiliśmy możliwości zastosowania tych funkcji w środowisku Delphi. Czytając obecny fragment książki, każdy mógł się zorientować, iż widoczna jeszcze na początku lat 90. różnica pomiędzy Pascalem i C++ w Windows została już praktycznie zatarta. Dlatego, moim zdaniem, bezprzedmiotowe jest prowadzenie sporów na temat wyższości Buildera nad współczesnym Object Pascalem. Oba wymienione środowiska programistyczne mogą się natomiast znakomicie uzupełniać, zaś ich poznanie z pewnością będzie świadczyło o naszej uniwersalności, która jest zawsze mile widziana wśród programistów.

Ćwiczenia

  1. Wykorzystując projekt p_RS_16.dpr, zmodyfikuj program w ten sposób, by można było zapisywać na dysku otrzymywane informacje.

  2. Uzupełnij projekt p_RS_17.dpr o możliwość wysyłania wybranego wiersza tekstu. W tym celu możesz posłużyć się funkcją Eoln().

3. Uzupełnij projekt p_RS_18.dpr o możliwość cyklicznego zapisu otrzymywanych danych.

Dalej należałoby formalnie posługiwać się pojęciem typu TDCB, czyli pewnego strukturalnego typu danych, ale, by niepotrzebnie nie komplikować dalszych wyjaśnień, pozostaniemy przy określeniu struktura DCB. Ta subtelność widoczna jest jedynie w deklaracji.

Nazwy takie jak: fParity czy fOutxCtsFlow nie są podtrzymywane w Delphi, dlatego w programie będą traktowane jako stałe, które uprzednio należy odpowiednio zadeklarować.

Jeżeli zajdzie potrzeba uwzględnienia wymienionego protokołu, należy jawnie odwołać się do DCB.

W pewnych szczególnych okolicznościach, np. po wykryciu błędu odczytu, w obiekcie TMemo efekt ten może również wystąpić.

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

H:\Książki\!Lukasz\RS 232. Praktyczne programowanie\4 po jezykowej\R_5_III.doc

163

Czy można to słowo usunąć albo zastąpić innym (niepowtarzalny, specyficzny)?

Składnia!!!



Wyszukiwarka