Tekninen artikkeli

Tietotaulukon renderöinti PDF:ksi HotPDFillä

Dataset koostuu riveistä ja sarakkeista, mutta PDF-sivu on tyhjä koordinaattiruudukko ilman kumpaakaan käsitettä. HotPDFissä ei ole DrawTable-kutsua valmiin ruudukon tuottamiseen. Taulukko rakennetaan primitiiveistä: TextOut sijoittaa tekstin, SetFont valitsee fontin, Rectangle ja Fill varjostavat alueen ja MoveTo, LineTo sekä Stroke piirtävät viivat. Luotettava viejä muuntaa rivi- ja sarakemallin eksplisiittisiksi X/Y-koordinaateiksi ja ylläpitää niitä datan ylittäessä sivun alareunan

Seuraava esimerkki raportoi asiakastietueita, mutta mikään piirtokoodissa ei tiedä tai välitä, mistä rivit tulevat. Alkuperäinen käytti vanhaa TTable-komponenttia; FireDAC-kysely, muistin sisäinen tietojoukko tai tavallinen tietuetaulukko (array of records) syöttää samoja rutiineja muuttamattomina. Tärkeintä on, että voit käydä datan läpi rivi kerrallaan ja lukea jokaisesta neljä merkkijonokenttää. Pidä renderöinti erillään tietolähteestä, ja voit muuttaa kumpaakin puolta häiritsemättä toista

Sarakkeiden geometria tulee ensin

Ennen kuin yhtäkään merkkiä on piirretty, päätä, missä kukin sarake asuu. Tässä taulukossa on neljä saraketta, joten se tarvitsee neljä vasenta reunaa ja tunnetun oikean marginaalin. Maagisen numeron koodaaminen kiinteästi jokaiseen TextOut-kutsuun, kuten pikaisilla esimerkeillä on tapana, on juuri se, mikä tekee taulukon leventämisestä myöhemmin tuskallista. Nimeä reunat kerran pisteinä (points) vasemmasta alakulmasta (origo), ja jokainen piirtokutsu viittaa niihin nimeltä:

const
  ColNo   = 70;    // left edge of the "No." column
  ColName = 110;   // company name
  ColAddr = 300;   // street address
  ColCity = 480;   // city
  RowLeft = 50;    // table frame: left rule
  RowRight = 570;  // table frame: right rule
  RowStep = 20;    // vertical distance between baselines

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // A shaded band behind the row. Rectangle takes X, Y, Width, Height.
    Page.SetRGBFillColor($00FFF3DD);
    Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
    Page.Fill;
    Page.SetRGBFillColor(clBlack);
  end;
  Page.TextOut(ColNo,   Y, 0, ANo);
  Page.TextOut(ColName, Y, 0, AName);
  Page.TextOut(ColAddr, Y, 0, AAddr);
  Page.TextOut(ColCity, Y, 0, ACity);
end;

Kaksi yksityiskohtaa ansaitsevat paikkansa tässä. Varjostettu nauha piirretään ensin, sitten teksti sen päälle, koska maalausjärjestys on z-järjestys PDF:ssä: täytä suorakulmio tekstin jälkeen, ja hautaat rivin. Vaihteleva varjostus ei myöskään ole koriste vain sen itsensä vuoksi. Tiheässä raportissa se on halvin tapa estää silmää liukumasta väärälle riville, minkä vuoksi silmukka myöhemmin kääntää totuusarvon (boolean) jokaisella rivillä ja välittää sen suoraan Shaded-parametrille

Yllä olevat sarakkeiden sijainnit ovat kiinteät, mikä on reilua raportille, jonka skeemaa hallitset. Kun data on muuttuvaa, mittaa arvaamisen sijaan. HotPDF paljastaa tekstin leveyden mittauksen sivuobjektissa, joten PrintRow:n tuotantoversio voi ottaa pisimmän odotetun arvon kussakin sarakkeessa, mitata sen kerran valitulla fonttikoolla ja johtaa vasemmat reunat näistä leveyksistä plus välistä (gutter). Rutiinin muoto ei muutu; vain vakioiden lähde muuttuu

