QBS >> Elementy standardowe >> Q-Tenberg >> Podręcznik do Q-Tenberg'a

QVL - język Q-Tenberga

 

Podręcznik do QVL-a, czyli Q-Line Virtual Language

wykorzystanego w Q-Tenberg'u.


Wstęp |  Wypisywanie danych |  Rekordy |  Tabele |  Struktura Kodu
Zawężanie |  Grupowanie |  Lookupy |  Inne funkcje
Przykład łatwy |  Przykład trudniejszy |  Własne obiekty |  Null

Wstęp

Jeżeli nie wiedzą Państwo co to jest Q-Tenberg, proszę kliknąć tu. Dopiero po zapoznaniu się z podręcznikiem do generatora raportów Q-Tenberg można przystąpić do poznawania jego języka QVL.

W pierwszej kolejności powiemy na czym polega robienie wydruków (głównie raportów i zestawień), potem prześledzimy dwa przykłady: łatwy i trudniejszy. Oprócz podręcznika dostępna jest jeszcze dokumentacja QVL-a, w której podaliśmy klasy i ich metody. Zarówno dokumentacja i podręcznik nie opisują wszystkich możliwośći (w szczególności wszystkich funkcji) języka, ma tylko przybliżyć robienie trudniejszych wydruków.

Wstawianie danych na wydruk

Zakładamy, że potrafią Państwo już tworzyć w Q-Tenberg'u pola tekstowe, obrazki i tabelki. Jeśli nie, proszę kliknąć tu.
Do wstawiania danych do tabelki używa się czterech funkcji:

Zapis Buffer.set() oznacza tu, że wywołujemy funkcję (poprawnie: metodę) na rzecz obiektu klasy Buffer. Podstawowy obiekt klasy Buffer dostajemy jako pierwszy parametr funkcji main (zazwyczaj nazywa się mainBuf). Aby wyciągnąć z niego "podbufor" (dziecko mainBuf-a) używamy funkcji getBuffer. Jest to opisane w piątej lekcji Q-Tenberga.

Metoda set wstawia po prostu dane do bufora

Metoda put wstawia dane do bufora i je fiksuje. Od tej chwili nie będzie można już wstawić do tego bufora innych danych. Jeżeli do tabelki-listy spróbujemy wstawić putem dane do zajętego już bufora, spowoduje to dodanie nowego wiersza tabelki.

Metoda set przydaje się, kiedy np. wstawiamy wszędzie jakieś dane domyślne, a potem w zależności od różnych warunków, może będziemy je zmieniać. Rada: czasami jeśli w wydruku coś się chrzani z polami, warto zamienić puta na seta lub odwrotnie. Q-Tenberg jest nieprzewidywalny. (To chyba powinno być motto tej rozprawy).

Metody setLine() i putLine() to pójście w stronę user-friendly software. Do metod tych zazwyczaj podajemy jako argument rekord pełen danych. Jeżeli pola rekordu nazywają się tak jak bufory we fragmentach, Q-Tenberg wszystkie (albo jak najwięcej się da) wstawi je hurtem. Nie musimy zatem pisać

	
	mainBuf.getBuffer("S.SPECYFIKACJA.TAB.LINE1.IMIE").put(moj_rekord.get("IMIE", ""));
	mainBuf.getBuffer("S.SPECYFIKACJA.TAB.LINE1.NAZWISKO").put(moj_rekord.get("NAZWISKO", ""));
	mainBuf.getBuffer("S.SPECYFIKACJA.TAB.LINE1.MIASTO").put(moj_rekord.get("MIASTO", ""));
a możemy
	mainBuf.getBuffer("S.SPECYFIKACJA.TAB.LINE1").putLine(moj_rekord);
o ile oczywiście moj_rekord ma pola IMIE, NAZWISKO I MIASTO.

Rekordy

W przykładzie w poprzednim rozdziale użyłem zmiennej moj_rekord. Była to zmienna klasy Record. Znajomość lasy Record jest niezbędna do pisania w QVL-u.
Rekord możemy tworzyć na dwa sposoby:

  1. Jako pusty: Record rec := new Record();
  2. Albo na bazie tabeli z qconów: Record rec2 := new Record("TABELA");
Po takim stworzeniu rekordu, w żadnym z nich nie ma danych. Jednak rec2 ma już pola takie jak TABELA (tyle samo, o takich samych nazwach i tego samego typu). Ważne jest, aby stosotawać odpowiednie konstruktory rekordu w odpowiednim czasie (wskazówki później).

