Java (31) - základy tisku

Java U mnoha programů si nevystačíme jen s prohlížením dokumentu nebo nebo jiných dat na obrazovce. Potřebujeme to vytisknout. V Javě je to překvapivě jednoduché, tím spíš, že se stírají rozdíly mezi jednotlivými platformami, kde se jinak k tisku přistupuje dost odlišně.

16.1.2007 06:00 | Lukáš Jelínek | přečteno 24344×

Základní pojmy

Než můžeme vůbec uvažovat o nějaké implementaci tisku, je potřeba dobře vědět, jak vůbec tiskový proces vypadá. A neuškodí si před tím připomenout některé pojmy, které se v této oblasti vyskytují.

Grafický kontext

Již jsme se s ním setkali, a to při kreslení. Reprezentuje ho třída Graphics a její potomci (typicky Graphics2D). Zjednodušeně řečeno, grafický kontext vyjadřuje, kam se kreslí. Pro kreslení na obrazovku se použije kontext spjatý s obrazovkou, pro tisk kontext tiskárny, mohou být i jiné kontexty (třeba pro nějaké operace v paměti).

Dokument

Dokument je to, co tiskneme. Data v něm obsažená se převádějí do tisknutelné podoby, do dat určených pro tiskárnu. Pod dokumentem si v této souvislosti můžeme představit cokoliv, třeba i nějakou komponentu GUI, pokud se nám zachce si ji vytisknout. Zde budeme k dokumentu přistupovat přes rozhraní Printable.

Stránka

Vcelku logická věc. Obvykle se tiskne na stránky, tedy listy papíru. Sice známe cosi jako "nekonečný papír", ale i tyto tiskárny, pokud netisknou pouze řádky textu, se stránkou jako plochou o určité velikosti pracují. Dokument se pro tisk děli právě na stránky, z nichž se tisknou třeba pouze některé. V Javě existuje rozhraní Pageable, představující sadu stránek, a dále třída PageFormat s popisem parametrů stránky.

Tisková úloha

Každý tisk má svůj začátek a konec. Například začneme první (nebo jinou) stránkou a skončíme poslední. Ať je to jakkoliv, jedná se o ohraničenou záležitost, které se říká tisková úloha. Lze si to představit třeba jako postscriptový soubor, který se sestaví, někam uloží a pak se pošle do tiskárny. Jak je to ve skutečnosti, je záležitost systému. V programu ovšem vidíme tak, že v podstatě vytvoříme ucelený balík dat a ať si s ním systém dělá, co chce. Pro úlohu slouží abstraktní třída PrinterJob.

Tisková služba

Tisková služba je v podstatě zobecněná virtuální tiskárna. Může to být místní tiskový démon, vzdálená tisková služba, ale třeba i jen nějaký program, který převádí data do určitého výstupního formátu. Tiskové služby teď necháme ještě spát, na ty přijde čas později. Zatím je důležité pouze to, že není-li k dispozici žádná tisková služba, nelze nic tisknout.

Tiskneme - nebo kreslíme?

Vrátíme-li se zpět do 25. dílu seriálu, kde se popisuje kreslení v metodě paint() grafických objektů, je tam zmínka též o metodách print() a printAll(). A také o tom, že by se na ně nemělo sahat. To je sice pravda, nicméně jsou případy, kdy to neplatí. Ale o tom až později. Nejdřív je důležité vědět, co se vůbec při tisku děje.

Každá komponenta obsahuje metody print() a printAll(). Jejich výchozí implementace jsou velmi jednoduché. Nastavují příznak tisku a chytají výjimky. Odlišně se chová pouze třída JTable, a to kvůli možnosti různě nakládat s dělením na stránky. Že jsou metody dvě, je pozůstatek z historie. Běžně se používá jen print().

Výše řečené znamená, že je tisk v podstatě ekvivalentní s běžným zobrazováním. Liší se jen v tom, že se pracuje s jiným grafickým kontextem. Pokud potřebujeme jiné chování, můžeme předefinovat print(), případně též další metody související s tiskem. Pokud jde o menší změny, stačí si jen někam poznamenat, že se tiskne (výše uvedený příznak není k dispozici - je deklarován jako private) a reagovat na to v paint().

Tiskový dokument

Nyní potřebujeme implementovat rozhraní Printable, které má jedinou metodu: print(Graphics, PageFormat, int). Jak je vidět z uvedeného prototypu, jako argumenty se předávají grafický kontext, formát stránky a její index. Metoda se stará o samotný tisk, přesněji řečeno vykreslení jedné stránky.

"Obyčejný" grafický kontext (třída Graphics) je navržen pro práci s celými čísly, v pixelech. Při tisku ovšem pixely nevidíme rádi, přirozenější jsou "klasické" měrné jednotky, tedy například tiskařský bod nebo centimetr. Proto musíme získat objekt třídy Graphics2D, který umožňuje takové jednotky (v plovoucí řádové čárce) používat.