Otsikko, viivat ja yksi paikka, joka omistaa ne

Taulukko, joka jatkuu sivun yli ja jatkuu seuraavalla ilman sarakeotsikoita, on lukukelvoton. Korjaus on käsitellä otsikkoa asiana, jonka piirrät uudelleen, ei asiana, jonka piirrät kerran. Laita sarakkeiden otsikot ja niitä kehystävät vaakaviivat yhteen rutiiniin, ja kutsu tuota rutiinia sekä alussa että uudelleen joka kerta, kun avaat uuden sivun. Koska otsikko ja runko jakavat samat sarakevakiot, ne kohdistuvat toisiinsa rakenteensa ansiosta

procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Left: source label and page number. Right: generation time.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Two horizontal rules that box the column titles.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // The column titles, in a heavier face so they read as headings.
  Page.SetFont('Times New Roman', [fsBold], 12);
  Page.SetRGBFillColor(clNavy);
  PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
  Page.SetRGBFillColor(clBlack);

  Y := Y + RowStep + 45;  // advance past the boxed header before the first body row
end;

Huomaa, että DrawHeader ottaa Y:n viitteenä (by reference) ja siirtää sitä eteenpäin. Kutsujan ei koskaan tarvitse muistaa, kuinka korkea otsikko on; rutiini, joka piirtää sen, on rutiini, joka tietää. Tämä yhden omistajan sääntö pitää asettelun ajelehtimasta, kun myöhemmin lisäät logon tai suodattimen yhteenvedon otsikkonauhaan. Runkosilmukka pysyy tietämättömänä. Se jatkaa vain rivien piirtämistä siitä, mihin Y tällä hetkellä osoittaa

Itse viivat ovat ero luettelon ja taulukon välillä. Pystysuorat sarake-erottimet ovat sama idea sovellettuna x-akseliin: MoveTo / LineTo / Stroke kunkin sarakkeen reunassa, vedettynä yläviivasta sivun viimeisen rivin pohjaan. Esimerkki pitäytyy vaakaviivoissa pysyäkseen luettavana, mutta tuotantovaihe on mekaaninen, kunhan sarakevakiot ovat olemassa

Kohdistinsilmukka omistaa sivunvaihdon

Piirtäminen on helppo puolisko. Se puolisko, joka erottaa lelun raportista, on sivutus (pagination): sen tietäminen, ennen kuin piirrät rivin, mahtuuko se vielä, ja uuden sivun aloittaminen tuoreella otsikolla, kun se ei mahdu. Tämä päätös kuuluu tasan yhteen paikkaan, datan läpikäyvään silmukkaan, eikä minnekään muualle

var
  Pdf: THotPDF;
  Page: THPDFPage;
  Y: Single;
  PageNo: Integer;
  Shaded: boolean;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'CustomerReport.pdf';
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;

    // Report title, once, at the top of the first page.
    Page.SetFont('Arial', [fsBold], 24);
    Page.TextOut(200, 800, 0, 'Customer Report');

    PageNo := 1;
    Y := 760;
    DrawHeader(Page, Y, PageNo);
    Shaded := False;

    CustomerTable.First;
    while not CustomerTable.Eof do
    begin
      // Out of room? Open a new page and repeat the header there.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage moves CurrentPage forward
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont must be reissued on every new page
      PrintRow(Page, Y,
        VarToStr(CustomerTable['CustNo']),
        VarToStr(CustomerTable['Company']),
        VarToStr(CustomerTable['Addr1']),
        VarToStr(CustomerTable['City']),
        Shaded);

      Y := Y - RowStep;
      CustomerTable.Next;
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Kaksi koordinaattitietoa ohjaa koko silmukkaa. PDF mittaa y:tä ylöspäin vasemmasta alakulmasta, joten rivit marssivat alaspäin sivua pitkin vähentämällä RowStep:n Y:stä joka kerta, ja sivu-täynnä-testi laukeaa, kun Y putoaa alamarginaalin alle pikemminkin kuin jonkin yläreunan yläpuolelle. Jos suunta on väärin, ensimmäinen rivi tulostuu alareunan ulkopuolelle samalla, kun silmukka luulee, että sillä on koko sivu tilaa

