Teknik Makale

Delphi'de HotPDF ile Veri Tablosunu PDF'ye Aktarma

Bir veri kümesi satırlardan ve sütunlardan oluşur; bir PDF sayfası ise bunların hiçbirinden haberdar olmayan boş bir koordinat ızgarasıdır. Bu boşluğu kapatmak buradaki tüm işi oluşturur. HotPDF'de bir veri kümesini alıp size biçimlendirilmiş bir ızgara veren bir DrawTable çağrısı yoktur. Bunun yerine aldığınız şey bir ızgarayı oluşturan ilkellerdir: bir noktaya bir dize yerleştirmek için TextOut, yazı tipini seçmek için SetFont, bir şeridi gölgelendirmek için Rectangle ve Fill, ve kuralları (çizgileri) çizmek için MoveTo / LineTo / Stroke. Çalışan bir tablo dışa aktarıcısı, satır ve sütun düşüncesini açık x ve y koordinatlarına dönüştürme ve veriler sayfanın altını geçtiğinde bu koordinatları doğru tutma disiplinidir

Aşağıdaki örnek müşteri kayıtlarını raporlar, ancak çizim kodundaki hiçbir şey satırların nereden geldiğini bilmez veya umursamaz. Orijinal sürümde eski bir TTable kullanılmıştır; bir FireDAC sorgusu, bellek içi bir veri kümesi veya düz bir kayıt dizisi aynı rutinleri değişmeden besler. Önemli olan, veriler üzerinde her defasında bir satır ilerleyebilmeniz ve her birinden dört dize alanı okuyabilmenizdir. Oluşturma işlemini veri kaynağından ayrı tutarsanız, diğerini bozmadan her iki tarafı da değiştirebilirsiniz

Sütun geometrisi önce gelir

Tek bir karakter çizilmeden önce, her bir sütunun nerede bulunacağına karar verin. Buradaki tablonun dört sütunu vardır, bu nedenle dört sol kenara ve bilinen bir sağ kenar boşluğuna ihtiyacı vardır. Hızlı örneklerin eğiliminde olduğu gibi her TextOut çağrısında sihirli bir sayıyı sabit kodlamak, daha sonra bir tabloyu genişletmeyi zahmetli hale getiren şeydir. Sol alt başlangıç noktasından nokta cinsinden olacak şekilde kenarları bir kez adlandırın ve her çizim çağrısı onlara adıyla atıfta bulunsun:

const
  ColNo   = 70;    // "No." sütununun sol kenarı
  ColName = 110;   // şirket adı
  ColAddr = 300;   // sokak adresi
  ColCity = 480;   // şehir
  RowLeft = 50;    // tablo çerçevesi: sol çizgi
  RowRight = 570;  // tablo çerçevesi: sağ çizgi
  RowStep = 20;    // taban çizgileri arasındaki dikey mesafe

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // Satırın arkasında gölgeli bir şerit. Rectangle X, Y, Width, Height alır.
    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;

Burada iki ayrıntı dikkat çekiyor. Gölgeli şerit önce, ardından üstüne metin çizilir, çünkü PDF'de boyama sırası z düzenidir: metinden sonra dikdörtgeni doldurursanız satırı gömersiniz. Ayrıca dönüşümlü gölgeleme sadece süs amaçlı değildir. Yoğun bir raporda, gözün yanlış satıra kaymasını engellemenin en ucuz yoludur; bu nedenle döngü daha sonra her satırda bir boolean değerini tersine çevirir ve bunu doğrudan Shaded değişkenine iletir

Yukarıdaki sütun konumları sabittir ki bu, şemasını kontrol ettiğiniz bir rapor için dürüst bir yaklaşımdır. Veriler değişken olduğunda, tahmin etmek yerine ölçün. HotPDF, sayfa nesnesinde metin genişliği ölçümünü kullanıma sunar, bu nedenle PrintRow'un üretim sürümü her bir sütunda beklenen en uzun değeri alabilir, bunu seçilen yazı tipi boyutunda bir kez ölçebilir ve sol kenarları bu genişlikler artı bir boşluk üzerinden türetebilir. Rutinin şekli değişmez; yalnızca sabitlerin kaynağı değişir