Aby do rekordu wprowadzić dane, używamy funkcji put.

	rec.put("NAZWISKO", "Hyska");
Jeżeli rekord nie miał jeszcze pola "NAZWISKO", zostanie ono stworzone. I tutaj ważne jest rozróżnienie między dwoma konstruktorami: jeśli rekord wzięty był z tabeli, ma on potrzebne nam pole i jest zdefiniowany typ danych w typ polu (np. money). Jeżeli przy dodawaniu pola nie było, jest ono tworzone jako Object. Inaczej wtedy można rekordy porównywać, sumować itp.
Aby pobrać dane z rekordu, wywołujemy metoda get():
	mainBuf.getBuffer("S.SPECYFIKACJA.TAB.LINE1.IMIE").put(moj_rekord.get("IMIE"));
Powinno się jednak używać metody get(2):
	mainBuf.getBuffer("S.SPECYFIKACJA.TAB.LINE1.IMIE").put(moj_rekord.get("IMIE",""));
Drugi parametr geta, to wartość domyślna, jak ma zostać zwrócona, gdyby pobierana okazała się nullem. Tutaj jest to pusty string "". Jednak pusty string to zupełenie co innego niż null (w szczególności będzie można go wypisać). W praktyce bardzo często się zdarza, że nie wszystkie dane w tabelach są wypełnione i chcąc coś wypisać trafiamy na nulle. Dlatego radzę zawsze używać tej dwuargumentowej wersji geta.

Jeszcze co do nulli: na razie nie ma w QVL-u stałej NULL. Aby coś porównać z nullem, można użyć wybiegu, o jakim powiedział mi KKW: Tworzymy pusty rekord my_null i my_null.get("xxx") zwraca nam nulla.

Rekord można stworzyć jeszcze w trzeci sposób: poberając go z tabeli:

	Record r:=nasza_tabela.get();
Wtedy ma on oczywiście też takie same pola jak tabela, ma też dane z aktualnego rekordu tej tabeli.

Tabele

Table to też jedna z podstawowych klas z jakich będziemy korzystać. Aby stworzyć tabelę piszemy:

	Table tabelka := new table("TABELA");
gdzie TABELA jest nazwą tabeli z naszego programu (z qconów).

Aby skorzystać z tabeli, należy ją otworzyć. Tabelę otwiera się zawsze z jakimś uporządkowaniem. Można do tego wykorzystać uporządkowanie utworzone przez nas w qconach

	tabelka.open("NAZWISKO");
ale powinno się tego unikać. Może się bowiem zdarzyć, że ktoś zmieni uporządkowania w qconach i cały wydruk nam się popsuje. Lepiej jest uniezależniać od siebie moduły, tak aby błedy się nie rozprzestrzeniały. Dlatego należy uporządkowanie tworzyć sobie w Qetnbergu samemu i wykorzystywać je do otwierania tabeli. Uporządkowanie tworzymy poprzez obiekt klasy Array. Wydląda to mniej więcej tak:
	Table tabelka := new table("TABELA");    //tworzymy tabelę
	Array orders := new Array();             //tworzymy tablicę uporządkowań
	orders.add("NAZWISKO");                  //do tablicy uporządkowań dodajemy nazwisko
	orders.add("IMIE");                      //do tablicy uporządkowań dodajemy imię
	orders.add("DATA_UR");                   //do tablicy uporządkowań dodajemy datę urodzenia
	tabelka.open(orders);                    //otwieramy tabelę posortowaną po nazwisku, imieniu i dacie urodzenia.
Teraz możemy już biegać po tabelce. W Q-Tenbergu tabelę przegląda się sekwencyjnie. Po otwarciu jest ona ustawiona na pierwszym rekordzie. Jeśli chcemy ustawić się na ostatnim, jest metoda setLast(). Jeśli chcemy przeskoczyć do pierwszego rekordu, setFirst().
Zazwyczaj po tabeli biega się po to, aby wyciągać z niej rekordy. Jak to zrobić, pokazałem wcześniej. Zawsze jednak przed pobraniem rekordu trzeba sprawdzić czy aktualny rekord istnieje. Do tego jest funkcja Table.isCurrent(). Aby przejść do następnego rekordu używa się Table.next(), do poprzedniego Table.prev(). W praktyce prawie zawsze sprawdza się taki schemat:
	tabelka.open(orders);
	while (tabelka.isCurrent())
	{
		Record r := tabelka.get();
		//wypisz i zrób-co-tam-chcesz z rekordem
		tabelka.next();
	}
	tabelka.close();
