Technical Article

Визуализиране на таблица с данни в PDF в Delphi с HotPDF

Наборът от данни (dataset) се състои от редове и колони, докато PDF страницата е празна координатна мрежа без никаква представа за тях. Преодоляването на тази разлика е основната задача тук. В HotPDF няма извикване от типа DrawTable, което да приема набор от данни и да ви връща форматирана мрежа. Вместо това получавате основните елементи, от които се състои мрежата: TextOut за поставяне на низ на определено място, SetFont за избор на шрифт, Rectangle и Fill за оцветяване на заден фон и MoveTo / LineTo / Stroke за изчертаване на линии. Създаването на работещ модул за експортиране на таблици е дисциплина за превръщане на мисленето за редове и колони в ясни X и Y координати и поддържането на тези координати точни, когато данните преминат долния край на страницата.

Примерът по-долу показва записи на клиенти, но нищо в кода за изчертаване не се интересува откъде идват редовете. Оригиналният пример е използвал по-стария TTable. Заявка с FireDAC, набор от данни в паметта (in-memory dataset) или обикновен масив от записи захранват същите рутини без промяна. Важното е да можете да обхождате данните ред по ред и да четете четири текстови полета от всеки от тях. Дръжте визуализацията отделно от източника на данни и ще можете да променяте всяка от страните, без да пречите на другата.

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

Преди да бъде изчертан дори един символ, определете къде се намира всяка колона. Нашата таблица има четири колони, така че се нуждае от четири леви граници и известно дясно поле. Хард-кодирането на произволни стойности (magic numbers) при всяко извикване на 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-порядъка (z-order) в PDF: ако оцветите правоъгълника след текста, ще скриете реда. Редуването на фоновите цветове не е просто декорация. В подробен отчет това е най-лесният начин да предпазите погледа от приплъзване към грешния ред, поради което цикълът по-късно превключва булева стойност за всеки ред и я предава директно на параметъра Shaded.

Позициите на колоните по-горе са фиксирани, което е правилно за отчет, чиято схема контролирате. Когато данните са променливи, измервайте размерите, вместо да гадаете. HotPDF позволява измерване на ширината на текста чрез обекта на страницата, така че реалната версия на PrintRow може да вземе най-дългата очаквана стойност във всяка колона, да я измери веднъж при избрания размер на шрифта и да изчисли левите граници въз основа на тези ширини плюс разстояние за разделител (gutter). Структурата на подпрограмата не се променя â€?променя се само източникът на константите.

Заглавната част, линиите и мястото за управлението им

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

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 по референция (var) и го придвижва напред. Извикващият код не трябва да помни колко е висока заглавната част; рутината, която я изчертава, е тази, която знае това. Това правило за управление от едно място предотвратява разместване на оформлението, когато по-късно добавите лого или обобщение на филтрите към заглавната лента. Цикълът за изчертаване на тялото на таблицата не се определя от това â€?той просто продължава да добавя редове оттам, накъдето сочи Y в момента.

Цикълът за обхождане управлява пренасянето на страници

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

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 се извиква отново преди изчертаването на редовете от тялото. Пропуснете повторното четене и ще продължите да рисувате върху предходната страница. Пропуснете шрифта и новата страница ще се визуализира с шрифта по подразбиране на съответния четец.

Случаите, които могат да повредят експортирането на таблици

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

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

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

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

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