Техническа статия

Рендиране на таблица с данни в PDF в Delphi с HotPDF

Наборът от данни е редове и колони; PDF страницата е празна координатна решетка без понятие за нито едно от двете. Преодоляването на тази празнина е цялата работа тук. В HotPDF няма извикване DrawTable, което да вземе набор от данни и да ви предаде форматирана решетка. Това, което получавате вместо това, са примитивите, от които е съставена решетката: TextOut за поставяне на низ в точка, SetFont за избор на нейния вид, Rectangle и Fill за засенчване на лента и MoveTo / LineTo / Stroke за изчертаване на линии. Работещият експортьор на таблици е дисциплината за превръщане на мисленето в редове и колони в изрични координати x и y, и след това поддържане на тези координати честни, когато данните преминат покрай долната част на страницата

Примерът, който следва, отчита клиентски записи, но нищо в кода за чертане не знае или не се интересува откъде идват редовете. Оригиналът използва наследена TTable; заявка FireDAC, набор от данни в паметта или обикновен масив от записи захранва същите рутини непроменени. Важното е, че можете да обхождате данните ред по ред и да четете четири низови полета от всеки. Поддържайте рендирането отделно от източника на данни и можете да променяте която и да е от страните, без да смущавате другата

Геометрията на колоните е на първо място

Преди да бъде начертан един-единствен знак, решете къде живее всяка колона. Една таблица тук има четири колони, така че се нуждае от четири леви ръба и известен десен марж. Твърдото кодиране на магическо число при всяко извикване на TextOut, както обикновено правят бързите примери, е точно това, което прави една таблица болезнена за разширяване по-късно. Назовете ръбовете веднъж, в пунктове от началото долу вляво, и всяко извикване за чертане се отнася към тях по име:

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;

Два детайла печелят своето място тук. Засенчената лента се изчертава първа, след това текстът отгоре, тъй като редът на рисуване е z-ред в PDF: запълнете правоъгълника след текста и ще погребете реда. И редуващата се сянка не е декорация сама по себе си. При плътен отчет това е най-евтиният начин да спрете окото да се плъзне към грешния ред, поради което цикълът по-късно обръща булева стойност на всеки ред и я предава направо в Shaded

Позициите на колоните по-горе са фиксирани, което е честно за отчет, чиято схема контролирате. Когато данните са променливи, мерете, вместо да гадаете. HotPDF излага измерване на ширината на текста в обекта на страницата, така че производствената версия на PrintRow може да вземе най-дългата очаквана стойност във всяка колона, да я измери веднъж при избрания размер на шрифта и да извлече левите ръбове от тези ширини плюс отстъп. Формата на рутината не се променя; само източникът на константите се променя

Заглавката, правилата и едно място, което ги притежава

Таблица, която излиза от страницата и се възобновява на следващата без етикети на колони, е нечетима. Решението е да третирате заглавката като нещо, което преначертавате, а не нещо, което чертаете веднъж. Поставете заглавията на колоните и хоризонталните линии, които ги рамкират, в една рутина и извиквайте тази рутина както в началото, така и всеки път, когато отваряте нова страница. Тъй като заглавката и тялото споделят едни и същи константи за колони, те се изравняват по конструкция

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;

Обърнете внимание, че DrawHeader приема Y по референция и го движи напред. Извикващият никога не трябва да помни колко е висока заглавката; рутината, която я чертае, е рутината, която знае. Това правило за единствена собственост е това, което предпазва оформлението от разместване, когато по-късно добавите лого или резюме на филтър към лентата на заглавката. Цикълът на тялото остава в неведение. Той просто продължава да чертае редове оттам, накъдето сочи Y в момента

Самите линии са разликата между списък и таблица. Вертикалните разделители на колони са същата идея, приложена към оста x: MoveTo / LineTo / Stroke на всеки ръб на колона, движещи се от горната линия до дъното на последния ред на страницата. Примерът се придържа към хоризонтални линии, за да остане четим, но производствената стъпка е механична, след като константите на колоните съществуват

