ELTE Informatikai kar - Prognyelvek portál

Smalltalk

Példaprogram: nagyítós kurzor

A feladat

Ebben a példaprogramban a SmallTalk grafikus osztályait mutatjuk be, egy nagyító funkcióval kiegészített kurzor megvalósításán keresztül. Az implementációhoz használt környezet a Squeak 3.9-es verziója volt.

A kurzor működése:

Megvalósítás

A kurzort a MagnifyingGlass osztály implementálja. Annak érdekében, hogy az alapvető kurzorműködéseket megörököljük, és saját művünket a SmallTalk meglévő kurzoraihoz hasonlóan használhassuk, a Cursor osztályból származtatunk. Továbbá szükségünk lesz két új példányváltozóra, hogy a nagyítandó kör sugarát (radius) és a nagyítás mértékét (ratio) tároljuk.

Új példány létrehozásakor a konstruktorban (#initialize) beállítjuk a két példányváltozót az alapértelmezett értékre. Mivel a Cursor egy speciális Form, így rendelkezik a bits példányváltozóval is, amely a kurzor megjelenítendő képét tartalmazza. Tehát annak érdekében, hogy a választott képet (jelen esetben egy szemüveget) lássuk, a konstruktorban a bits értékét is beállítjuk a megfelelő képre. Ezt legegyszerűbben úgy tudjuk megtenni, ha értékül adjuk neki a read kurzor bits tömbjét. A konstruktorunk tehát a következő:

initialize "Inicializálás default értékekkel" radius := 16. ratio := 2. bits := Cursor read bits

Természetesen definiálnunk kell a példányváltozókat beállító és lekérdező műveleteket is, ezek a szokásos módon tehetők meg.

Kurzorunkat az #activate üzenettel aktiválhatjuk. A műveletben el kell mentenünk az addigi aktív kurzort, és a mi kurzorunkat kell megjeleníteni egészen a jobb egérgomb lenyomásáig. Az aktuális kurzort a Sensor megfelelő műveletén keresztül érhetjük el. A Sensor az InputSensor osztály globális változója, ez felelős a beviteli eszközök eléréséért és kezeléséért. Az aktuális kurzort a #currentCursor üzenet adja vissza. Az #activate metódusunk „kerete” tehát a következő:

oldCursor := Sensor currentCursor. "régi kurzor elmentése egy lokális változóba" … A saját kurzorunk megjelenítése a jobb egérgomb lenyomásáig … oldCursor show "régi kurzor aktívvá tétele"

Saját kurzorunk megjelenítéséhez dupla pufferelést használunk. Ezt a technikát animációknál használják, a lényege, hogy a következő képkockát nem közvetlenül a képernyőre (a képernyőpufferbe) rajzoljuk, hanem egy másik képpufferbe, majd a képkocka megjelenítésekor a két puffer szerepét felcseréljük (esetleg a tartalmukat cseréljük meg). Így az előzőleg, a segédpufferbe megrajzolt kép, vagyis az előző képkockához képest történt változások egyben, egyszerre jelennek meg a képernyőn, melyet a szemünk folyamatosnak észlel. Ezzel szemben, ha közvetlenül a képernyő pufferébe rajzolgatnánk, a rajzoló műveletek eredménye fokozatosan, egyenként megjelenne, amit a legtöbb esetben villogásnak érzékelnénk (ennek oka többek között az, hogy az új képkocka előállítása általában fokozatosan, több lépésben történik, melynek során gyakran olyan elemek is szerepelnek a képen, amelyek a végeredményben eltűnnek, vagy megváltoznak). Annak érdekében tehát, hogy például a nagyító mozgatásakor a nagyított kép ne villogjon, dupla pufferelést kell használnunk. Ezt jelen esetben úgy érhetjük el, hogy létrehozunk, egy, a képernyő méretével megegyező méretű Form objektumot (a példában bgForm lokális változó), és a különféle rajzolásokat ezen végezzük, majd a form teljes tartalmát kirajzoljuk a képernyőre.

A nagyítás során a képernyőre új dolgokat fogunk rajzolni (nevezetesen a nagyított képet), ezért szükségünk van a képernyő „eredeti” tartalmára, azaz a nagyított kép alatti részre, hogy a nagyító mozgatásával, vagy kikapcsolásával vissza tudjuk állítani a korábbi állapotot. Mivel a nagyított kép helye és mérete is dinamikusan változhat, ezért az egyszerűség kedvéért kurzorunk aktiválása előtt a teljes képernyő tartalmát elmentjük egy formba.

Ilyen formot legegyszerűbben a következő módon hozhatunk létre:

oldBg := Form fromDisplay: (Display boundingBox)

A #fromDisplay: üzenet a Form osztály egy konstruktora, amely a képernyő megadott részének tartalmával inicializálja a formot. Paramétere egy téglalap objektum, mely azt a képernyőterületet írja le, amelyet az új formba szeretnénk másolni. A Display globális változó a képernyőt reprezentálja, a #boundingBox üzenet pedig visszaadja a képernyő korlátait, vagyis a fenti kódrészlet a teljes képernyő tartalmával inicializálja az oldBG formot. Ne felejtsük el a kurzor működésének végeztével, vagyis az #activate üzenet végén kirajzolni az eredeti hátteret, ellenkező esetben kilépés után is látható marad a nagyított kép. Egy form képernyőre való kirajzolását (a bal felső sarokba pozícionálva) a #display üzenettel hajthatjuk végre.

oldBg display

A kurzor megjelenítéséhez a Cursor osztálytól megöröklött #showWhile: üzenetet használjuk, mely egészen addig megjeleníti a kurzort, amíg a paraméterként kapott blokk nem terminált. Paraméternek tehát egy olyan blokkot kell átadnunk, amely a jobb egérgomb lenyomásáig fut. A korábban említett Sensor globális változó #yellowButtonPressed üzenete egy logika típusú objektumot ad vissza, amelynek értéke pontosan akkor igaz, ha a jobb egérgomb az utolsó hívás óta le lett nyomva. Így tehát a kurzor megjelenítését az alábbi ciklusban végezhetjük:

super showWhile: [ [Sensor yellowButtonPressed] whileFalse: [ … További funkciók kezelése, nagyítás rajzolása … ]. ].

A nagyító működését – azaz a bal egérgomb lenyomása esetén a nagyított kép kirajzolását és a nagyítás paramétereinek változtatását a megfelelő billentyűk lenyomására – a fenti ciklus magjában valósítjuk meg. A SHIFT billentyű lenyomása esetén, melyet az előzőekhez hasonló módon a Sensor globális változó #shiftPressed üzenetével tudunk lekérdezni, a nagyítandó kör sugarát eggyel megnöveljük. Hasonlóan az ALT lenyomására (#commandkeyPressed) a nagyítás arányát növeljük.

"dupla pufferelés" bgForm display. "a bgForm itt mindent tartalmaz, ami a képernyőn lesz," "tartalma az előző ciklusban, vagy a ciklus előtt jött létre" oldBg displayOn: bgForm. "a segédpuffert előkészítjük a rajzoláshoz, tartalma a kurzor és elemei nélküli háttér" Sensor redButtonPressed ifTrue: [ "..." "bal egérgomb lenyomása esetén megjelenítjük a nagyító ikonját, valamint a nagyított képet (ld. alább)" "..." ] Sensor shiftPressed ifTrue: "SHIFT billentyű lenyomása esetén növeljük a sugarat" [ radius < 50 ifTrue: "érdemes limitálni a maximális sugarat" [ radius := radius + 1. ]. ]. "ALT lenyomására növeljük a nagyítási arányt" Sensor commandKeyPressed ifTrue: [ ratio < 5 ifTrue: "a használhatóság érdekében ezt is limitáltuk" [ ratio := ratio + 0.1. ]. ]. Sensor blueButtonPressed ifTrue: "harmadik egérgomb lenyomására visszaállítjuk az alapértelmezett értékeket" [ radius := 16. ratio := 2. ]

A fenti kód hatékonyságán könnyen javíthatunk, ha különféle megszorításokat vezetünk be a billentyűlenyomások ellenőrzésére, például ha lenyomtuk az ALT gombot (nagyítás mértékének növelése), akkor nem ellenőrizzük ugyanabban a ciklusban, hogy a harmadik egérgombot lenyomtuk-e (paraméterek visszaállítása az alapértelmezett értékre). Ezenkívül rajzolni is csak abban az esetben kell a képernyőre, ha az előző ciklushoz képest változás történt. Ezen ellenőrzéseket a példaprogram egyszerűségének és áttekinthetőségének megtartása érdekében elhagytuk.

A nagyítás megjelenítése két lépésből áll. Első lépésben valamilyen ábrával megjelöljük a nagyítandó képernyőterületet. Ezt az egyszerűség kedvéért két koncentrikus körrel tesszük meg, melyek középpontja a kurzor pozíciója, sugaruk pedig valamivel nagyobb, mint a nagyítandó terület sugara. Ehhez mindenek előtt szükségünk lesz a kurzorpozícióra (SmallTalk terminológiában a kurzor „hotspot”-ja), melyet a Sensor a #cursorPoint üzenet hatására ad vissza. A két kört az alábbi módon rajzolhatjuk meg:

hotspot := Sensor cursorPoint. "kurzor pozíciójának lekérdezése" "a nagyító keretének rajzolása, két körrel" ( Circle new center: hotspot radius: self radius+2 ) displayOn: bgForm at: 0@0 clippingBox: Display boundingBox rule: Form over fillColor: Color black. ( Circle new center: hotspot radius: self radius+5 ) displayOn: bgForm at: 0@0 clippingBox: Display boundingBox rule: Form over fillColor: Color black.

Először létrehozunk egy kör objektumot a kört reprezentáló Circle osztály #new konstruktorával. A kör középpontját és sugarát a #center: és a #radius: üzenetekkel állíthatjuk be, esetünkben a középpont az előzőleg lekérdezett kurzorpozíció, a sugár pedig a kurzor objektumunk radius adattagjánál valamivel nagyobb érték (azért, hogy a kirajzolt kör ne takarjon ki a nagyítandó területből). Mint minden rajzolható osztály, a Circle is a DisplayObject osztály leszármazottja, így rendelkezik a fent használt #displayOn: aDisplayMedium #at: aDisplayPoint #clippingRectangle: clipRectangle #rule: rulelnteger #fillColor: aColor alakú üzenettel, melyek rendre a rajzolás célját, a célon belüli rajzolási pozíciót, a vágási téglalapot, a rajzolás szabályát, és a rajzolás színét írják le. A rajzolás célja jelen esetben a segédpufferünk, azaz a bgForm változó által hivatkozott form objektum. Mivel a kör pozícióját beállítottuk a kurzorpozícióra, így a rajzolás pozíciója a célform origója kell, hogy legyen (természetesen fordítva, azaz a kör középpontjának 0@0-t, és a rajzolás helyének a kurzorpozíciót választva ugyanezt az eredményt kapjuk). Ez a paraméter egy Point típusú objektumot vár. Az ilyen objektumok létrehozására a Smalltalk külön nyelvi elemet is definiál: az x@y literállal létrehozhatjuk az (x,y) koordinátájú pont objektumot. A vágási téglalap szerepe, hogy a rajzolást a cél bizonyos részére korlátozza, erre most nincs szükségünk, így a képernyő (Display globális változó) teljes befoglaló téglalapját megadtuk. A rajzolási szabály azt szabja meg, hogy a cél pixelei hogyan keveredjenek a forrás pixeleivel. Minden szabályt egy-egy egész szám reprezentál, és erre a célra a SmallTalk több konstans műveletet is definiál. A Squeak ezen verziójában ezek a konstansok a Form osztály üzenetein keresztül érhetők el. Jelen esetben azt szeretnénk, hogy a háttér megmaradjon, és csak a körvonal rajzolódjon ki, melyre az „over” szabály alkalmas, azaz a Form osztály #over üzenetét kell használnunk. Rajzolási színnek feketét adtunk meg. További, előre definiált színkonstansok érhetők el a Color osztály üzenetein keresztül.

A második lépés, hogy magát a nagyított képet kirajzoljuk. Megoldásunkban első lépésként meghatározzuk a kurzorpozíció körüli, a nagyítandó területet befoglaló, 2*radius oldalú négyzetet. A Rectangle osztály reprezentálja a SmallTalk téglalapjait, ennek egyik konstruktora a #center: #extent:, melyben a téglalap középpontját (azaz az átlók metszéspontját) adjuk meg, valamint az oldalak hosszát Point típusú objektum formájában.

imageRectangle := Rectangle center: hotspot extent: (2*radius)@(2*radius)

A nagyított kép rajzolása több lépésből áll. Először is el kell mentenünk a nagyítandó képernyőrészt. Ezt csak téglalap formájában tudjuk megtenni, így egy kicsit ügyeskednünk kell a továbbiakban. A mentéshez először létrehozunk egy megfelelő méretű formot, amibe bele fogjuk másolni a képernyő szükséges pixeleit (jelen esetben a bgForm segédpufferből másolunk, ugyanis tartalma a program ezen pontján megegyezik a képernyő hátterével, azaz a kurzorunk nélküli képpel). A másolások absztrakciója SmallTalkban a BitBlt osztály. Paraméterei a cél -és forrásform, egy halftone bitmaszk (esetünkben ez érdektelen), a kombinálási szabály, a célformban, valamint a forrásformban a másolás origója, a másolandó téglalap mérete, valamint megadható vágási téglalap. Fontos megjegyezni, hogy a BitBlt objektumok magát a bittömb másolást reprezentálják, tehát csak leíró objektumok, így létrehozásukkal a másolás nem történik meg. A másolás elvégzésére az osztály #copybits üzenete szolgál.

imageForm := Form extent: imageRect extent depth: bgForm depth. (BitBlt destForm: imageForm sourceForm: bgForm halftoneForm: nil combinationRule: Form over destOrigin: 0@0 sourceOrigin: imageRect origin extent: imageRect extent clipRect: imageForm boundingBox ) copyBits.

Kép nagyítására szerencsére van beépített függvény, így ezt egyetlen üzenet küldésével megtehetjük:

imageForm := imageForm magnifyBy: self ratio.

Következő lépésként meg kell határoznunk a nagyított kép pozícióját a képernyőn. Ezt önkényesen a kurzor pozíciójától jobbra és lefelé, attól mindkét koordinátatengely mentén a nagyítandó terület sugarának háromnegyedével eltolva helyezzük el. Utóbbi biztosítja, hogy a két kör (vagyis a nagyítandó, és a nagyított kép) ne lógjon egymásba. Előfordulhat, hogy a nagyított kép kilóg a képernyőről. Hogy kurzorunkat intelligensebbé tegyük, kilógás esetén egy másik oldalt próbálunk, és ha minden esetben kilóg, akkor azt az oldalt választjuk, ahol a nagyított képből a legtöbb látszik. A rajzolás befoglaló téglalapját kiszámító metódusunk tehát a következő lesz:

calcDisplayRect: hotspot | displayRect minArea newArea returnRect | "létrehozunk egy téglalapot a kívánt pozícióval és mérettel" "origin a téglalap origója, azaz bal felső sarka" displayRect := Rectangle origin: hotspot extent: 2*radius*ratio. returnRect := displayRect. "translateBy üzenet eltolja a téglalapot" "vegyük észre, hogy az üzenetnek egyetlen paramétere van, ez a paraméter egy Point típusú objektum," " mely az eltolásvektort írja le. Point objektum automatikusan létrejön egyetlen Integerből is, " " ekkor mindkét koordinátában az Integer értéke szerepel" displayRect := displayRect translateBy: radius * 0.75. "Ha jobbra lefelé tolva nem lógunk ki a képernyőről, visszatérhetünk, egyébként nézzük a többit" "containsRect vizsgálja, hogy egy téglalap tartalmazza-e a másikat" (Display boundingBox containsRect: displayRect) ifTrue: [ ^displayRect ]. "minimumkeresés a nagyított képből látszó területeken" "az intersect üzenet létrehoz egy új téglalapot, mely a két téglalap metszete" "az area üzenet kérdezi le a téglalap objektum területét" minArea := ( Display boundingBox intersect: displayRect ) area. "megnézzük, mi a helyzet balra le" displayRect := displayRect translateBy: ( 0 - radius*1.5 - (displayRect extent x))@0. (Display boundingBox containsRect: displayRect) ifTrue: [ ^displayRect ]. newArea := ( Display boundingBox intersect: displayRect ) area. "megnézzük, hogy az eddigiek közül melyik lógott ki kevésbé" newArea < minArea ifTrue: [ minArea := newArea. returnRect := displayRect. ]. "balra fel irány." displayRect := displayRect translateBy: 0@( 0 - radius*1.5 - (displayRect extent y)). (Display boundingBox containsRect: displayRect) ifTrue: [ ^displayRect ]. newArea := ( Display boundingBox intersect: displayRect ) area. newArea < minArea ifTrue: [ minArea := newArea. returnRect := displayRect. ]. "jobbra fel irány." displayRect := displayRect translateBy: ( radius*1.5 + (displayRect extent x))@0. (Display boundingBox containsRect: displayRect) ifTrue: [ ^displayRect ]. newArea := ( Display boundingBox intersect: displayRect ) area. newArea < minArea ifTrue: [ minArea := newArea. returnRect := displayRect. ]. ^returnRect

Könnyen észrevehetjük, hogy ügyes paraméterezéssel a négy oldal vizsgálata egy ciklusba összevonható.

Most, hogy a nagyított kép helyét is meghatároztuk, nincs más hátra, mint hogy a képet háttérre rajzoljuk. Első látásra ez teljesen egyszerű feladatnak tűnik, azonban egy kicsit megnehezíti a dolgunkat, hogy a nagyított képünk négyzet alakú. Az eredményben ebből a négyzet alakú képből egy kör alakú szeletet szeretnénk kirajzolni a segédpufferbe úgy, hogy a kör körül az eredeti tartalom, vagyis a háttér megmaradjon. Megoldásunk több részből áll, és a részletek (a felhasznált másolási szabályok) az egyes SmallTalk implementációkban eltérhetnek. Először a nagyított képből kör alakút varázsolunk, vagyis meghagyjuk a körön belül szereplő pixeleket, a körön kívül mindent fehérre festünk. Ehhez a SmallTalk egyik form konstruktorát használjuk, mely egy fehér alapon fekete, kitöltött kört ábrázoló formot hoz létre. Ha egy ilyen formot kétszer ráfestünk a nagyított képre és először az erase, majd az under módot használjuk, pont a kívánt eredményt kapjuk. Az erase eredményeképp a használt Squeak verzióban a nagyított képet kör alakban meghagyja, körülötte a négyzet maradék pixeleit feketére festi. Az under segítségével pedig a kör körüli részt festjük fehérre. Ezután a háttérből kell előállítanunk egy „lukas” képet, vagyis a középső, kör alakú részt fehérre festjük. Ezt szintén két lépésben tehetjük meg: and szabályt használva a fekete kört ráfestjük a háttérképre, ezáltal középen egy fekete színű kör keletkezik. Majd egy fekete alapon fehér kört az under szabállyal ráfestünk az így kapott képre, ezáltal a középső körből fehér lesz. Fekete alapon fehér kör létrehozására nincs nyelvi elem, viszont fehér alapon fekete körből invertálással könnyedén létrehozhatjuk. Az eddigiek eredményeképp tehát rendelkezésünkre áll a nagyított kép kör alakban, körülötte fehér színnel kitöltve, és a háttér, a közepén egy fehér színű lukkal. A kettőt „összemosva”, azaz and szabállyal az egyiket a másikra festve éppen azt kapjuk, amit szerettünk volna. Egyetlen teendőnk még, hogy a nagyított kép köré egy fekete kört rajzoljunk, és az egészet átmásoljuk a segédpufferbe.

drawMagnification: hotspot imageRectangle: imageRect backgroundForm: bgForm "a nagyítás kirajzolását végző metódus" | imageForm blackDotForm oldBg whiteDotForm displayRect | "elmentjük a nagyítandó képet, azaz a kurzor körüli 2*radius oldalú négyzetet" imageForm := Form extent: imageRect extent depth: bgForm depth. (BitBlt destForm: imageForm sourceForm: bgForm halftoneForm: nil combinationRule: Form over destOrigin: 0@0 sourceOrigin: imageRect origin extent: imageRect extent clipRect: imageForm boundingBox ) copyBits. "kinagyítjuk a képet" imageForm := imageForm magnifyBy: self ratio. "meghatározzuk a kinagyított kép helyét" displayRect := self calcDisplayRect: hotspot. "mielőtt kirajzolnánk a nagyított képet, elmentjük a mögötte lévő képernyőtartalmat" oldBg := Form extent: imageForm extent depth: bgForm depth. (BitBlt destForm: oldBg sourceForm: bgForm halftoneForm: nil combinationRule: Form over destOrigin: 0@0 sourceOrigin: displayRect origin extent: imageForm extent clipRect: oldBg boundingBox ) copyBits. "fehér alapon fekete és fekete alapon fehér körök létrehozása a nagyított kép méretének megfelelően." "ezek segítségével vágunk majd ki kör alakú részeket a kör alakú nagyításhoz." blackDotForm := (Form dotOfSize: imageForm extent x) offset: 0@0. whiteDotForm := (Form dotOfSize: imageForm extent x) offset: 0@0. (BitBlt destForm: whiteDotForm sourceForm: nil fillColor: Color black combinationRule: Form reverse destOrigin: 0@0 sourceOrigin: 0@0 extent: whiteDotForm extent clipRect: whiteDotForm boundingBox) copyBits. "a nagyított képből meghagyjuk a kör alakú részt, körülötte fehérre festjük" blackDotForm displayOn: imageForm at: 0@0 rule: Form erase. blackDotForm displayOn: imageForm at: 0@0 rule: Form under. "a régi háttérnek kivágjuk a közepét, és fehérre festjük" blackDotForm displayOn: oldBg at: 0@0 rule: Form and. whiteDotForm displayOn: oldBg at: 0@0 rule: Form under. "összemossuk az előző két képet, ezáltal a nagyított kép mellett látszani fog a háttér" oldBg displayOn: imageForm at: 0@0 rule: Form and. "keret" ( Circle new center: ( ratio * self radius ) radius: ( ratio * self radius ) ) displayOn: imageForm at: 0@0 clippingBox: imageForm boundingBox rule: Form over fillColor: Color black. "a képernyő helyett a pufferbe rajzolunk, ez a puffer fogja tartalmazni az új 'képkockát'," " és ezt rajzoljuk ki egyben" imageForm displayOn: bgForm at: displayRect origin.

Az alábbi kép bemutatja a nagyított kép létrehozásának lépéseit. A felső sorban balra a végleges, nagyított kép a háttérrel együtt látható, középen a nagyított kép, miután a kör körüli részt feketére festettük (erase), jobbra pedig a kör körüli rész fehérre festve (under). Az alsó sorban a nagyított kép alatti háttér különböző fázisai láthatók, jobbra az első fázis, feketével kivágott középső körrel (fehér alapon fekete körrel and mód), balra pedig a középső kört fehérre festve láthatjuk (az előző eredményére fekete alapon fehér körrel under mód). A végleges kép (bal felső) a jobb felső és a bal alsó kép „összemosásából” adódik.