W rubryce "Dla praktyków" padło już wiele propozycji rozwiązań zastępujących funkcjonalnie standardowy moduł CRT. Nie będę zatem prezentować podejścia do tego problemu. Przedstawię natomiast coś, co może stanowić bardzo przyjemny dodatek do wspomnianych prac - kilka procedur i funkcji niskiego poziomu znacznie rozszerzających możliwości zarówno pascalowskiego modułu CRT, jak i modułów zastępujących go. Przy okazji powiem kilka słów na temat szybkości działania programów w Pascalu, wykorzystania wbudowanego w kompilator asemblera i różnych zalet stąd płynących.
Jeżeli chcemy stworzyć jakiekolwiek procedury obsługi ekranu w trybie tekstowym, mamy do wyboru: skorzystać z funkcji udostępnianych przez BIOS i system operacyjny za pośrednictwem przerwań lub spróbować samodzielnie poprzestawiać coś bezpośrednio w pamięci ekranu. Pierwsze z rozwiązań ma co najmniej dwie wady - przede wszystkim funkcje dostępne poprzez przerwania nie oferują zbyt wielu możliwości, a poza tym często wykonują się niezwykle wolno (szczególnie w trybie chronionym procesora). Bezpośredni dostęp do pamięci ekranu nie zawsze jest znacząco szybszy, pozwala jednak na tworzenie właściwie dowolnych procedur zarządzania ekranem, ograniczonych jedynie inwencją programisty. Moje rozwiązanie opiera się właśnie na ingerencji w pamięć ekranu, wykorzystuje jednak w niektórych momentach przerwania.
Aby móc robić cokolwiek z pamięcią ekranu, trzeba znać jej strukturę i wiedzieć, jak się do niej dostać. Pamięć tę możemy traktować jako tablicę rekordów. Każdy rekord składa się z dwóch pól - znaku i odpowiadającego mu atrybutu. Zdefiniowany w module tScreenChar jest takim właśnie rekordem, a typ tScreen odzwierciedla w pełni strukturę pamięci ekranu. Zmienna Screen jest wskaźnikiem do tak zdefiniowanej tablicy. Należy ją jeszcze odpowiednio zakotwiczyć, czyli przypisać jej adres początku pamięci ekranu. Wiadomo, że w trybach tekstowych nr 2 i 3 adres ten to B800:0000h, a w trybie nr. 7 (tryb monochromatyczny) B000:0000h. Innych trybów nie rozpatrujemy, gdyż tylko te trzy umożliwiają nam wyświetlenie 80 znaków w 25 wierszach. Do detekcji aktualnego trybu graficznego użyjemy funkcji VideoMode, która zwraca wartość przechowywaną w komórce 0040:0049h. Nie można tutaj posłużyć się zmienną typu absolute, ponieważ mogłyby wtedy pojawić się trudności w trybie chronionym (z tego powodu występują identyfikatory SegXXXX). Wszystkie te zadania wykonuje procedura InitExtCrt. Jest ona wywoływana automatycznie w części inicjalizacyjnej modułu, dlatego nie ma potrzeby używania jej bezpośrednio w programie (chyba że dokonaliśmy zmiany bieżącego trybu pracy karty graficznej). Skoro mamy już zdefiniowany wskaźnik do pamięci ekranu, możemy operować na jej elementach bezpośrednio z poziomu Pascala. Dostęp do znaku w punkcie P(x,y) uzyskujemy, pisząc Screen^[y,x].Char. W module nie ma właściwie funkcji pascalowskich, dlatego tak zdefiniowany wskaźnik nie jest potrzebny (wystarczyłby zwykły typ Pointer), ale może przydać się w programie korzystającym z usług naszego modułu, zatem warto było go stworzyć.
Poszczególne procedury i funkcje modułu wykonują następujące zadania :
- Frame - rysuje ramkę o zadanych wymiarach i grubości (single lub double),
- WriteXY - wypisuje ciąg znaków Str począwszy od punktu (x,y),
- ReadChar - zwraca znak znajdujący się na ekranie w punkcie (x,y),
- ReadAttr - zwraca atrybut znaku znajdującego się w punkcie (x,y),
- VideoMode - zwraca numer aktualnego trybu pracy karty graficznej,
- ClrScr - czyści ekran, ustawia kursor w punkcie (1,1),
- ScrFillRect - wypełnia prostokątny fragment ekranu znakiem c,
- ScrFillAttrRect - wypełnia prostokątny fragment ekranu atrybutem a,
- ScrGetRect - zapamiętuje na stercie prostokątny fragment ekranu i zwraca wskaźnik do tak utworzonego bufora.
- ScrPutRect - kopiuje na ekran prostokąt zapamiętany wcześniej za pomocą funkcji ScrGetRect i zwalnia przydzieloną pamięć.
Moduł zawiera także podstawowe procedury obsługi drukarki :
- PrintScreen - jej działanie daje taki sam efekt, jak naciśnięcie klawisza PrintScreen,
- PrnReset - wykonuje reset drukarki i zwraca jej status,
- PrnGetState - zwraca status drukarki,
- PrnPrintStr - drukuje łańcuch znaków str, umożliwia przesyłanie do drukarki kodów sterujących (stałe PrnXXXX),
- PrnPrintRect - drukuje prostokątny fragment ekranu, przesuwając wydruk do kolumny określonej argumentem AtPosition licząc od lewego marginesu (w przypadku niektórych drukarek trzeba będzie zmienić kody sterujące odpowiedzialne za przesunięcie poziome wydruku).
Znajdujące się w oddzielnym pliku deklaracje stałych mają charakter pomocniczy. Najważniejsze spośród nich to stałe kodów sterujących drukarki. Są one zgodne ze standardem Epsona i działają poprawnie na większości popularnych drukarek (były tworzone dla drukarki Star LC-100) bez potrzeby wprowadzania znaczących zmian. Gdy ktoś używa drukarki akceptującej inne kody sterujące, będzie musiał znaleźć je w instrukcji obsługi i wpisać w miejsce poprzednich.
Celowo zamieściłem na wydruku tylko mały fragment swego pełnego rozwiązania, które określam nazwą ExtendedCrt. Opuściłem inne procedury niskiego poziomu, np. tworzące i obsługujące proste menu, gdyż ich opis wykraczałby poza ramy tego artykułu. Nie podałem również procedur zastępujących działanie standardowych mechanizmów modułu Crt - były one już prezentowane na łamach PCKuriera.
Podstawową wadą mojego rozwiązania jest ograniczenie się tylko do trybów tekstowych 80 znaków w 25 wierszach. Jest to świadomy wybór związany z szybkością działania. Wiadomo bowiem, że im dana procedura jest bardziej uniwersalna, im więcej rzeczy musi sprawdzać i przeliczać w zależności od okoliczności, tym jest wolniejsza. Przerobienie wszystkich funkcji tak, by pracowały również w innych trybach, nie jest sprawą trudną i z pewnością upora się z tym każdy, komu taka przeróbka będzie potrzebna.
Wszystkie procedury modułu napisałem początkowo w Pascalu, ale ze względu na to, że są one odpowiedzialne za operacje niskiego poziomu, zdecydowałem się na przepisanie ich w asemblerze. Zysk na szybkości był imponujący - przy niektórych operacjach procedury asemblerowe były ponad cztery i pół raza szybsze, generując przy tym znacznie krótszy kod. Nie zamierzam w tym miejscu namawiać wszystkich piszących w Pascalu do przejścia na asembler, ale jeśli zamierzamy stworzyć wydajne procedury niskiego poziomu, to czasami po prostu nie ma innego wyjścia.
Pojawił się w tym miejscu ważny problem - szybkość pracy programów. Byłem ciekaw, na ile zapisanie pewnych operacji w asemblerze przyspieszy ich wykonywanie. W tym celu pozostawiłem sobie starszą wersję mojego modułu, zawierającego funkcje w Pascalu, i porównałem ją z nową, asemblerową wersją. Napisałem specjalny program, który wykonywał kolejne operacje ekranowe w pętlach for po kilka i kilkadziesiąt tysięcy razy, mierząc przy tym czas. Zapisywałem wyniki testu dla jednego modułu i porównałem z wynikami dla drugiego. Co się okazało ? Zgodnie z moimi przewidywaniami asembler okazał się znacznie szybszy.
- Procedury ScrFillRect i ScrFillAttrRect napisane w Pascalu za pomocą podwójnej pętli for :
Procedure ScrFillRect(Const x1,y1,x2,y2: Byte; Const C: Char); Var x,y : byte; Begin For Y := Y1 To Y2 Do For X := X1 To X2 Do Screen^[Y,X].Char := Byte(C); {} End;wykonywały się przez 49,22 sekundy, zaś te same procedury napisane w asemblerze - przez 12,80 sekundy, czyli 3,84 raza szybciej. - Dla procedur ScrGetRect i ScrPutRect napisanych w Pascalu w podobny sposób, również za pomocą pętli for, przesyłających jednak całe słowa, a nie bajty, pomiar szybkości dał wynik 79,91 sekundy. Te same procedury napisane w asemblerze wykonywały się przez 13.84 sekundy, a więc 5,75 raza szybciej.
- Procedura Frame w Pascalu rysowała ramki przez 30,16 sekundy, a napisana w asemblerze przez 9,28 sekundy (3,25 raza szybciej).
Wyniki tak przeprowadzonego testu nie są jednak w pełni obiektywne : żaden program nie wykonuje przecież jednej i tej samej operacji w kółko tysiące razy. Poza tym okazało się , że wyniki te znacząco różnią się od siebie w zależności od użytego do testu sprzętu. Wpadłem więc na inny pomysł. Napisałem program, który symulował działanie rzeczywistej aplikacji, tzn. rysował różne okna, wypełniał je wzorkami i tekstem, przesuwał po ekranie w różnych kierunkach (wykorzystując funkcje buforowania obrazu) i oczywiście mierzył czas całej tej zabawy. Tutaj okazało się, że rzeczywisty zysk na szybkości nie jest aż tak wielki, aczkolwiek dostrzegalny gołym okiem. Wynosił on, w zależności od sprzętu, od 3,02 do 3,56 raza. Jest to naprawdę dużo - trzykrotnie szybciej działający program daje przecież takie złudzenie, jakbyśmy zwiększyli częstotliwość taktowania zegara w naszym komputerze powiedzmy z 33 Mhz do 100 Mhz.
Kolejną zaletą prezentowanego modułu jest to, że nie korzysta on z żadnych modułów zewnętrznych, dołączanych poprzez "Uses". W gotowym programie również nie musimy tego robić - mamy przecież do dyspozycji wszystkie mechanizmy potrzebne do zrobienia krótkiego, szybkiego programu. Unikanie dołączenia bibliotek znacznie skraca kod wynikowy, nie jest więc sprawą drugorzędną.
Przyszła pora na wyjaśnienie tajników technicznej realizacji poszczególnych procedur. Starałem się nie używać żadnych sztuczek czy niezrozumiałych konstrukcji, zatem ci, którzy znają choć trochę asembler, nie powinni mieć z niczym problemów. Wszystkie procedury, mające za zadanie zapisać coś na ekran bądź coś z niego odczytać począwszy od danego punktu (x,y), muszą najpierw obliczyć adres tego punktu. Jest to adres elementu tablicy, jednak znający przekład programów pascalowskich na asembler zauważą, że zastosowany tutaj sposób różni się nieco od tego, jaki generuje kompilator Pascala. Ten jest o kilka taktów procesora szybszy, kosztem mniejszej uniwersalności stosowania (w tym przypadku nie gra to roli). Adres początku pamięci ekranu ładowany jest do rejestrów ES:DI (w jednym przypadku do DS:SI), następnie obliczane jest przesuniecie względem początku (ze wzoru [(y-1)*80+x-1]*2) i dodawane do rejestru DI. W funkcjach ScrFillRect i ScrFillAttrRect do rejestru DX wpisuje się obliczoną liczbę wierszy do wypełnienia, a do rejestru CX liczbę elementów w każdym wierszu (jest ona zapamiętywana w BX, aby nie liczyć jej za każdym razem od początku). Następnie w AL umieszczana jest wartość, którą będziemy wypełniać zadany obszar, ustalany jest kierunek wypełniania (CLD) i za pomocą rozkazów operacji na łańcuchach wypełniany jest co drugi bajt w wierszu. Następnie zmniejsza się licznik wierszy (DX), odtwarza licznik elementów w wierszu (CX, pamiętane w BX), w prosty sposób oblicza adres pierwszego elementu w następnym wierszu (DI) i powtarza całą zabawę z wypełnianiem tak długo, aż licznik wierszy (DX) osiągnie zero. W innych procedurach wygląda to podobnie. W ScrGetRect obliczany jest najpierw rozmiar w bajtach danego obszaru na ekranie, następnie przydzielana jest pamięć za pomocą GetMem (duża czekolada dla tego, kto powie mi, jak wywołać GetMem bezpośrednio z kodu asemblerowego). Sprawdzamy teraz, czy aby nasz wskaźnik nie ma wartości nil; tak jak w innych procedurach liczymy adres pierwszego elementu bloku i wszystkie inne dane: liczbę wierszy w DX, elementów w wierszu w CX (pamiętana w BX) i przesyłamy kolejno całe słowa do bufora. W procedurze ScrPutRect wszystko działa w odwrotnym kierunku, dane wędrują z bufora na ekran, na końcu obliczany jest rozmiar bloku i zwalniana jest pamięć (FreeMem). Ciekawa jest procedura Frame. Wędruje ona kolejno po adresach, najpierw zwiększając je, rysuje górny i prawy bok ramki, następnie, zmniejszając, rysuje dolny i lewy bok. WriteXY to nic innego, jak napisane razem GotoXY i PrintStr, nie wymaga więc omówienia. Procedury obsługi drukarki są równie proste. PrnPrintStr ładuje najpierw do CX pierwszy bajt łańcucha, czyli jego długość, a następnie przesyła kolejne bajty do drukarki poprzez funkcję 0 przerwania 17h.
Tu pojawia się problem. Wszystkie te procedury wykonują operacje niskiego poziomu, aby zatem przyśpieszyć czas ich wykonywania, ważne było maksymalne ich uproszczenie. Z tego powodu nie sprawdzają one poprawności przekazywanych argumentów. To programista powinien zadbać o to, by np. wartości y były w przedziale [1...25], a wartości x w przedziale [1...80], oraz o to, by wywołania procedur miały logiczny sens, czyli np. by współrzędne lewego górnego wierzchołka prostokąta nie były większe od współrzędnych prawego dolnego. Szczególną uwagę należy zwrócić na operacje buforowania fragmentów ekranu na stercie. Funkcja ScrGetRect wykonuje alokację dokładnie takiej ilości pamięci, jaka jest potrzebna do zapamiętania określonego fragmentu ekranu. Procedura ScrPutRect zwalnia ten obszar, jednak do obliczenia jego rozmiaru wykorzystuje współrzędne danego prostokąta. Z tego powodu należy zwrócić uwagę na to, by obszar wypełniany procedurą ScrPutRect był taki sam, jak obszar wcześniej pobrany poprzez ScrGetRect. Źle dobrany rozmiar powoduje błędy przy alokacji pamięci.
Moduł pracuje poprawnie zarówno w trybie rzeczywistym procesora, jak i w trybie chronionym. Napisany został przy użyciu kompilatora Borland Pascal 7.0. Kompiluje się bez problemów również Turbo Pascalem 7.0. Przy kompilacji wersją 6.0 mogą wystąpić drobne problemy - powinno je rozwiązać usunięcie słowa const z nagłówków procedur i funkcji.
Na koniec dodam kilka słów na temat korzystania z wbudowanego w kompilator asemblera. Daje on bardzo duże możliwości, nawet mniej zaawansowanemu programiście. W wielu przypadkach zwalnia nas z obowiązku pamiętania o wszystkich szczegółach, z jakimi mamy do czynienia przy pisaniu programu od podstaw w asemblerze. Mimo to jest kilka rzeczy, których znajomość w dużym stopniu upraszcza nasze zmagania z tym niełatwym przecież językiem. We wbudowanym asemblerze najczęściej piszemy całe procedury lub funkcje. Najwygodniej jest wtedy opatrzyć taką procedurę czy funkcję słowem kluczowym assembler i pominąć standardowe begin i end. Kompilator generuje wtedy znacznie krótszy kod, właściwie pozbawiony wszelkich, nie zawsze potrzebnych operacji. Nie musimy także w takim przypadku korzystać z predefiniowanego identyfikatora @Result. Jeśli nasza funkcja ma zwracać wynik, to jest on po prostu zawartością odpowiednich rejestrów. I tak np. w przypadku typów Byte, Char, Boolean po wyjściu z funkcji zwracana jest zawartość rejestru AL, dla typów Integer, Word itp, jest to rejestr AX, dla typów Pointer i Longint - para rejestrów DX:AX, zaś dla typów rzeczywistych: Real - rejestry DX:BX:AX, a charakterystycznych dla koprocesora - ST(0) w koprocesorze. Tylko wtedy, gdy wynikiem funkcji jest dana String, musimy posłużyć się identyfikatorem @Result.
Często podczas pisania procedur w asemblerze zastanawiamy się, do jakiego stopnia możemy wykorzystywać poszczególne rejestry, nie powodując konfliktów z innymi elementami programu. Wbudowany asembler jest pod tym względem wyjątkowo przyjazny. Wolno nam robić wszystko z dowolnymi rejestrami oprócz SS, DS, CS SP i BP. Jakakolwiek procedura zmieniająca ich wartość musi po zakończeniu przywrócić stan początkowy.
Kolejna sprawa to optymalizacja kodu procedur asemblerowych. Jest to niezwykle ważny element, szczególnie w przypadku funkcji wykonujących operacje niskiego poziomu, których wywołania występują w programie po kilkadziesiąt i więcej razy. Starajmy się tutaj, o ile to możliwe, unikać pożerających czas mnożeń; tworząc pętlę, wykorzystujmy wbudowane w tym celu mechanizmy (LOOP, REP itp.); używajmy poleceń operujących na łańcuchach (STOS, MOVS, LODS) zamiast tworzyć samemu skomplikowane konstrukcje. Nawet taki drobiazg, jak np. napisanie XOR DH, DH zamiast MOV DH, 0 daje nam zysk w postaci jednego taktu procesora. Warto również czasami prześledzić przekład naszego programu na asembler za pomocą Turbo Debuggera - widać wtedy wyraźnie, co można usprawnić w funkcji pascalowskiej, przepisując ją na asembler. Pisząc program pracujący w trybie chronionym, oprócz unikania częstych odwołań do różnych bloków pamięci, starajmy się ograniczyć korzystnie z funkcji udostępnianych poprzez przerwania. Pracują one dużo wolniej niż inne funkcje w porównaniu z trybem rzeczywistym.
Mam nadzieję, że przedstawione procedury będą atrakcyjnym dodatkiem do wszystkich rozwiązań mających za zadanie zastąpić cieszący się wątpliwym powodzeniem moduł Crt.
Wydruk 1.
{*** Extended Crt Unit v 2.1 ***}
{*** Borland Pascal 7.0 with Objects ***}
{*** DOS real, DOS protected ***}
{*** Piotr Chlebowicz ***}
{ FRAGMENTY }
Unit ExtCrt;
{$A+, B-, D-, G-, L-, Q-, R-, S-, V-, X+, Y-}
{$IFDEF DPMI}
{$G+}
{$C MOVEABLE DEMANDLOAD DISCARDABLE}
{$ENDIF}
Interface
{$I EXTCRT.INC}
Type pScreenChar = ^tScreenChar;
tScreenChar = Record
Char : Byte;
Attr : Byte;
End;
pScreen = ^tScreen;
tScreen = Array[1..25,1..80] Of tScreenChar;
Var Screen: pScreen;
Procedure Frame(Const x1, y1, x2, y2, thickness: Byte);
Procedure ScrFillAttrRect(Const x1, y1, x2, y2, a: Byte);
Procedure ScrFillRect(Const x1, y1, x2, y2: Byte; Const C: Char);
Procedure WriteXY(Const x, y: Byte; Const Str: String);
Procedure ClrScr;
Function ReadAttr(Const x, y: Byte): Byte;
Function ReadChar(Const x, y: Byte): Char;
Function VideoMode: Byte;
Function ScrGetRect(Const x1, y1, x2, y2: Byte): Pointer;
Procedure ScrPutRect(Var Buf: Pointer; Const x1, y1, x2, y2: Byte);
Procedure PrintScreen; InLine($CD/$05); {int 5h}
Function PrnReset: Byte;
Function PrnGetState: Byte;
Procedure PrnPrintStr(Const Str: String);
Procedure PrnPrintRect(Const x1, y1, x2, y2, AtPosition: Byte);
Procedure InitExtCrt;
Implementation
Procedure Frame(Const x1, y1, x2, y2, thickness: Byte);
Type FrameCharacters = Array[0..5] Of Byte;
Var FrameChr : FrameCharacters;
Const SingleFrame : FrameCharacters = (
196, 179, 218, 191, 192, 217
);
DoubleFrame : FrameCharacters = (
205, 186, 201, 187, 200, 188
);
{ Kody ASCII dla ramek }
Begin
If Thickness = Single Then FrameChr := SingleFrame
Else FrameChr := DoubleFrame;
Asm
LES DI, Screen
MOV AL, y1
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
MOV DL, x1
ADD AX, DX
DEC AX
SHL AX, 1
ADD DI, AX
MOV AL, BYTE PTR [FrameChr + 2]
CLD
STOSB { Pierwszy narożnik }
MOV BL, x2
XOR BH, BH
SUB BL, x1
DEC BX
MOV CX, BX
JCXZ @@2
MOV AL, BYTE PTR [FrameChr]
@@1:
INC DI
STOSB { Górny bok }
LOOP @@1
@@2:
INC DI
MOV AL, BYTE PTR [FrameChr + 3]
STOSB { Drugi narożnik }
MOV DL, y2
XOR DH, DH
SUB DL, y1
DEC DX
MOV CX, DX
JCXZ @@4
MOV AL, BYTE PTR [FrameChr + 1]
@@3:
ADD DI, 9Fh
STOSB { Prawy bok }
LOOP @@3
@@4:
ADD DI, 9Fh
MOV AL, BYTE PTR [FrameChr + 5]
STD
STOSB { Trzeci narożnik }
MOV CX, BX
JCXZ @@6
MOV AL, BYTE PTR [FrameChr]
@@5:
DEC DI
STOSB { Dolny bok ]
LOOP @@5
@@6:
DEC DI
MOV AL, BYTE PTR [FrameChr + 4]
STOSB { Czwarty narożnik }
MOV CX, DX
JCXZ @@8
MOV AL, BYTE PTR [FrameChr + 1]
@@7:
SUB DI, 9Fh
STOSB { Lewy bok }
LOOP @@7
@@8:
End;
End;
Procedure ScrFillAttrRect(Const x1, y1, x2,y2, a: Byte);
Assembler;
Asm
LES DI, Screen
MOV AL, y1
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
MOV DL, x1
ADD AX, DX
DEC AX
SHL AX, 1
ADD DI, AX
MOV AL, y2
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV CL, x2
XOR CH, CH
SUB CL, x1
INC CX
JCXZ @@2
MOV BX, CX
MOV AL, a
XOR AH, AH
CLD
@@1:
INC DI
STOSB
LOOP @@1
DEC DX
JZ @@2
MOV CX, BX
SHL BX, 1
SUB DI, BX
SHR BX, 1
ADD DI, 0A0h
JMP @@1
@@2:
End;
Procedure ScrFillRect(Const x1, y1, x2, y2: Byte; Const c: Char);
Assembler;
Asm
LES DI, Screen
MOV AL, y1
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
MOV DL, x1
ADD AX, DX
DEC AX
SHL AX, 1
ADD DI, AX
MOV AL, y2
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV CL, x2
XOR CH, CH
SUB CL, x1
INC CX
JCXZ @@3
MOV BX, CX
MOV AL, c
XOR AH, AH
CLD
JMP @@2
@@1:
INC DI
@@2:
STOSB
LOOP @@1
DEC DX
JZ @@3
MOV CX, BX
SHL BX, 1
SUB DI, BX
SHR BX, 1
ADD DI, 0A0h
JMP @@1
@@3:
End;
Procedury WriteXY(Const x, y: Byte; Const str: String);
Assembler;
Asm
MOV AH, 2H { GotoXY }
MOV DL, x
MOV DH, y
DEC DL
DEC DH
XOR BH, BH
INT 10h
PUSH DS { PrintStr }
LDS SI, str
CLD
LODSB
XOR AH, AH
XCHG AX, CX
MOV AH, 40h
MOV BX, 1
MOV DX, SI
INT 21h
POP DS
End;
Procedure ClrScr; Assembler;
Asm
MOV AH, 600h
MOV BH, 07h
XOR CX, CX
MOV DX, 184Fh
INT 10h
MOV AH, 2H
XOR DX, DX
XOR BH, BH
INT 10h
End;
Function ReadAttr(Const x, y: Byte): Byte;
Assembler;
Asm
LES DI, Screen
MOV AL, y
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
ADD AL, x
DEC AX
SHL AX, 1
ADD DI, AX
MOV AX, ES:[DI] { Odczyt całego słowa }
MOV AL, AH { zwraca starszy bajt }
End;
Function ReadChar(Const x, y: Byte): Char;
Assembler
Asm
LES DI, Screen
MOV AL, y
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
ADD AL, x
DEC AX
SHL AX, 1
ADD DI, AX
MOV AX, ES:[DI]
End;
Function VideoMode: Byte; Assembler;
Asm
MOV ES, [System.Seg0040]
MOV AL, ES:[49h]
End;
Function ScrGetRect(Const x1, y1, x2, y2: Byte): Pointer;
Var Buf : Pointer;
Size : Word;
Begin
Asm
MOV AL, y2 {Liczy rozmiar podanego bloku }
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV AL, x2
SUB AL, x1
INC AX
MUL DX
SHL AH, 1
MOV Size, AX
End;
GetMem(Buf, Size);
Asm
PUSH DS
LES DI, Buf
MOV AX, ES
OR AX, DI
JZ @@2 { if buf = nil then exit }
LDS SI, Screen
MOV AL, y1
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
MOV DL, x1
ADD AX, DX
DEC AX
SHL AX, 1
ADD SI, AX
MOV AL, y2
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV CL, x2
XOR CH, CH
SUB CL, x1
INC CX
JCXZ @@2
MOV BX, CX
CLD
@@1:
REP MOVSW { przenosi całe słowa }
DEC DX
JZ @@2
MOV CX, BX
SHL BX, 1
SUB SI, BX
SHR BX, 1
ADD SI, 0A0h
JMP @@1
@@2:
POP DS
End;
ScrGetRect := Buf;
End;
Procedure ScrPutRect(Var Buf: Pointer; Const x1,y1,x2,y2: Byte);
Var Size : Word;
Begin
Asm
PUSH DS
LES DI, Screen
LDS SI, Buf
LDS SI, DS:[SI]
{ argument Buf przekazywany jest jako var, }
{ dlatego musi być podwójne wskazanie }
MOV AX, DS
OR AX, SI
JZ @@2
MOV AL, y1
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
MOV DL, x1
ADD AX, DX
DEC AX
SHL AX, 1
ADD DI, AX
MOV AL, y2
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV CL, x2
XOR CH, CH
SUB CL, x1
INC CX
JCXZ @@2
MOV BX, CX
CLD
@@1:
REP MOVSW { przenosi całe słowa }
DEC DX
JZ @@2
MOV CX, BX
SHL BX, 1
SUB DI, BX
SHR BX, 1
ADD DI, 0A0h
JMP @@1
@@2:
POP DS
MOV AL, y2 { liczy rozmiar bloku }
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV AL, x2
SUB AL, x1
INC AX
MUL DX
SHL AX, 1
MOV Size, AX
End;
FreeMem(Buf, Size);
Buf := Nil;
End;
Function PrnReset: Byte; Assembler;
Asm
MOV AH, 1
XOR DX, DX
INT 17h
MOV AL, AH { zwraca status drukarki }
End;
Function PrnGetState: Byte; Assembler;
Asm
MOV AH, 2
XOR DX, DX
INT 17h
MOV AL, AH
End;
Procedure PrnPrintStr(Const Str: String); Assembler;
Asm
PUSH DS
LDS SI, Str
CLD
LODSB { pobiera długość }
MOV CL, AL
XOR CH, CH
JCXZ @@2
XOR DX, DX
@@1:
LODSB
XOR AH, AH
INT 17h
LOOP @@1
@@2:
POP DS
End;
Procedure PrnPrintRect(Const x1,y1,x2,y2,AtPosition: byte);
Assembler;
Asm
PUSH DS
LDS SI, Screen
MOV AL, y1
XOR AH, AH
DEC AX
MOV DX, 50h
MUL DX
MOV DL, x1
ADD AX, DX
DEC AX
SHL AX, 1
ADD SI, AX
MOV AL, y2
XOR AH, AH
SUB AL, y1
INC AX
MOV DX, AX
MOV CL, x2
XOR CH, CH
SUB CL, x1
INC CX
JCXZ @@3
MOV BX, CX
CLD
DEC SI
@@1:
PUSH DX
XOR DX, DX
MOV AL, 1Bh { wysyła do drukarki }
XOR AH, AH { kody sterujące dla }
INT 17h { przesunięcia w prawo }
MOV AL, 66h { do kolumny AtPosition }
XOR AH, AH
INT 17h { kody dla Epsona to hex.: }
XOR AX, AX { 1B, 66, 0, AtPosition }
INT 17h { dla innej drukarki należy }
MOV AL, AtPosition
XOR AH, AH { je zmienić lub usunąć }
INT 17h { cały ten blok }
@@2:
INC SI
LODSB
XOR AH, AH
INT 17h
LOOP @@2
MOV AL, 0A0h { 0A0h - Line Feed }
XOR AH, AH
INT 17h
POP DX
DEC DX
JZ @@3
MOV CX, BX
SHL BX, 1
SUB SI, BX
SHR BX, 1
ADD SI, 0A0h
JMP @@1
@@3:
POP DS
End;
Procedure Error(Const ExitCode: Word);
Begin
Halt(ExitCode);
End;
Procedure InitExtCrt; Assembler;
Asm
CALL VideoMode
CMP AL, 7
JNE @@1
MOV DX, [System.SegB000] { jesli tryb 7 }
JMP @@3
@@1:
CMP AL, 2
JE @@2 { czy to tryb 2 ? }
CMP AL, 3
JE @@2 { a może 3 ? }
MOV AL, 3 { jesli nie, to }
XOR AH, AH { ustawia 3 }
INT 10h
CALL VideoMode
CMP AL, 3
JE @@2
MOV AX, 1 { gdy nie można }
PUSH AX { ustawić trybu 3 }
CALL Error { to wywołuje Error }
@@2:
MOV DX, [System.SegB800]
@@3:
XOR AX, AX
MOV WORD PTR [Screen], AX { ustawia wskaźnik }
MOV WORD PTR [Screen+2], DX
End;
Begin
InitExtCrt;
End.
--- * --- * ---
Wydruk 2.
{ EXTCRT.INC - Deklaracje stałych dla ExtCrt }
Const { grubosć ramki }
Single = 1;
Double = 2;
{ Kody sterujące drukarki (standard Epson) }
PrnDraft = #27#120#0;
PrnNLQ = #27#120#1;
PrnSelectNLQFont = #27#107;
{ wymaga przesłania dodatkowego argumentu }
PrnStandard = #27#53#27#70#27#72;
PrnItalic = #27#52
PrnSemiBold = #27#69
PrnBold = #27#71;
PrnNoItalic = #27#53;
PrnNoSemiBold = #27#70;
PrnNoBold = #27#72;
PrnUnderline = #27#45#1;
PrnNoUnderline = #27#45#0;
PrnSuperScript = #27#83#0;
PrnSubScript = #27#83#1;
PrnNoSuperSubScript = #27#84;
Prn10Cpi = #27#80;
Prn12Cpi = #27#77;
PrnCondensed = #15;
PrnProportional = #27#112#1;
PrnNoCondensed = #18;
PrnNoProportional = #27#112#0;
PrnExpanded = #27#87#1;
PrnNoExpanded = #27#87#0;
PrnNormalSize = #27#104#0;
PrnDoubleSize = #27#104#1;
PrnQuadrupleSize = #27#104#2;
PrnNormalHeight = #27#119#0;
PrnDoubleHeight = #27#119#1;
PrnLineFeed = #10;
PrnRevLineFeed = #27#10;
PrnFeedXLines = #27#102#1
{ wymaga przesłania dodatkowego argumentu }
PrnFormFeed = #12;
PrnCarriageReturn = #13;
PrnBackSpace = #8;
PrnDisablePaperOutDetector = #27#56;
PrnEnablePaperOutDetector = #27#57;
PrnOffLine = #19;
PrnOnLine = #17;
PrnBell = #7;
PrnBiDirectional = #27#85#0;
PrnUniDirectional = #27#85#1;
PrnResetPrinter = #27#64;
{ kody fontów drukarki }
FntCourier = #0;
FntSanSerif = #1;
FntOrator1 = #7;
FntOrator2 = #8;
FntDraft = #9;
{ kody statusu drukarki }
PrnTimeOut = $01;
PrnError = $08;
PrnSelected = $10;
PrnPaperOut = $20;
PrnReady = $40;
PrnNotBusy = $80;
--- * --- * ---
Wydruk 3.
Program Demo;
{ Bardzo prosty przykład wykorzystania modułu }
{ ExtCrt. Program rysuje okienko i za pomocą }
{ funkcji buforowania obrazu umożliwia jego }
{ przesuwanie po ekranie. Enter kończy pokaz. }
Uses ExtCrt;
Var BackGround, Window : Pointer;
x1, y1, x2, y2 : Byte;
k : Word;
Const kbUp = $4800; { najprostsza obsługa klawiatury }
kbDown = $5000;
kbLeft = $4B00;
kbRight = $4D00;
kbEnter = $1C0D;
Function ReadKey: Word; Assembler;
Asm
XOR AH, AH
INT 16h
End;
Procedure Desktop;
Begin
ScrFillRect(1,1,80,25,'_'); { rysowanie tła }
ScrFillAttrRect(1,1,80,25,blue*16+yellow);
Frame(8,3,24,6,Single);
ScrFillRect(9,4,23,5,' ');
WriteXY(10,4,'Okienko 1');
ScrFillAttrRect(8,3,24,6,green*16+white);
Frame(59,12,78,19,double);
ScrFillRect(60,13,77,18,'.');
WriteXY(63,13,'Okienko 2');
ScrFillAttrRect(59,12,78,19,black*16+lightred);
Frame(2,14,65,23,single);
ScrFillRect(3,15,64,22,'#');
WriteXY(4,15,'Okienko 3');
ScrFillAttrRect(2,14,65,23,brown*16+lightgray);
Frame(35,2,56,12,double);
ScrFillRect(36,3,55,11,' ');
WriteXY(37,3,'Okienko 4');
ScrFillAttrRect(35,2,56,12,magenta*16+lightred);
Frame(37,5,51,10,double);
Scr(FillRect(38,6,50,9,'&');
WriteXY(39,7,'Okienko 5');
ScrFillAttrRect(37,5,51,10,red*16+lightblue);
BackGround := ScrGetRect(x1,y1,x2,y2);
{ zapamiętanie tego co pod spodem okienka }
Frame(x1,y1,x2,y2, double);
{ rysowanie okienka }
ScrFillRect(x1+1, y1+1, x2-1, y2-1, ' ');
WriteXY(x1+3, y1+2, 'Okienko');
WriteXY(x1+3, y1+3, 'testowe');
ScrFillAttrRect(x1,y1,x2,y2,black*16+magenta);
ScrFillAttrRect(x1+1,y1+1,x2-1,y2-1,black*16+white);
WriteXY(66,25,'ENTER - koniec');
End;
Procedure MoveLeft;
Begin
If x1 = 1 Then Exit;
Window := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(BackGround, x1,y1,x2,y2);
Dec(x1);
Dec(x2);
BackGround := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(Window,x1,y1,x2,y2);
End;
Procedure MoveRight;
Begin
If x2 = 80 Then Exit;
Window := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(BackGround,x1,y1,x2,y2);
Inc(x1);
Inc(x2);
BackGround := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(Window,x1,y1,x2,y2);
End;
Procedure MoveUp;
Begin
If y1 = 1 Then Exit;
Window := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(BackGround,x1,y1,x2,y2);
Dec(y1);
Dec(y2);
BackGround := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(Window,x1,y1,x2,y2);
End;
Procedure MoveDown;
Begin
If y2 = 25 Then Exit;
Window := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(BackGround,x1,y1,x2,y2);
inc(y1);
inc(y2);
BackGround := ScrGetRect(x1,y1,x2,y2);
ScrPutRect(Window,x1,y1,x2,y2);
End;
Begin
x1 := 5;
y1 := 5;
x2 := 17;
y2 := 11; (współrzędne początkowe okienka }
Desktop;
Repeat
K := ReadKey;
Case K Of
kbLeft : MoveLeft;
kbRight : MoveRight;
kbUp : MoveUp;
kbDown : MoveDown;
End;
Until K = kbEnter;
ScrPutRect(BackGround,x1,y1,x2,y2);
ClrScr;
End.
{}
Piotr Chlebowicz, PC Kurier 03/95, 2 Lutego 1995r.