Başlık, çizgiler ve onlara sahip olan tek bir yer

Bir sayfadan taşıp sütun etiketleri olmadan bir sonraki sayfada devam eden bir tablo okunamaz. Çözüm, başlığı bir kez çizdiğiniz bir şey olarak değil, yeniden çizdiğiniz bir şey olarak ele almaktır. Sütun başlıklarını ve onları çerçeveleyen yatay çizgileri tek bir rutine koyun ve o rutini hem başlangıçta hem de her yeni sayfa açtığınızda tekrar çağırın. Başlık ve gövde aynı sütun sabitlerini paylaştığı için, yapısal olarak hizalanırlar

procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Sol: kaynak etiketi ve sayfa numarası. Sağ: oluşturma zamanı.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Sayfa ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Sütun başlıklarını kutu içine alan iki yatay çizgi.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Başlık olarak okunabilmeleri için daha kalın bir yüzeyde sütun başlıkları.
  Page.SetFont('Times New Roman', [fsBold], 12);
  Page.SetRGBFillColor(clNavy);
  PrintRow(Page, Y + 25, 'No.', 'Şirket', 'Adres', 'Şehir', False);
  Page.SetRGBFillColor(clBlack);

  Y := Y + RowStep + 45;  // ilk gövde satırından önce kutulu başlığı geç
end;

Dikkat ederseniz DrawHeader rutini Y'yi referans olarak alır ve onu ileri taşır. Çağıranın başlığın ne kadar uzun olduğunu hiçbir zaman hatırlaması gerekmez; onu çizen rutin, onu bilen rutindir. Başlık şeridine daha sonra bir logo veya filtre özeti eklediğinizde düzenin kaymasını önleyen şey bu tek sahiplik kuralıdır. Gövde döngüsü ise bundan habersiz kalır. Yalnızca Y'nin o an işaret ettiği yerden itibaren satırlar çizmeye devam eder

Çizgilerin kendisi, bir liste ile bir tablo arasındaki farktır. Dikey sütun ayırıcıları, aynı fikrin x eksenine uygulanmasıdır: her bir sütun kenarında, sayfanın en üst çizgisinden sayfanın son satırının altına kadar uzanan bir MoveTo / LineTo / Stroke dizisi. Bu örnek, okunabilir kalmak için yatay çizgilere bağlı kalır, ancak sütun sabitleri var olduktan sonra üretim adımı mekaniktir

İmleç döngüsü sayfa sonunun sahibidir

Çizim işin kolay yarısıdır. Bir oyuncağı bir rapordan ayıran yarı, sayfalandırmadır: bir satırı çizmeden önce onun hâlâ sığıp sığmadığını bilmek ve sığmadığında yeni bir başlıkla yepyeni bir sayfa başlatmak. Bu karar tam olarak tek bir yere aittir; veriler üzerinde gezinen döngüye ve başka hiçbir yere değil

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

    // Rapor başlığı, sadece bir kez, ilk sayfanın en üstünde.
    Page.SetFont('Arial', [fsBold], 24);
    Page.TextOut(200, 800, 0, 'Müşteri Raporu');

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

    CustomerTable.First;
    while not CustomerTable.Eof do
    begin
      // Yer kalmadı mı? Yeni bir sayfa aç ve başlığı orada tekrarla.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage, CurrentPage'i ileriye taşır
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont her yeni sayfada yeniden verilmelidir
      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;

Tüm döngüyü iki koordinat gerçeği yönlendirir. PDF, y eksenini sol alt köşeden yukarıya doğru ölçer, bu nedenle satırlar her seferinde Y'den RowStep değerini çıkararak sayfada aşağı doğru ilerler ve sayfa dolu testi, Y'nin bir üst sınırın üzerine çıkmasından ziyade alt kenar boşluğunun altına düştüğünde tetiklenir. Yönü tersine çevirirseniz, döngü tam bir sayfa boşluk olduğunu düşünürken ilk satırınız alt kenarın dışına yazdırılır