Není pro to nutné udělat nic zvláštního, protože získaný grafický kontext je instancí právě této třídy, takže stačí přetypování. Navíc implementuje rozhraní PrinterGraphics, o kterém ještě bude řeč. Kontext můžeme používat obvyklým způsobem - tedy normálně kreslit dle libosti. Lze používat jak "celočíselné" metody (ze třídy Graphics), tak i metody přijímající parametry v plovoucí řádové čárce.

Transformace

Jistě se nyní můžeme ptát: jak vůbec zacházet s rozměry? Je to poměrně jednoduché. Grafický kontext obsahuje transformační matici o velikosti 3 x 3. Přes tuto matici se přepočítává vstupní prostor (ten, do kterého kreslíme) na výstupní (prostor zařízení). Matici můžeme měnit podle potřeby - pokud to neuděláme, použije se výchozí. Ta se získává z objektu grafické konfigurace (GraphicsConfiguration) příslušného zařízení, ale to není až tak důležité.

Mnohem větší význam má, jak ta matice vlastně vypadá. Máme tři případy:

Výše popsané má samozřejmě hlubokou logiku, jak každý jistě pochopí. Pokud by to přesto někomu nevyhovovalo a chtěl by třeba místo tiskových bodů používat milimetry, cesta je snadná. Stačí prostě vytvořit novou matici (nebo lépe použít kopii té existující), příslušné koeficienty změnit a pak matici přiřadit grafickému kontextu. Ovšem pozor na to, že se s tím pak musí počítat úplně všude (třeba i u okrajů stránek)!

Nejjednodušší tisk

Dost bylo řečí, jdeme tisknout. Připomínám: implementujeme rozhraní Printable. Zde je krátký příklad:

public int print(Graphics g, PageFormat pageFmt, int index) {
  if (index != 0)
    return NO_SUCH_PAGE;
  
  Graphics2D g2 = (Graphics2D) g;
  
  g2.translate(pageFmt.getImageableX(), pageFmt.getImageableY());
  
  String s = "Tohle je text";
  TextLayout tl = new TextLayout(s, g2.getFont(), g2.getFontRenderContext());
  Rectangle2D rt = tl.getBounds();
  g2.drawString(s, 0, (float) rt.getHeight());
  
  return PAGE_EXISTS;
}

Vypadá to možná složitě, ale složité to není. První podmínka kontroluje, že jde o požadavek na tisk 1. stránky (pro ostatní vrátí, že stránka neexistuje). Pak následuje přetypování kontextu a jeho posun do oblasti tisku. Máme totiž definovány okraje a mimo ně tisknout nelze (nic by se tam nevytisklo). Posun lze samozřejmě pokaždé přičítat k souřadnicím, ale jednorázově je to lepší.

Další část se týká vykreslení textu. A tady pozor. Metoda drawString() totiž pracuje se svislou pozicí účaří, nikoli horní hranice oblasti textu. To je sice velmi důležité pro správné řádkování, působí to ovšem určité komplikace při správném umístění vzhledem k tisknutelné ploše. Proto musíme zjistit, kolik místa text ve svislém směru zabírá a podle toho ho umístit, aby nevyběhl za okraj.

Příklad pracuje s aktuálním písmem, tedy obvykle výchozím. Chceme-li jiné, musíme ho nastavit, a to samozřejmě ještě před výpočtem umístění. Obdobně, kdybychom třeba chtěli změnit barvu.

Tisk GUI

Již dříve jsem zmínil snadný tisk komponent GUI. Nyní uvedu příklad, jak by to mohlo vypadat:

JComponent c = ...

RepaintManager mgr = RepaintManager.currentManager(c);
boolean db = mgr.isDoubleBufferingEnabled();
mgr.setDoubleBufferingEnabled(false);
c.print(g2);
mgr.setDoubleBufferingEnabled(db);

Výše uvedený kód by nahradil odpovídající část v metodě print() z předchozího příkladu (tedy vše kromě záležitostí ohledně čísla stránky). Vlastní tisk spočívá pouze v zavolání metody print(). Vzhledem k výchozí transformační matici bude 1 pixel GUI odpovídat 1 tiskovému bodu.

Co stojí za zmínku, je dočasná deaktivace dvojitého bufferingu, pokud byl zapnutý. Při tisku je tato technika zbytečná (má za cíl zabránit blikání na obrazovce) a pokud by se použila, bude spotřebovávat paměť a čas procesoru.

Směr ... tiskárna

Zatím jsme ještě nic nevytiskli. Sice už víme, jak tisknout, ale ještě ne, jak vytvořit tiskovou úlohu a poslat ji na tiskárnu. Také tento úkol není vůbec těžký. Podívejme se na příklad:

protected void doPrint(Printable pt) {
  PrinterJob pj = PrinterJob.getPrinterJob();
  pj.setPrintable(pt);
  try {
    pj.print();
  } catch (PrinterException e) {
    JOptionPane.showMessageDialog(this,
        "Při tisku došlo k chybě: " + e.getMessage(),
        "Chyba", JOptionPane.ERROR_MESSAGE);
  }
}

Vše, co v tuto chvíli potřebujeme, je vytvořit tiskovou úlohu, předat jí dokument a spustit tisk. Tiskne se na výchozí tiskárnu s výchozími parametry (velikost stránky, okraje, rozlišení atd.). Všimněte si, že při tisku může nastat výjimka, která v příkladu způsobí zobrazení panelu s chybovou zprávou. Ještě připomenu již zmíněné rozhraní PrinterGraphics - přes něj se můžeme dostat k instanci tiskové úlohy (metodou getPrinterJob()).

Změna parametrů tisku

Někdy potřebujeme před tiskem změnit některé parametry - cílovou tiskárnu, formát stránky apod. Existují dva dialogy, které to umožňují. Prvním z nich je hlavní tiskový dialog, dostupný přes metodu printDialog(). Používá se například takto:

if (pj.printDialog())
  pj.print();

Dialog má dvě varianty, kromě výše uvedené (nativní) ještě přenositelnou, ke které se dostaneme příště. Pozor, metodu lze volat jen v případě, že máme možnost dialog zobrazit (jinak metoda vyhodí HeadlessException). Ke druhému dialogu, stránkovému, se dostaneme za chvíli.

Stránkovaný tisk

I když byl původní příklad navržen k tisku jen jediné stránky, klidně by jich mohl tisknout víc. Protože máme index stránky a její formát, není to problém. Jenže někdy to tak jednoduché není. Stránky v sadě mohou mít různý formát a pocházet z jiného logického dokumentu. Také předem neznáme celkový počet stránek k tisku.

Proto existuje rozhraní Pageable, které tyto problémy řeší. Má tři metody: getNumberOfPages(), vracející počet stránek, dále getPageFormat(), poskytující formát určité stránky, a konečně getPrintable(), zpřístupňující příslušný dokument ke stránce.

Teď jde o to, jak to použít. Buď si můžeme napsat vlastní implementaci, nebo použít tu, která už existuje. A to je třída Book. Má metody append() a setPage(), s jejichž pomocí lze snadno sestavit celou "knihu", tedy sadu stránek. Má-li to smysl, můžeme uživateli poskytnout možnost změnit formát stránky - jako jsme již použili metodu printDialog(), existuje podobná metoda pageDialog(). Poskytne se jí původní nastavení a získáme nové - tedy pokud uživatel provede změnu a potvrdí ji.

Objekt třídy Pageable máme, ale co s ním? Musíme ho nastavit tiskové úloze, a to metodou setPageable(). Úloha si "vytáhne" potřebné informace a použije je při tisku.

Jak získat náhled

Mnohdy se hodí mít možnost náhledu před tiskem. Ideální je generovat ho naprosto stejnou cestou jako samotný tisk. Vytvoříme si tedy nějakou komponentu (potomka JComponent), implementujeme metodu paint(), která bude obsahovat všechno potřebné ke kreslení. Bude-li se toto používat pro vícestránkový dokument, musí komponenta nějak rozlišit, které stránky se to týká (např. uložit si číslo aktuální stránky do nějaké proměnné).

Pak už stačí jen implementovat rozhraní Printable a v metodě print(Graphics, PageFormat, int) nejdřív nastavit aktuální číslo stránky a pak zavolat print(Graphics). Náhled se bude zobrazovat standardní cestou (jako normální grafická komponenta), jen je potřeba nějak zajistit výběr aktuální stránky (třeba tlačítky na panelu).

Ještě je tu jeden faktor, který má vliv na tisk i náhled: formát stránky, tedy instance PageFormat. Je potřeba, aby komponenta znala tento formát předem, aby se mohl použít už při náhledu.

Tiskové služby

Poslední oblastí v problematice tisku jsou tiskové služby. Je to záležitost poměrně rozsáhlá, proto jí bude věnován celý příští díl. Využití tiskových služeb umožňuje postoupit o úroveň výše a nemuset se tedy zabývat tím, kterou dostupnou tiskárnu použít k tisku požadovaného dokumentu. Vrstva tiskových služeb to totiž umí vyřešit za nás. A zvládá i mnohé další věci.

Ještě jedno doporučení na závěr. Budete-li experimentovat s tiskem, můžete používat pseudotiskárnu s výstupem do souboru (PostScript nebo PDF). Výsledek uvidíte hned a nemusíte plýtvat papírem.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1400