Цикълът на курсора притежава прекъсването на страницата

Чертането е лесната половина. Половината, която отличава играчката от отчета, е пагинирането: да знаете, преди да начертаете ред, дали той все още се побира, и да започнете нова страница с нова заглавка, когато това не е така. Това решение принадлежи точно на едно място, цикъла, който обхожда данните, и никъде другаде

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;

Два координатни факта движат целия цикъл. PDF измерва y нагоре от долния ляв ъгъл, така че редовете маршируват надолу по страницата чрез изваждане на RowStep от Y всеки път, и тестът за пълна страница се задейства, когато Y падне под долния марж, а не над някакъв връх. Сбъркайте посоката и първият ви ред се отпечатва извън долния ръб, докато цикълът мисли, че има цяла страница свободно място

Другият факт хваща почти всеки веднъж. AddPage създава нова страница и насочва CurrentPage към нея, но не пренася нищо: нито шрифта, нито цвета на запълване, нито позицията. Ето защо Page се препрочита от CurrentPage след всеки AddPage и защо SetFont се преиздава преди редовете на тялото. Пропуснете препрочитането и продължавате да чертаете върху страницата, която току-що сте оставили; пропуснете шрифта и новата страница се рендира в каквото и да е подразбиране, към което четецът се връща

Случаите, които чупят експортьор на таблици

Повечето бъгове в таблици не се показват по щастливия път от няколко дузини спретнати редове. Те живеят по краищата, а краищата са евтини за тестване, след като знаете къде са

  • Празни набори от данни. Цикъл върху нула реда произвежда страница със заглавка и нищо под нея, което поне изглежда умишлено. Празна страница без заглавка изглежда като повреда. Решете кое от двете искате преди доставката
  • Редът, който попада точно на границата. Генерирайте отчет, чийто последен ред седи една стъпка над маржа, след това такъв, чийто следващ ред е една стъпка под него. Пагинирането с грешка от единица се крие, докато данните не са с точно грешната дължина
  • Твърде дълги стойности. Име на компания, по-широко от колоната си, ще се сблъска със следващата. Измерете полето и вземете решение за политика: пренасяне на втори ред, изрязване или отрязване с многоточие. Мълчанието не е политика
  • Нулеви (Null) полета. Четенето на null директно в TextOut може да излезе като буквалния текст Null или като празно място, в зависимост от това как го конвертирате. Изберете рендирането умишлено, вместо да оставяте конверсията на варианта да избира вместо вас

Прекарайте резултата през повече от един четец, преди да го наречете готов. Заместването на шрифтове и изрязването се държат различно при различните рендерери и таблица, която изглежда квадратна в един PDF четец, може да покаже разместена колона или изрязан град в друг. Потвърдете, че повтарящата се заглавка, засенчването на редовете и полетата оцеляват при преместването, и че номерата на страниците остават непрекъснати, след като данните преминат граница

Изчертаването на решетката сами, вместо да се опирате на визуален дизайнер на отчети, е повече код, и компромисът си струва да бъде назован ясно: вие притежавате всяка координата, което е точно това, което искате за сървърни пакетни задачи, фактури и одитни експорти, които трябва да се рендират идентично на всяка машина, и точно режийните разходи, които бихте предпочели да избегнете за еднократен вътрешен списък. За първото контролът се изплаща първия път, когато даден отчет трябва да изглежда същият в производството, както е изглеждал на бюрото ви

Правилата и засенчените ленти по-горе се опират на същите векторни и цветови примитиви, разгледани в ръководството за чертане на платно, ако искате извикванията на Rectangle, MoveTo и LineTo да бъдат разгледани самостоятелно първо. Примитивите за чертане, използвани тук, са част от HotPDF Component за Delphi и C++Builder