Diğer gerçek, neredeyse herkesi bir kez yakalar. AddPage yeni bir sayfa oluşturur ve CurrentPage'i bu sayfaya yönlendirir, ancak hiçbir şeyi aktarmaz: yazı tipini, dolgu rengini veya konumu değil. İşte bu nedenle her AddPage işleminden sonra Page, CurrentPage üzerinden yeniden okunur ve gövde satırlarından önce SetFont işlemi yeniden verilir. Yeniden okumayı atlarsanız, geride bıraktığınız sayfaya çizim yapmaya devam edersiniz; yazı tipini atlarsanız, yeni sayfa görüntüleyicinin hangi varsayılan değere sahip olduğuna bağlı olarak oluşturulur

Bir tablo dışa aktarıcısını bozan durumlar

Çoğu tablo hatası, birkaç düzine düzenli satırın bulunduğu sorunsuz yolda ortaya çıkmaz. Onlar uç noktalarda yaşarlar ve nerede olduklarını öğrendikten sonra uç noktaları test etmek ucuzdur:

  • Boş veri kümeleri. Sıfır satır üzerinde dönen bir döngü, en azından kasıtlı görünen, başlığı olan ve altında hiçbir şey olmayan bir sayfa üretir. Başlığı olmayan boş bir sayfa ise bir hata gibi görünür. Göndermeden önce hangisini istediğinize karar verin
  • Tam olarak sınıra inen satır. Son satırı kenar boşluğunun bir adım üzerinde oturan bir rapor oluşturun, ardından bir sonraki satırı kenar boşluğunun bir adım altında olan bir rapor oluşturun. Bir sapmalı sayfalandırma hatası, veri tam olarak yanlış uzunluğa gelene kadar gizlenir
  • Aşırı uzun değerler. Sütunundan daha geniş olan bir şirket adı bir sonrakine taşacaktır. Alanı ölçün ve bir kurala karar verin: ikinci bir satıra kaydırın, kesin veya bir üç nokta ile kırpın. Sessiz kalmak bir kural değildir
  • Boş (Null) alanlar. Bir null değerini doğrudan TextOut içine okumak, onu nasıl dönüştürdüğünüze bağlı olarak harfi harfine Null metni veya bir boşluk olarak yüzeye çıkabilir. Değişken dönüştürmenin sizin yerinize seçim yapmasına izin vermek yerine, oluşturma işlemini kasıtlı olarak seçin

İşin bittiğini söylemeden önce sonucu birden fazla görüntüleyiciden geçirin. Yazı tipi değişimi ve kırpma işlemleri işleyiciler arasında farklı davranır ve bir PDF okuyucusunda düzgün görünen bir tablo, diğerinde hizalanmamış bir sütun veya kırpılmış bir şehir gösterebilir. Tekrarlanan başlığın, satır gölgelendirmesinin ve kenar boşluklarının taşınmadan kurtulduğunu ve veri bir sınırı geçtikten sonra sayfa numaralarının sürekli kaldığını doğrulayın

Görsel bir rapor tasarımcısına yaslanmak yerine ızgarayı kendiniz çizmek daha fazla kod demektir ve bu takasın adını açıkça koymaya değer: her makinede aynı şekilde oluşturulması gereken sunucu tarafı toplu işler, faturalar ve denetim dışa aktarımları için tam olarak istediğiniz şey olan her koordinatın sahibi olursunuz; ancak tek seferlik dahili bir listeleme için kaçınmayı tercih edeceğiniz genel gider de tam olarak budur. İlki için bu kontrol, bir raporun üretim ortamında da sizin masanızda göründüğü gibi görünmesi gerektiği ilk seferde kendi masrafını çıkarır

Eğer Rectangle, MoveTo ve LineTo çağrılarının önce kendi başlarına ele alınmasını isterseniz, yukarıdaki çizgiler ve gölgeli şeritler, tuval çizimi yönergesinde ele alınan aynı vektör ve renk ilkellerine dayanır. Burada kullanılan çizim ilkelleri, Delphi ve C++Builder için HotPDF Component'in bir parçasıdır