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×
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í.
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 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
.
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.
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 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.
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()
.
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.
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)!
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.
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.
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()
).
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.
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.
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.
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.