Jak łatwo się zorientować, na końcu jest funkcja zamykająca tabelę.
Do tabel jeszcze wrócimy, tymczasem...

Ogólna struktura kodu QVL

Kiedy drukowany jest wydruk, Q-Tenberg wywołuje funkcję main(Buffer mainBuf, Record current, Record recEd, Table entryTable).

mainBuf to główny bufor, z którego możemy wyciągać dzieci.
current to rekord na rzecz którego wywołane jest drukowanie. Jeżeli drukujemy coś spod browsera (np. dokumenty), current jest rekordem z browsera na którym staliśmy.
recEd to tab parametrów wydruku (zazwyczaj przy raportach)
entryTable to tabela na jakiej wywołaliśmy wydruk (spod browsera). Jest ona posortowana i pozawężana jak w browserze, wymaga tylko otwarcia entryTable.open().

Parametry recEd, current i entryTable są opcjonalne, ale żeby ich używać muszą się dokładnie tak nazywać (liczą się nazwy, a nie kolejność, tak jak to jest w C)

Parę faktów o QVL-u:

Ostatni punkt to specjalna konstrukcja wymyślona do Q-Tenberga. Wygląda tak:
afterbreak
{
	//najczęściej dużo wypisywania do tabelki-listy
}
do
{	
	//zazwyczaj wypisz podsumowanie strony
}
w bloku afterbreak piszemy jak program wypełnia kolejne wiersze tabelki, w bloku do piszemy co ma zrobić w momencie łamania strony.

Więcej o tabelach - zawężanie

Zazwyczaj nie chcemy po prostu wypisywać posortowanej tabeli, chcemy ograniczyć obrabiane rekordy jakimś warunkiem. Możnaby używać if-a, ale byłoby to bardzo nieefektywne. Jeśli nasz warunek dotyczy pola, które jest w danej tabeli, całe zawężanie można przeprowadzić za pomocą jednej operacji: Table.setNarrowTable(Record, int) Żeby tabelę można było zawęzić, musi ona być posortowana po polach, po których chcemy zawężać. Ważna jest kolejność uporządkowań (kolejność dodawania pól do tablicy uporządkowań). W tablicy uporządkowań (u nas Array orders) muszą być wszysktie pola po których zawężamy, i to muszą być pokolei i na początku.

	Table tabelka := new table("TABELA");    
	Array orders := new Array();             
	orders.add("NAZWISKO");                  
	orders.add("IMIE");                      
	orders.add("DATA_UR");                   
	tabelka.open(orders);                    
	Record narRec := new Record("TABELA");
	narRec.put("NAZWISKO", "Kowalski");
	narRec.put("IMIE", "Adam");
	tabelka.setNarrowTable(narRec, 2);
W ten sposób dostajemy tabelę zawierającą tylko Adasiów Kowalskich posortowaną po dacie urodzenia. Przy okazji operacja ta ucina z tabeli pola po których zawężaliśmy (bo przecież i tak wszyscy są Adasie Kowalscy).

Jak widać powyżej, do zawężenia tabeli używamy wzorcowego rekordu. Tworzymy go na bazie tabeli, którą będziemy zawężać (to ważne!) i wrzucamy w jego pola wartości jakie chcemy.

Drugim parametrem setNarrowTable jest pole po ilu polach zawężać.
Ponieważ często w wydrukach dostępne są różne zawężąnia (np. można zawężać imionach ale nie trzeba), a posortowanie musi dokładnie odpowiadać zawężaniu, trzeba to dobrze wszytko obsłużyć. Bardziej wyrafinowane zawężania są w przykładzie.

Często chcemy zawężać rekordy nie według wzorca i relacji równości, ale także "od-do". Dobrym przykładem jest wybranie ludzi urodzonych w jakimś okresie. Do tego używamy metody

setBeginEndTable(Record beginRec, Record endRec, int n).
Metoda ta ogranicza tabelę rekordami beginRec i endRec. Obowiązuje tu ten sam rygor uporządkowania tablicy co w setNarrowTable. Rekordy beginRec i endRec nie muszą się różnić we wszystkich polach. W skrajnym przypadku (kiedy będą takie same) dostaniemy ten sam wynik co w zwykłym zawężaniu.