Toinen tieto nappaa melkein jokaisen kerran. AddPage luo uuden sivun ja osoittaa CurrentPage:n siihen, mutta se ei siirrä mitään mukanaan: ei fonttia, ei täyttöväriä, ei sijaintia. Siksi Page luetaan uudelleen CurrentPage:stä jokaisen AddPage:n jälkeen, ja siksi SetFont annetaan uudelleen ennen runkorivejä. Ohita uudelleenluku, ja jatkat piirtämistä sivulle, jonka juuri jätit taaksesi; ohita fontti, ja uusi sivu renderöidään millä tahansa oletusarvolla, mihin katseluohjelma turvautuu

Tapaukset, jotka rikkovat taulukkoviejän

Useimmat taulukkobugit eivät näy muutaman kymmenen siistin rivin onnellisella polulla. Ne elävät reunoilla, ja reunat ovat halpoja testata, kun tiedät, missä ne ovat

  • Tyhjät tietojoukot. Nollan rivin yli menevä silmukka tuottaa sivun, jossa on otsikko ja ei mitään sen alla, mikä ainakin näyttää tarkoitukselliselta. Tyhjä sivu ilman otsikkoa näyttää virheeltä. Päätä, kumman haluat ennen toimittamista
  • Rivi, joka osuu tasan rajalle. Luo raportti, jonka viimeinen rivi on yhden askeleen marginaalin yläpuolella, ja sitten sellainen, jonka seuraava rivi on yhden askeleen sen alapuolella. Yhdellä heittävä (off-by-one) sivutus piileskelee, kunnes data on tasan väärän pituista
  • Ylipitkät arvot. Sarakettaan leveämpi yrityksen nimi juoksee seuraavaan. Mittaa kenttä ja päätä käytännöstä: rivitä toiselle riville, leikkaa (clip) tai katkaise ellipsillä. Hiljaisuus ei ole käytäntö
  • Null-kentät. Null-arvon lukeminen suoraan TextOut-kutsuun voi tulla esiin kirjaimellisena tekstinä Null tai tyhjänä riippuen siitä, miten muunnat sen. Valitse renderöinti tarkoituksella mieluummin kuin antaisit varianttimuunnoksen valita puolestasi

Aja tulos useamman kuin yhden katseluohjelman läpi, ennen kuin kutsut sitä valmiiksi. Fontin korvaaminen ja leikkaus (clipping) käyttäytyvät eri tavalla eri renderöijissä, ja taulukko, joka näyttää suorakulmaiselta yhdessä PDF-lukijassa, voi näyttää väärin kohdistetun sarakkeen tai leikatun kaupungin toisessa. Varmista, että toistettu otsikko, rivivarjostus ja marginaalit selviävät siirrosta, ja että sivunumerot pysyvät jatkuvina sen jälkeen, kun data ylittää rajan

Ruudukon piirtäminen itse visuaaliseen raporttisuunnittelijaan tukeutumisen sijaan on enemmän koodia, ja kompromissi on syytä nimetä selvästi: omistat jokaisen koordinaatin, mikä on juuri sitä, mitä haluat palvelinpuolen eräajoille, laskuille ja tarkastusvienneille (audit exports), joiden on renderöidyttävä identtisesti jokaisella koneella, ja juuri sellaista lisätyötä (overhead), jota välttäisit mieluummin kertaluonteisessa sisäisessä listauksessa. Edellisten osalta hallinta maksaa itsensä takaisin ensimmäisen kerran, kun raportin on näytettävä samalta tuotannossa kuin se näytti työpöydälläsi

Yllä olevat viivat ja varjostetut nauhat nojaavat samoihin vektori- ja väriprimitiiveihin, joita käsiteltiin kankaan (canvas) piirtämisen läpikäynnissä, jos haluat, että Rectangle-, MoveTo- ja LineTo-kutsut käsitellään ensin erikseen. Tässä käytetyt piirtoprimitiivit ovat osa HotPDF Componentia Delphille ja C++Builderille