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.