Uwaga! Jeśli jednym z pól tych rekordów jest data (różne daty) to powinna ona być dodana jako ostatnia w sortowaniu. Najprościej na przykładzie:

	Table tabelka := new table("TABELA");
	Array orders := new Array();
	orders.add("NAZWISKO");     
	orders.add("IMIE");         
	orders.add("DATA_UR");      
	tabelka.open(orders);       
	Record beginRec := new Record("TABELA");
	Record endRec := new Record("TABELA");
	beginRec.put("NAZWISKO", "Kowalski");
	beginRec.put("IMIE", "Adam");
	beginRec.put("DATA_UR", jakas-tam-data);
	endRec.put("NAZWISKO", "Kowalski");
	endRec.put("IMIE", "Adam");
	endRec.put("DATA_UR", jakas-inna-data);
	tabelka.setBeginEndTable(beginRec, endRec, 3);
Dostajemy tabelkę Adasiów Kowalskich urodzonych pomięszy podanymi datami.
setBeginEndTable nie obcina pól po których zawężaliśmy.

Grupowanie

Grupowanie przebiega w QVL-u bardzo prosto. Należy naszą tabelę otworzyć w uporząkowaniu po polach, po których chcemy grupować. Stosujemy się tu do tych samych zasad jak w przypadku zawężania - tabela musi byc posortowana po wszytkich polach po kolei od początku. Grupowanie ma dodatkową funkcję - potrafi dodawać wybrane pola dla grupy.

Operacji grupowania dokonujemy poprzez metodę setGroupTable(Array po_czym_grupwać, Array co_sumować). Parametami są tu tablice z nazwami pól o które nam chodzi. Do tabicy po_czy_grupować wrzucamy nazwy pól z tabeli po których grupujemy, do tablicy co_sumować wrzucamy nazwy pól ktore sumujemy dla każdej grupy. W przykładzie mamy tabelę WYDATKI_NA_CIASTKA zawierającą wszystkie nasze zakupy w cukierni (datę, nazwę ciastka, ile wydano itp.). Po pogrupowaniu dostaniemy sumę wydanych przez nas pieniędzy na poszczególne rodzaje ciastek:

	Table wydatki_na_ciastka := new table("WYDATKI_NA_CIASTKA");
	Array orders := new Array();
	orders.add("NAZWA_CIASTKA");
	wydatki_na_ciastka.open(orders);
	Array po_czym_grupowac := new Array();
	po_czym_grupowac.add("NAZWA_CIASTKA");
	Array co_sumowac := new Array();
	co_sumowac.add("ILE_WYDANO");
	wydatki_na_ciastka.setGroupTable(po_czym_grupwac, co_sumowac);
I mamy już, że na szarlotki wydaliśmy w sumi 24 zł, a na pączki 36 zł. Oczywiście można to było jeszcze zawęzić tylko do jakiś dat itp.

Czasami chcemy coś grupować na podstawie pól z innej tabeli niż nasza otwarta. Wtedy zazwyczaj nie da się tego załatwić jedną instrukcję, trzeba pieczołowicie przejść przez tabelę i grupować w pamięci funkcją memGroupTable, ale o tym później.

Lookup'y

Czasami bardzo wygodne jest, jeśli możemy do tabeli dostawić na chwilę jakieś pole (kolumnę). Dzięki temu jednym posunięciem mamy przyporządkowane nowe pole, nie musimy dla każdego rekordu sięgać do bazy. Do prostego lookup'u służy funkcja

addOneSimpleLookup(AutFunString z_jakiej_tabeli, AutFunString klucz_wlasny, AutFunString klucz_obcy, AutFunString nazwa_zrodlowa, AutFunString nazwa_docelowa)

Nazwy parametrów dużo mówią:
Przy lookup'ie Q-Tenberg sięga do tabeli z_jakiej_tabeli, szuka tam rekordu o ID równym klucz_obcy, dopasowuje do naszego rekordu z ID równym klucz_własny i ściąga pole nazwa_źródłowa do naszej tabeli z nazwą nazwa_docelowa. Proste, prawda? Na przykładzie:

	Table wlasciciele := new Table("WLASCICIELE_PSOW");
	wlasciciele.addOneSimpleLookup("PSY", "ID", "ID_WLASCICIELA", "NAZWA", "NAZWA_PSA");
	wlasciciele.open(orders);
