Urmet Liin

Tarkvarasüsteemide seminari ettekanne

Java joonistus- ning pilditöötlusvahendid


Sisukord


Lihtne graafika


Joonistamiseks vajalikud klassid on pärit pakendist java.awt nagu näha ka allpool toodud joonisel
Joonis 1.

Klassi java.awt.Graphics eksemplari kutsutakse graafiliseks kontekstiks Ta esitab teatavat joonistatavat ala (näiteks komponendi ekraanipind või nn eeljoonistuspuhver(off-screen buffer). Graafikakontekst pakub meetodeid kõigi põhiliste graafikaoperatsioonide täitmiseks oma pinnal. Sinna kuuluvad ka pildi andmete joonistamiseks vajalikud meetodid. Kontekstiks annab teda alust kutsuda see, et ta sisaldab endas ka selliseid kontekstuaalseid andmeid nagu tekstifont, aktiivne värv, edastuse tüüp.

Üldjoontes on neli põhilist võimalust Graphics objekti kasutamiseks Need on:


Iga paint() või update()välja kutsumisega seob AWT meie komponendi uue Graphics objektiga. See tähendab aga, et kõik parameetrid, mis ühe joonistamissessiooni käigus määratud on, saavad taas vaikimisi väärtused.

Tegeldes mingi komponendi update() meetodiga võime eeldada, et pilt ekraanil püsib muutmatuna ning me saame seda vaid mõningate täiendustega ajakohastada. Sellise tegevuse juures aitab sageli joonistamistegevust optimiseerida nn. väljalõigatava (Clipping) ala määramine. paint() meetodi puhul joonistatakse kogu ala uuesti üle.

Joonistamise meetodid

Graphics klassi objektid kasutavad standardset koordinaatide süsteemi, kus iga uue graafikakonteksti nullpunkt asub ülal vasakul.

Kuna antud koordinaatide süsteem on vaid vaikimisi ning pole kindlasti mitte kohustuslik, siis saab koordinaate translate() meetodiga ka nihutada andes talle argumendiks uue null-punkti koordinaadid.

clipRect() meetodiga saab joonistatavast alast teha väljalõike. Töötamaks mitme väljalõigatud alaga tuleks luua originaalsest graafika kontekstist create() meetodiga koopia.

Kui olete teinud mingitki graafikat sisaldavat programmeerimist, tulevad nii joonistusmeetodid kindlasti tuttavad ette. Järgnev näide TestPatterndemonstreerib peamisi kujundi joomistamise meetodeid. 


TestPattern rakend ja TestPattern lähtetekst
Antud näide joonistab mitmeid lihtsaid kujundeid ning võtab vastu hiirevajutusi, et nende järgi pöörata üht täidetud sektorit ning pilt üle joonoistada. Kui korduvalt antud rakendil hiirt klõpsutada, on näha, kuidas pilt üle joonistamisest tingituna "sähvib". Mõne aja pärast vaatame ka vahendeid selle "sähvimise" vältimiseks.

Kõik eelpoolmainitud näites toodud meetodid välja arvatud fillArc() ja fillPolygon() vajavad oma tööks ülemise vasaku punkti koordinaate ning kõrgust ning laiust.

Uue ja huvitava kujundina antud näites kasutasime hulknurka (Polygon). Hulknurga joonistamiseks on vaja kahte massiivi, kus oleksid siis vastavalt iga hulknurga tipu x ja y koordinaadid. Ehkki Graphics klassis on ka lihtsad meetodid kahe massiivi järgi täidetud hulknurga joonistamiseks, lõime me näites Polygon objekti, kuna tal on mitmeid päris kasulikke omadusi, nagu näiteks see, et ta võib teatada oma nn. "raamkasti" ning ka seda, kas mingi asub hulknurga sees.

fillArc() meetod vajab oma argumentideks kuut täisarvu. Neli esimest määravad sektori "raamkasti" ning kaks viimast stardinurga ning kaare suuruse (mõlemad kraadides).

Tabelis 11-1 näidatakse kujundijoonistamise meetodeid Graphics klassist. Nagu näha, on pea iga fill() meetodi kõrval ka vastav draw() meetod, mis renderdab kujundit tühjana. 


Meetod Kirjeldus
draw3DRect() 3D ristkülik
drawArc() kaar
drawLine() joon
drawOval() ovaal
drawPolygon() hulknurk
drawRect() ristkülik
drawRoundRect() ümarate nurkadega ristkülik
fill3DRect() täidetud 3D ristkülik
fillArc() täidetud kaar
fillOval() täidetud ovaal
fillPolygon() täidetud hulknurk
fillRect() täidetud ristkülik

Tabelis pole kõiki joonistamismeetodeid, nagu näiteks drawString(), mis joonistab ekraanile teksti ning drawImage() mis joonistab pildi, aga neid me vaatleme pisut hiljem detailsemalt.

Värvid

Meie TestPattern rakend kasutas erinevate kujundite värvimiseks Graphics objekti setColor() meetodit.

Värvidega tegeleb klass java.awt.Color. Objekt Color kirjeldab üksikut värvi. Uue värvi loomiseks tuleb määrata tema punase-, rohelise- ja sinise (RGB) väärtus kas kolme täisarvuna, või ujukomaarvudega vahemikus 0.0 ... 1.0. Samuti saab kasutada getColor() meetodit saamaks süsteemset värvi nime. getColor() võtab String värvi omaduse, teisendab selle täisarvuks omaduste listist ning tagastab Color objekti, mis sellele numbrile vastab.

Color klassis defineeritakse ka mitmed static final tüüpi värviväärtusi, mida me kasutasime TestPattern näites. 


Fondid


Fondid on Javas esitatud klassis java.awt.Font. objekt FONT sisaldab fondinime, stiili identifikaatorit, suurust punktides. 
Näide paarist fondist

Font timesFont = new Font("TimesRoman", Font.PLAIN, 12);
Font courierFont = new Font("Courier", Font.BOLD, 18);



Fondi nimi on string, mis määrab fondi pere. järgnevad viis fondinime peavad igal platvormil kasutatavad olema:
 

Fondi nimi on seotud tegeliku fondiga kohalikul platvormil. Töö fontidega on siiski veel seotud mõningaste platvormist sõltuvate raskustega, aga eelpoolmainitud nimede kasutamisel peaks tulemus olema igati platvormist sõltumatu.

Samuti on võimalik kasutada static meetodit Font.getFont() saamaks fondi nime süsteemi atribuutide listist. Sarnaselt getColor()'ile teisendab getFont() fondi String omaduse vastavaks fondinimeks Properties tabelist ning väljastab siis vastava Font objekti.

Klass Font defineerib kolm static tüüpi identifikaatorit: PLAIN, BOLD ja ITALIC. Neid väärtusi võib kasutada iga fondiga. Fondi suurus punktides määrab tema suuruse ekraanil. Kui vastava suurusega fonti süsteemis pole, omandab font vaikimisi suuruse.

Font'i kohta saab infot mitmete erinevate meetoditega. Näiteks olgu toodud getName(), getFamily(), getSize() ning getStyle

Font objekti kasutatakse argumendina setFont() meetodis, määrates kas komponendi või Graphics objekti kirjastiili.

Fondi mõõtmed

Saamaks detailset infot fondi suuruse ja vahede kohta võime me seda küsida objektist java.awt.FontMetrics. Kuna sihtplatvormil ei pruugi määratud font olla sama suuruse ja kujuga, kui me esialgu arvasime, siis hoiab FontMetrics endas infot antud süsteemi fontide mõõtmete kohta.

getFontMetrics()'i abil saab küsida FontMetrics objekti nii mingi kindla fondi kohta, kui ka Graphics objekti vaikimisi fondi kohta (nt. g.getFontMetrics() ). Järgnev näide näitab ekraanil sõna, ning joonistab mõningad abijooni. HiireVajutusega vahetatakse suurt ja väikest fonti. 


FontShow rakend ja FontShow lähtetekst.
Vaikimisi kuvatakse teksti suunaga vasakult paremale ning sinna, kuhu viitavad drawString()'ile parameetriteks antud koordinaadid.

Järgnev tabel näitab mõningaid fondi atribuute käsitlevaid meeetodeid: 


Meetod Kirjeldus
getFont() objekt Font koos kirjeldusega
getAscent() kõrgus ülalpool baasjoont
getDescent() sügavus allpool baasjoont
getLeading() standardne vertikaalne ruum ridade vahel
getHeight() täielik rea kõrgus (ascent + descent + leading)
getWidth( char ch ) tähe laius
stringWidth( String str ) stringi laius
getWidths() antud stiili esimese 256 sümboli laiused, väljastab int()'i 
getMaxAdvance() kõigi märkide laiuste maksimum


Pildid


Siiani tegelesime lihtsate kujundite ning teksti esitamisega. Et graafikast komplekssemat pilti saada vaatame ka piltide esitamist. AWT pakub väga rikkaliku valiku piltide genereerimiseks ning esitamiseks mõeldud vahendeid. Me alustame põhilisest klassis java.awt.Image ning vaatame, kuidas üldse saada oma rakendisse pilti. Ülesanne pole siiski nii lihtne, kui esmapilgul paistab, kuna pilt võib olla pärit võrgust ning tema lugemisel võib tekkida probleeme, siis sellest veidi lähemalt. Esialgu püüame näidata pilti vaid siis, kui ta on valmis ning laseme AWT'l teha ülejäänu. Hiljem vaatame ka muid võimalusi.

java.awt.Image

Klass java.awt.Image esitab vaate pildile. Pildi allikas võib olla staatiline (JPEG või GIF fail) või siis dünaamiline (videovoog või graafikamootor) Rakend võib pildi järele kysida getImage() meetodiga. Kui meil on olemas Image objekt, saame me teda kuvada graafikakonteksti drawImage() meetodiga. Lihtsaimal juhul vajab DrawImage nelja parameetrit: pilti, mida joonistada, x ja y koordinaati ning viidet spetsiaalsele pildivaatleja objektile.

Pildivaatleja (Image Observer)

AWT's töödeldakse pilte sünkroonselt. See tähendab, et java teeb selliseid pilditöötlusprotseduure nagu pildi lugemine ja tema möötmete muutmine, omal ajal. Näiteks lõpetab getImage() alati viivitamatult isegi siis, kui pildist pole veel bittigi kohal. Antud strateegia annab võimaluse teha suuremahulisi graafikaoperatsioone paralleelselt mitmetes erinevates lõimedes. Siiski toob selline lähenemine kaasa ka hulgaliselt probleeme, nagu näiteks see, et me ei tea, kui suur on meie poolt laaditud pilt, samuti tekib probleem sellega, et me ei tea, kuidas laabub pildi laadimine ning millal see valmis on.

Nimetatud probleemidega tegelevadki pildivaatlejad - spetsiaalsed objektid, mis implementeerivad ImageObserver liidest. Kõik pildi joonistamise või uurimise meetodid lõpetavad alati viivitamatult, aga võtavad pildivaatleja objekti omale parameetriks.
ImageObserver valdab pildi staatuse kohta käivat infot ning teeb pildi info ülejäänud rakendusele kättesaadavaks. Pildivaatleja saab informeeritud nii pildi lugemise infost (kas pildi andmed on kohal, kas on veel lisapunkte saabumas, kas kogu pildi raam on valmis, kas pildi lugemisel saadi viga) kui ka pildi karakteristikutest (mõõdud, muud omadused) niipea, kui need osutuvad määratuteks.

drawImage() meetod, nagu ka teised pildioperatsioonid võtab parameetriks ImageObserver objekti ning tagastab tõeväärtuse selle kohta, kas pilt joonistati oma täies terviklikkuses või mitte. Kui kogu pilti ei saa joonistada, siis kuvab drawImage() pildi kõik osad, mida ta kuvada suudab ning lõpetab. Pildivaatleja tegeleb edaspidisega juba iseseisvalt. Kui terve pilt on kohal võib pildivaatleja teha sellega mida iganes. Kõige tuihemini kutsub ta välja repaint() meetodi.

Detailsemalt puudutame pildivaatlejat pisut hiljem. praegu püüame kasutada klassi Component pakutavaid olemasolevaid vahendeid. Nagu sellest aru võib saada, saab iga komponent kasutada oma isiklikku pildivaatlejat. 


Pisike näide:
 

  class MyApplet extends java.applet.Applet;
    ...
    public void paint(Graphics g) {
      drawImage( myImage, x, y, this );
      ...

Antud rakend käitub, kui pildivaatleja ning kutsub välja repaint()'i pildi ülejoonistamiseks. Aeglase pildiinfo saabumise korral kuvatakse pilti nii mitu korda üle. Sellist ülejoonistamist kontrollivad omadused awt.Image.Incrementaldraw ning awt.Image.redrawrate. Neist esimene näitab, kas pilti andmete lugemise ajal näidatakse ning teine, mitme millisekundi tagant toimub ülejoonistamine.

Skaleerimine ja suurus

drawImage() teine versioon võimaldab pilti vajaliku suurusega esitada. 
      drawImage( myImage, x, y, x2, y2, this );

Siin kuvatakse pilt raami, mis on määratud nende nelja koordinaadiga.

Pildi suuruse teada saamiseks on meetodid getWidth() ning getHeight() et pildi mõõtmeid ei pruugi tema lugemise ajal veel teada olla on ka nende meetodite parameetriks pildivaatleja objekt. Kui pildi suurust ei saa hetkel määrata tagastavad antud meetodid -1 ja informeerivad vaatlejat. 


Graafika tehnikad


Järgnevas vaatleme mõningaid meetodeid kiire ning vilkumisvaba joonistamise teostamiseks. Animatsioonide loomiseks ning ladusaks pildi uuendamiseks peaks järgnevast kasu olema.

Kuna joonistamine võtab aega, ning ajakulu viib viivituste ning ebatäiusliku tulemuseni, siis on meie eesmärk seda joonistamiseks kuluvat aega minimiseerida. Nagu eespool juba veendusime oli TestPattern'il probleeme vilkumisega. See tulenes sellest, et me joonistasime üle suuri värvitud alasid iga kord, kui paint() meetod välja kutsuti. Antud juhul saab vilkumise eemaldada, kui pilt joonistada pildipuhvrisse ning seejärel kopeerida puhver ekraanile. Lähemalt sellest mõni aeg hiljem.

Järgmine näide illustreerib mõningaid pildi uuendamisel üles kerkivaid probleeme. Nagu paljudes animatsioonides, on ka siin tagapõhi ning liigutatavaks objektiks on skaleeritud pilt.


TerribleFlicker rakend ning TerribleFlicker lähtetekst
Kui selles näites pilti hiirega vedada, on märgata nii pildi, kui ka tausta vilkumist. Mis siis juhtus ja mis oli valesti?

Kui me hiirt vedasime, muutusid kaks muutujat (CurrentX ja CurrentY) ning repaint() kutsuti välja pildi uuendamiseks. Selle peale käivitatakse paint() ning tema kuvabki kogu pildi üle.

Meie esimene viga on see, et me küll uuendasime ekraanipilti, kuid me jätsime hooletusse rakendi update() meetodi. update() meetodi vaikimisi sisu on järgmine:

    // default implementation of applet.update
    public void update( Graphics g) {
      setColor( BackgroundColor );
      fillrect( 0, 0, size().width, size()height );
      paint( g );
   }

See meetod lihtsalt kustutab kogu pildi ja joonistab selle siis uuesti. See pole peaaegu kunagi parim lahendus, aga ta on ainus variant uuendada pilti teadmata, kui palju meil on tegelikult vaja uuendusi teha.

Me võime ära jätta selle tagapõhja ülejoonistamise, sest meie rakend teeb seda niikuinii. Uus update() näeks siis välja järgmine:

    public void update( Graphics g) {
      paint( g );
   }

See teeb meie rakendi töö tunduvalt sujuvamaks, aga siiski teeme me veel ülipalju tarbetut tööd.

Väljalõiked

Graphics klassis leiduv clipRect() meetod annab võimaluse piirata graafikakonteksti väiksemale alale. Normaalselt on konteksti joonistust piirav ala kogu vastava komponendi mõõtmetes.

Miks me peaksime joonistatavat ala piirama?. Aga selleks, et uuendada vaid antud (kitsamat) ala. Ko~iki joonistamisi, mis antud alast va"ljapoole ja"a"vad, ignoreeritakse. Teise hea omadusena saab hea realiseerimise korral graafilist konteksti panna a"ra tundma ko~iki va"ljaspool seda ala toimuvaid joonistamisi ning neid siis ignoreerida. Sellisest tegevusest saab keerukate joonistusoperatsioonide korral korraliku aja kokkuhoiu. Meie na"ite korralikuks toimimiseks peame piirama joonistatava ala pisimaks ristkülikuks, mis sisaldab nii "vana" kui ka "uut" pilti.

Joonis 2.

Meie uus ja "targem" update() peaks hoidma oluliselt kokku aega uuendades vaid vajalikku "valja lõigatud" ala. Kuna antud meetodi realiseerimiseks on vaja teha vaid va"heseid muudatusei meie eelmise na"ite koodis, siis olgu uus na"ide eelmise alamklass.


Clipped rakend ning selle lähtetekst
Võisite näha, et antud näide oli küll kiirem, aga siiski vilkus.

Pole just ta"htsusetu lisada, et va"ljakutsed repaint() poole mouseDrag() meetodis pole u"ks-u"heselt seotud AWT poolt tehtavate va"ljakutsetega update() poole. See pole siiski mitte puudus, vaid positiivne joon, mis aitab AWT'l planeerida ning koordineerida joonistusva"ljakutseid.

Saamaks iga hiirekursori koordinadimuutuse peale ka kohe muudetud pilti, on meil kaks vo~imalust. Me kas teeme natuke u"mber mouseDrag() meetodit vo~i paigutame oma su"ndmused teatavasse ja"rjekorda. Esimest neist vo~imalusist ka"sitleme me DoodlePad na"ites. Teise variandi puhul peaksime me mo"o"da hiilima AWT enda su"ndmuseplaneeringu vo~imalustest ning asendama need meie meie enda omadega, ga seda vastutust me siinkohal endale ei vo~ta.

Topeltpuhverdamine

Antud meetod peab meie näidete vilkumise probleemi täielikult lahendama. Me kasutame seda koos eelpool vaadeldud väljalõikamise meetodiga, et paremat tulemust saavutada, kuid antud meetodit on võimalik kasutada ka ilma selleta.

Topeltpuhverdus tähendab, et pilt joonistatakse enne näitamist nn. eeljoonistuspuhvrisse ning siis kopeeritakse valmis töö u"he joonistuska"suga ekraanile.

Joonis 3.

Topeltpuhverduse efekti saame me eelnevas näites vaid pisikesi muutusi tehes. 


DoubleBufferedClipped rakend ning tema lähtetekst

Antud na"ites oli muutuja offScrImg see eeljoonistuspuhver. Ta on tavaline joonistatav Image objekt, mille saamiseks kasutatakse createImage() meetodit. createImage() on sarnane getImage()'le, kuid tekitab ette antud mo~o~tmetega tu"hja pildiala. Edasi saame me oma eeljoonistuspilti, kui tavalist ekraani ala kasutada, ku"sides neilt na"iteks getGraphics() meetodiga graafilist konteksti. Pa"rast seda, kui oleme eeljoonistuspildi valmis saanud saame selle ekraanile kopeerida drawImage() meetodiga.

Iga uue update() puhul tekitame me uue eeljoonistuspuhvri, sest iga kord on meil vaja uut va"ljalo~igatavat ala.

Graphics klassi dispose() meetod vo~imaldab meil graafilise konteksti vabastada, kui me teda enam ei vaja. See on vaid optimiseerimine, millest on kasu, kui teeme keerukaid joonistamisi. Suuremate u"lejoonistamiste ning keerukmate arvutuste puhul ei ole lihtsalt otstarbekas prahikoristaja (carbage collection) peale loota. 


Eeljoonistamine

Eeljoonistamine on kasulik ka muude keerukust nõudva taustainfoga piltide joonistamiseks. Järgnevalt vaatame antud juhtu lihtsa näite varal. 
DoodlePad rakend ning selle Lähtetekst
Antud na"ites kasutasime me joone tekitamiseks drawLine() meetodit mouseDrag() meetodis, et a"ra hoida mo~ningaste hiireliigutuste kadumist, mis selle ta"itmisel update()'s oleks esinenud, kuna AWT repaint()'i ja update't u"ks-u"heses vastavuses va"lja ei kutsu. 

Pilditöötlus



Lipp Kirjeldus
HEIGHT pildi pikkus on valmis
WIDTH pildi laius on valmis
FRAMEBITS raam on valmis
SOMEBITS suvaline hulk punkte on kohal
ALLBITS pilt on valmis
ABORT pildi lugemine katkestati
ERROR pildi töötlemisel saadi viga,pildi kuvamise katse ebaõnnestus


Helitöötlus



WWW teostus: Urmet Liin
1997.a.