Mamy tabelę psów i tabelę właścicieli. Aby nie biegać po tabeli właścicieli i nie szukać zawsze dla nich psów, można dodać do tabeli pole "NAZWA_PSA". Robimy tak jak pokazano powyżej. Od tej chwili w tabeli przybyła kolumna z odpowiednią nazwą ściągniętą z innej tabeli.
Uwaga! Nie można zawężać po polu dodanym lookup'em.

Inne przydatne funkcje

A co, jeśli nie da się zrobić Lookup'u, a chcemy zajrzeć do innej tabeli? W SQL-u jest SELECT, tutaj do wyjęcia pojedynczego rekordu możemy użyć dwóch funkcji:

Record findRecord(str nazwa_tabeli, str nazwa_pola, str wartość);

var gv(str nazwa_tabeli, str nazwa_pola, str wartość, str które_pole);

Obie funkcje są napisane już w QVL-u i są zazwyczaj dostępne w pliku qvl.h (który trzeba zainkludować). Jeśli ich nie ma w Waszym programie, są w nalepszym na rynku programi do obsługi sprzedaży, Toradze.

findRecord zwraca jeden rekord z Tabeli nazwa_tabeli, dla którego pole nazwa_pola ma wartość wartość.

Funkcja gv (chyba od get value) zwraca wprost wartość takiego rekordu w polu które_pole.

Jeżeli nie znalibyśmy lookupów to nasz przykład z psami wyglądałby tak:

	Table wlasciciele := new Table("WLASCICIELE_PSOW");
	Array orders:= new Array();
	orders.add("NAZIWSKO");
	wlasciciele.open(orders);
	while (wlasciciele.isCurrent())
	{
		Record r := wlasciciele.get();
		str nazwa_psa := gv("PSY", "ID_WLASCICIELA", r.get("ID"), "NAZWA");
		// cos tam;
		wlasciciele.next();
	}
	wlasciciele.close()

Teraz warto prześledzić dwa przykłady wydruków: łatwy i trudniejszy.

Własne obiekty

W wydrukach w Q-Tenbergu możemy używać własnych, napisanych przez siebie obiektów. Często trudne obliczenia łatwiej jest zaprogramować w javie, działa to pewniej i szybciej. Aby w kodzie QVL używać własnych obiektów, należy wykonać 2 rzeczy:

  1. Napisać obiekt. Najlepiej, żeby jego nazwa zaczynała się przedrostkiem EO_, np. EO_MojaKlasa
  2. Zaimportować go w kodzie wydruku poprzez polecenie import.

Z poziomu kodu QVL (z wydruku) widoczne są tylko metody publiczne, które zaczynają się przedrostkiem qtg_.

Poniżej przykład z programu Q-PHU-3000, wydruk noty odsetkowej. Nota odsetkowa to dokument, który wyszczególnia karne odsetki od faktury zapłaconej po terminie. Ponieważ obowiązujące oprocentowanie zmienia się w czasie, niełatwo jest wyliczyć sumę odsetek. Do tego celu Krzysiek napisał obiekt EO_OverDueCalculator, który jest w pakiecie toraga.notyOdsetkowe.

Importujemy zatem obiekt:

import toraga.notyOdsetkowe.EO_OverDueCalculator;
i możemy go później używać, np:
EO_OverDueCalculator calc := new OverDueCalculator(today,"STOPY_NOT_ODSETKOWYCH");
calc.addPayment(payrec.get("DATAW"),payrec.get("KWOTAW"));

Klasa EO_OverDueCalculator, której używamy wygląda mniej więcej tak:

package toraga.notyOdsetkowe;

public class EO_OverDueCalculator
{
/* .... */

    public void qtg_setInvoice(AutFunDate a_invDueDate,AutFunMoney a_invAmount)
    {
        calc.setNewInvoice(a_invDueDate.getDate(),a_invAmount.getMoney());
    }

    public void qtg_addPayment(AutFunDate a_payDate,AutFunMoney a_amount)
    {
        calc.addPayment(a_payDate.getDate(),a_amount.getMoney());
    }

    public void qtg_addFillUp()
    {
        calc.addFillUp();
    }
    public AutFunMoney qtg_getCharge()
    {
        return new AutFunMoney(calc.getCharge());
    }

/* i tak dalej... */
}

Null

Niestety w QVL-u nie ma stałej null. Jeżeli jest nam taka potrzebna, obchodzimy to następującym trikiem:

Record temp := new Record();
var myNull := temp.get("NIE_MA_TAKIEGO_POLA");