A Smalltalk programozási nyelv

Beépített osztályok

Az Object osztály

Mint azt már korábban az osztályok és metaosztályok tárgyalásakor említettük, a Smalltalk rendszerben minden, még az osztályok is objektumok. Az osztályok közös őse az Object osztály, így minden objektum, tehát minden entitás a Smalltalk rendszerben az Object osztály egy példánya. Az Object osztályban definiált műveletekkel tehát minden Smalltalk objektum rendelkezik. Az Object osztály fontosabb metódusai kategóriájuk szerint a következők:

Funkcionalitás ellenőrzés

Egy objektum funkcionalitását az osztálya határozza meg. Az objektumok funkcionalitását kétféleképpen ellenőrizhetjük: annak vizsgálatával, hogy az adott objektum példánya-e (közvetve, vagy közvetlenül) egy osztálynak, vagy hogy az objektum rendelkezik-e egy megadott művelettel:

Összehasonlítás

Lévén, hogy a Smalltalk rendszerben minden információ objektumként jelenik meg, szükség van az objektumok összehasonlítására. A Smalltalk az Object osztály szintjén kétféle összehasonlítást ismer: az ekvivalencia és az egyenlőség vizsgálatát. Az ekvivalencia fogalma osztályfüggetlen, az egyenlőség osztályonként különbözőképen értelmezhető. Két objektum ekvivalens, ha megegyeznek és egyenlőek, ha ugyanazt a komponenst reprezentálják. Az Object osztály összehasonlítást végző metódusai a következők:

Példák:

a := Set new add: 1; add: 2; yourself. b := Set new add: 1; add: 2; yourself. a == b "--> false " a = b "--> true " d := b. d == b "--> true " d = b "--> true " a hash "--> 2588 " 12345 hash "--> 12345 " 1234567890 hash "--> 18898 " 1234567890 hash hash "--> 18898 "

Másolás

az alkalmazásokban gyakran kell egy-egy objektumról másolatot készíteni. A másolás az egyenlőséghez hasonlóan többféleképpen értelmezhető, az osztálytól, a komponensek reprezentációjától függően. Bizonyos esetekben szükség lehet még egy objektum felszíni, vagy épp ellenkezőleg minden részére kiterjedő másolására. Az Object osztály a következő objektummásoló metódusokat definiálja:

A shallowCopy és a deepCopy közti különbséget az alábbi ábra szemlélteti:

Példák:

a := #(1 'ketto' 3) b := a copy a = b "--> true " a == b "--> false "

Indexelt változók elérése

A Smalltalk egy sajátossága az indexelt példányváltozók támogatása. Minden objektumank lehetnek indexelt változói a nevesített példányváltozói mellett. Az Object osztály a következő metódusokat definiálja az indexelt változók elérésére:

Példák:

abc := #(a b c d e f g) abc size "--> 7 " abc at: 5 "--> e " abc at: 1 put: #A "--> A " abc "--> (A b c d e f g) "

Szöveges reprezentáció

Grafikus felület ide vagy oda, nélkülözhetetlen az objektumok szöveges reprezentációja. A Smalltalk Object osztálya megkülönbözteti az emberi olvasásra szánt áttekinthető szöveges reprezentációt a gép számára könnyen feldolgozható precíz leírástól, amely alapján az eredeti objektum könnyen megkonstruálható. Ezzel kapcsolatban a következő metódusokat érdemes kiemelni:

Példák:

halmaz := Set new add: 1; add: 2; add: 3; yourself. abc := #(a b c d e f g) halmaz printString --> 'a Set(1 2 3)' halmaz storeString --> '((Set new) add: 1; add: 2; add: 3; yourself)' abc printString --> '#(#a #b #c #d #e #f #g)' abc storeString --> '#(#a #b #c #d #e #f #g)'

Hibakezelés

Tekintve, hogy a Smalltalk rendszerben fordítási időben csak szintaktikai elemzés történik, a szemantikai hibák kezelésére futási időben kell, hogy sor kerüljön. A leggyakrabban előforduló hiba az, amikor egy objektum nem a feltételezett osztály egy példánya. Ez úgy jelentkezik, hogy előbb utóbb a kritikus objektum olyan üzenetet kap, amelyet nem tud értelmezni, nincsen annak megfelelő metódusa. A hibás helyzetek kezelésére az alábbi metódusokat definiálja az Object osztály:

A beépített osztályok hierarchiája

A Smalltalk igen kiterjedt osztályhierarchiával rendelkezik, és az eddigiekben is sok osztályt bemutattunk már, így most csak a legfontosabb elemekre térünk ki. A beépített könyvtár metódusaiban mindig találunk egy rövid leírást a metódusról, gyakran a metódus kódja is csak más Smalltalk metódusokra hivatkozik, így abból is kitalálhatjuk az eljárás célját. Lehetőség van arra is, hogy megkerestessük azon metódusokat, ahol hivatkoznak erre az eljárásra, így gyakran a felhasználás módjára is kapunk példát.

A leszármazási sor jelölésére az alábbiakban a következő jelölést használjuk: Ős~Leszármazott. Tehát például:
Object~Magnitude~Number~Integer~SmallInteger

A hierarchia csúcsán az Object osztály áll, amelyben definiálták például az egyenlőség(=) és az azonosság (==) műveletét, az indexelt adattagok kezeléséhez szükséges metódusokat, típusvizsgálatokat.

Az Object~Behavior osztályban szerepel a Class és a MetaClass osztály.

Az Object~Boolean osztályban két külön osztályt képez a True és a False. A Smalltalkban máshol is megfigyelhető az a módszer, hogy bizonyos, az osztályhoz tartozó dolgokat inkább minden leszármazottban külön-külön definiáljunk, mert ott azok sokkal egyszerűbben megvalósíthatók. A Boolean esetében például az ifTrue szerkezet a True osztályban a blokkot mindenképpen végrehajtja, a False osztályban pedig, egyszerűen üres utasítás. Van két pszeudováltozónk, true a True és false a False osztály egyetlen példánya.

Az Object~Magnitude osztályban szerepelnek a számtípusok (például racionális számok, Object~Magnitude~Number~Fraction), karakterek, dátum és idő, valamint az Association osztály, amely (kulcs,érték) párok tárolásával asszociatív adatszerkezet megvalósítására szolgál.

Az Object~Collection osztály szolgál az összetett adattípusok megvalósítására. Három alosztály a Bag, a Set és az IndexedCollection.

Az Object~Collection~Set osztály leszármazottja a Dictionary osztály, amely segítségével például az osztályok poolDictionaries részét adhatjuk meg.

Az Object~Collection~IndexedCollection osztály közvetett leszármazottja a FixedSizeCollection~Array, és néhány olyan osztály, amelyeknek jelentésük szerint nagyon máshova kellett volna kerülnie, felépítésük miatt mégis ide kerültek. Például a FixedSizeCollection~ByteArray~FileHandle, vagy az OrderedCollection~Process, amelynek osztályszintű metódusai között interrupt kezelő eljárások is szerepelnek.

Mivel a Smalltalkban minden objektum, így a fordító is. Osztálya az Object~Compiler.

A blokkok reprezentálására az Object~Context, illetve leszármazottja a HomeContext használható. (Bizonyos implementációkban e helyett a BlockClosure osztályt használják.)

File-ok kezeléséhez használható az Object~File és az Object~Directory osztály, amelyek tehát teljesen különálló osztályok, és bőséges metóduskészletet kínálnak a file-ok és könyvtárak kezelésére.

Az Object~Dos osztály példányaival közvetlenül elérhetjük a regisztereket, interruptokat válthatunk ki, stb.

A Smalltalk támogatja Windows-os programok létrehozását, így kezelhetünk ablakokat (Object~Window, Object~Win, ...), menüket (Object~Menu, Object~MenuItem), betűtípusokat (Object~Font).

File-ok adatfolyamként való kezelését valósíthatjuk meg a Object~Stream osztály segítségével.

Az Object~UndefinedObject osztály egyetlen példánya a nil.

Collections (ANSI Smalltalk)

Fogalmi szempontból egy gyűjtemény (collection) nem más, mint bizonyos értékek halmazának vagy sorozatának enkapszulációja önálló értékként (objektumként). Ezen objektum tárolójaként funkcionál a befoglalt objektumok halmazának / sorozatának.
Az Array, String, Set, Dictionaries (Map) osztályok mind Collections-ként vannak implementálva a Smalltalk-ban.
Működése szempontjából egy gyűjtemény azon objektum, amely reagál a felé érkező azon alap üzenetekre, melyek biztosítják a "gyűjtemény viselkedést".
Általában - bár nem szükségképpen - ez azt jelenti, hogy egy "Collection objektum" példánya egy olyan osztálynak, mely a standard Collection osztályból származik.
Az alapvető "gyűjtemény üzenet" a #do:, melynek fogadója egy gyűjtemény és argumentuma egy egy-argumentumos blokk. Válaszként a #do: üzenetre a gyűjtemény végigiterál minden értéken, melyet a gyűjtemény elemként tárol és kiértékeli rájuk az egy-argumentumos blokkot. (Ezen blokk a #do: üzenet argumentuma)
Így minden elemet egyesével beilleszt a blokkutasítás argumentumába, mely elementként végrehajtódik.

Néhány példa:

sum := 0.  #(-35 51 6 -192 278)  do: [:element | sum := sum + element].  ^sum

A példa kiszámítja az értékek összegét, melyek elemei egy tömb literálnak, és beállítja a sum változó értékét ezen összegre.
=>108

'Strings are collections, too!'  do: [:char | writeStream nextPut: char].

A példa kiírja egy String minden karakterét egy Stream-be.

minValue := Infinity positive.  #(-35 51 6 -192 278)  do: [:element | minValue := minValue min: element].  ^minValue

A példa visszatérési értékként a minValue változóba számítja egy tömb elemei közül azon értéket, mely az összes többi értéknél kisebb vagy egyenlő.
=>-192

A #do: üzenet mellett más széles körben használt gyűjtemény üzenetek is léteznek, melyek szintén a tartalmazott elemeken mennek végig.
Ezek a #select:#reject:#collect:#contains:#detect:ifNone: and #inject:into: üzenetek, melyek mind-mind implementálhatók csupán a #do: üzenet segítségével is. Eképp vannak implementálva magában a Smalltalk standard Collection osztályában is. (minden Collection-ből származtatott osztály is rendelkezni fog tehát ezen műveletekkel alapértelmezetten.)

Példa a használatukra, magyarázattal melyik pontosan mit is csinál:

Message pattern:
    aCollection select: aOneArgBlock

#(-35 51 6 -192 278) select: [:n | n < 0]

#select: válaszként visszaad egy új gyűjteményt (általában azonos típusút, mint a fogadó), amely elemei kielégítik a megadott megszorítást. Ezen állítás (mely egy funkció ami egy vagy több argmunetummal Boolean értékre értékelődik ki) egy egy-argumentumú blokkal van reprezentálva, ami kiértékelődik minden egyes elemére a fogadó objektumnak, mint blokk argumentum. Értéke minden "lépésben" true vagy false. A gyűjtemény csak azon elemekkel tér vissza, melyek true-ra értékelik ki a #select: argumentumaként adott logikai kifejezést.
=> #(-35 -192)

Message pattern:
    aCollection reject: aOneArgBlock

#(-35 51 6 -192 278) reject: [:n | n < 0]

#reject: A #select: inverz művelete, ebben az esetben a válasz egy olyan új gyűjtemény, melynek elemei csak azon elemek az eredeti gyűjteményből, melyek nem elégítik ki az argumentumként (egy-arugmentumos blokk formájában) megadott feltételt.
=> #(51 6 278)

Message pattern:
    aCollection collect: aOneArgBlock

'Strings are Collections, too!' collect: [:char | char asUppercase]

#collect: válaszként visszaad egy új gyűjteményt (általában azonos típusút, mint a fogadó), mely úgy konstruálódik, hogy a #collect: argumentumaként adott egy-argumentumos blokk kiértékelődik a gyűjtemény minden egyes elemére. A régi értékek természetesen nem kerülnek módosításra, az eredményként kapott elemek az új gyűjteménybe kerülnek.
=> 'STRINGS ARE COLLECTIONS, TOO!'

Message pattern:
    aCollection contains: aOneArgBlock

'Strings are Collections, too!' contains: [:char | char = $!]

#contains: válaszként megadja vajon a gyűjtemény tartalmaz-e olyan elemet, melyre az argumentumként megadott egy-argumentumos blokk true-ra értékelődik ki.
=> true

Message pattern:
    aCollection detect: aOneArgBlock ifNone: noArgBlock

#('http://www.apple.com/' 'http://planet.smalltalk.org/' 'ftp://elsie.nci.nih.gov/pub/' 'https://www.jpmorgan.com/') detect: [:string | string beginsWith: 'ftp://'] ifNone: ['ftp://localhost/']

#detect:ifNone: válaszként megadja az első olyan elemét a fogadú gyűjteménynek, melyre az első argumentumként megadott (a #detect: kulcsszót követő) egy-argumentumos blokk true-ra értékelődik ki. Ha nincs a feltételt teljesítő érték az elemek között, akkor az argumentummal nem rendelkező blokk értékelődik ki és tér vissza.
=> 'ftp://elsie.nci.nih.gov/pub/'

Message pattern:
    aCollection inject: anObject into: aTwoArgBlock

#(31 -117 208) inject: 0 into: [:sum :element | sum + element]

#inject:into: válasza egy olyan érték, mely a következő eljárás hatására jön létre:
A két-argumentumú blokk (mely az #into: kulcsszót követi) két argumentuma közül az első az #inject: kulcsszó után megadott argumentum értékét veszi fel, míg a második a fogadó gyűjtemény első elemének értékét. Majd a két-argumentumú blokk kiértékelődik, ezen érték lesz a következő lépésben a blokk első argumentuma, míg a fogadú gyűjtemény következő elem a második argumentuma. A végső blokk kiértékelés lesz végül maga a visszatérési érték.
Ez az üzenet általánosan arra használt, amire a példa is utal: kiszámítja egy gyűjtemény elemeinek összegét. (Bár sokkal több mindenre is felhasználható.)
=> 122

Vannak még további általánosan elterjedt üzenetek, melyeket gyűjteményeknek küldhetünk - bár néhány gyűjtemény-típus nem feltétlenül tud hozzájuk rendelni valódi jelentéssel bíró választ:

Message pattern:
    aCollection size

'How many characters are in this string?' size

#size válaszként megadja a fogadó gyűjtemény által tartalmazott elemek számát.
=> 39

Message pattern:
    aCollection at: aKeyOrIndex

#('one' 'two' 'three') at: 2

at: válaszként megadja a fogadú gyűjtemény azon elemét, melyet a megadott index vagy kulcs azonosít. (Szintaktikailag nincs különbség a két fogalom közt, de a megvalósításban, illetve a háttérszerkezetben igen nagy eltérések lehetnek!)
A fogadó vagy egy SequenceableCollection (e.g., a String) vagy egy KeyedCollection (e.g, a Dictionary) kell legyen.
=> 'two'

Message pattern:
    aCollection at: aKeyOrIndex put: anObject

aDictionary at: #surName put: 'Flintstone'

at:put: a #put: kulcsszót követő argumentumot letárolja, mint a fogadú gyűjtemény egy elemét, beillesztve azt az #at: argumentum által megadott index vagy kulcs pozícióra. (Szintaktikailag nincs különbség a két fogalom közt, de a megvalósításban, illetve a háttérszerkezetben igen nagy eltérések lehetnek!)
Az üzenet visszatérési értéke a #put: kulcsszót követő argumentum értéke lesz.
A fogadó vagy egy SequenceableCollection (e.g., a String) vagy egy KeyedCollection (e.g, a Dictionary) kell legyen.
=> 'Flintstone'

Message pattern:
    aCollection add: anObject

aSet add: 42

"#add: letárolja az argumentumként kapott objektumot, mint a fogadú gyűjtemény új elemét, az utolsó pozícióra illesztve azt rendezett esetben. Ha a fogadó halmaz szemantikát követ, akkor az új elem csak akkor kerül be a gyűjteménybe, ha értéke még nincs jelen. Az üzenet visszatérési értéke maga az argumentum értéke lesz.
A fogadó nem lehet egy KeyedCollection (e.g, a Dictionary), de növelhető Collection-nek kell lennie (e.g, not an Array or String).
=> 42

Grafikus osztályok

A felületről

A Smalltalk tervezésekor fontos szempont volt, hogy a nyelv egy interaktív, grafikus fejlesztői környezetet foglaljon magába. A nyelv megalkotói ennek megvalósítására fejlesztették ki a manapság elterjedt, ablakokat, ikonokat, menüket és egeret használó (WIMP – Window-Icon-Menu-Pointer) grafikus felület elődjét. Ez a grafikus modell azóta alapjában véve nem változott, de bizonyos részei implementációfüggőek, így a lent bemutatott beépített osztályok az egyes Smalltalk verziók esetén kisebb részletekben eltérhetnek, illetve új osztályokkal és szolgáltatásokkal egészülhetnek ki.

A Smalltalk grafikus modellje a képeket kétdimenziós tömbként (kezdetben még csak kétszínű kijelzők voltak, így bittömbként) fogja fel, melyben az egyes elemek a megfelelő pixel színét jelentik. A modell széleskörű grafikus eszköztárat nyújt, melyben a következőkre van lehetőségünk:

 

A Point és a Rectangle osztályok

A Point osztály kétdimenziós pontok, a Rectangle osztály kétdimenziós téglalapok leírására szolgál. Az egyes pixelek egy képen, vagy a képernyőn belül a Point osztály példányai segítségével érhetők el. Hasonlóan, pixelek téglalap alakú területének elérésére a Rectangle osztály objektumait használjuk.

A Smalltalk koordinátarendszerében az origó a bal felső sarokban található, az x-tengely jobbra nő, míg az y-tengely pozitív iránya a lefelé irány.

Pontok létrehozására külön nyelvi elem is van a Smalltalkban: a @ bináris operátor segítségével egy új pont objektumot hozhatunk létre. Ugyanerre szolgál az osztály #x: #y: üzenete is. Ügyeljünk arra, hogy a Point mindössze egy rendezett pár, létrehozáskor a paraméterekre semmilyen ellenőrzés nem hajtódik végre, vagyis az egyes koordináták típusa bármi lehet (például Point, sőt, akár egy blokk objektum is). Ezenkívül az Integer objektumokat az #asPoint művelet segítségével konvertálhatjuk Point-ra, az eredmény egy olyan pont lesz, melynek mindkét koordinátája megegyezik az Integer objektum értékével.

Téglalap létrehozására több lehetőségünk van. A leggyakrabban használt konstruktorban (#corner: #extent: ) a téglalap bal felső sarkát, és az oldalak hosszát adjuk meg, Point objektumokként. Itt is ügyeljünk arra, hogy a Rectangle objektumok is mindössze rendezett párok, vagyis létrehozáskor semmiféle ellenőrzést nem végez a rendszer a paraméterekre, nem vizsgálja, hogy azok valóban Point típusú objektumok-e. Hasonlóan azt sem ellenőrzi, hogy a téglalap érvényes-e, azaz a bal felső sarka nincs-e a jobb alsó sarok alatt, vagy tőle jobbra. Új téglalap létrehozására lehetőségünk van még az origó (azaz a téglalap középpontjának) és a bal felső sarok, az origó és a méretek, illetve a bal, jobb, felső, alsó koordináták megadásával is. Ezenfelül, interaktív módon is létrehozhatunk téglalap objektumokat. A #fromUser hatására a rendszer a felhasználótól várja a téglalap határainak kijelölését a képernyőn, míg az #originFromUser: extentPoint üzenet hívásával a téglalap origóját kell kijelölnünk, a méretet pedig az extentPoint határozza meg. Az előbbi műveleteket kiegészíthetjük egy Point típusú grid paraméterrel, mely azt határozza meg, hogy a téglalap határainak milyen felbontású rácsra kell esnie, azaz a felhasználó által kijelölt téglalap csúcsait a legközelebbi rácspontokhoz igazítja, melyek koordinátái a grid koordinátáinak többszörösei. A már meglévő téglalapok határainak módosítására is van lehetőség, valamint lekérdezhetjük a csúcsait, oldalainak felezőpontjait, illetve a területét.

A pont és téglalap objektumok kezelésére a nyelv rengeteg hasznos műveletet biztosít. A Point objektumokra definiált a legtöbb aritmetikai művelet, azaz a +, -, *, /, // és az abszolút érték (#abs), melyek mindegyike koordinátánként hajtódik végre. Ezenkívül az előbbi műveletek pontok és egész számok között is értelmezve vannak. Pontok között az összehasonlító operátorok is definiáltak, egy pont akkor kisebb egy másik pontnál, ha tőle balra és felfelé található (vagyis mindkét koordinátája kisebb). Hasznos lehet a pontok polárkoordinátás alakjának meghatározása is, a Smalltalk erre az #r és a #theta üzeneteket biztosítja, előbbi a helyvektor hosszát (a pont origótól való távolságát), utóbbi az x tengellyel bezárt szöget adja meg radiánban. A Point osztályban még néhány hasznos műveletet találunk: két pont távolsága, az objektumnak két másik pont által meghatározott szakaszhoz legközelebbi pontja, skaláris szorzat, normálvektor, valamint a pont irányába mutató egységvektor meghatározása, melyek rendre #dist: , #pointNearestLine: #to: , #dotProduct: , #normal, és #unitVector.

Mivel egy interaktív, ikonokat, menüt és egeret használó grafikus felületen annak meghatározására, hogy a felhasználó milyen műveleteket hajtott végre, mikre kattintott rá, miket jelölt ki, leginkább pontok és téglalapok közötti viszonyt megállapító műveletek szükségesek, a Smalltalk rengeteg ilyen metódust definiál. Lehetőségünk van például meghatározni, hogy egy téglalap tartalmaz-e egy másik téglalapot (#contains: ) vagy egy pontot (#containsPoint: ), két téglalap metszi-e egymást (#intersects: ), meghatározni a metszeteket, a különbségeket stb. Ezen felül, szintén elsősorban az interakció támogatására számos művelet áll rendelkezésünkre téglalapok és pontok transzformálására (pl. skálázásra, eltolásra, stb).

 

Képek létrehozása és kezelése

Smalltalkban a képeket a Form osztály objektumai reprezentálják, melyek többek között a kép pixeleinek leírására használt kétdimenziós tömböt tárolják. A képeken végzett műveletek leírására külön osztályt, a BitBlt-t vezették be. Az elnevezés a BitBlt utasításból ered, amely a Xerox Alto egyik nagyon hatékony blokkmozgató utasítása. Ez az egyik első olyan gép volt, mely támogatta a Smalltalkot. Mivel a nyelvben a képek kétdimenziós tömbök (a színes képernyők megjelenéséig bittömbök), így a leghatékonyabb módja a képek manipulálásának a résztömbökön, blokkokon végzett műveletek. A Smalltalk minden grafikus művelete leírható bitblokkok mozgatásával, vagyis egy forrásformból egy célformba való mozgatással (pl. képernyőre kirajzoláskor a célform maga a képernyő), melyeket a BitBlt objektumok írnak le.

A BitBlt objektumok a következőket tartalmazzák:

  • A forrás origója és mérete (source origin, extent): a forrás origója és a méret együtt meghatározzák a forrás formnak azt a részét, amelyből a másolás történik.

  • A cél origója (destination origin): az extent-tel együtt meghatározza a célformnak azt a részét, ahová történik a másolás.

  • Vágótéglalap (clipping rectangle): a forrásform meghatározása mellett megadhatunk egy téglalapot, a célformban az ezen kívül eső pixelértékek nem módosulnak.

BitBlt objektum létrehozásakor a fenti paraméterek mindegyikét meg kell adnunk (az egyes implementációk ettől eltérhetnek), az üzenet formája #destForm: #sourceForm: #halftoneForm: #combinationRule: #destOrigin: #sourceOrigin: #extent: #clipRect: . A megadott értékek utólag módosíthatók a különféle metódusok segítségével.

A BitBlt által leírt műveletet a #copyBits üzenet végzi el. A #drawFrom: startPoint #to: endPoint üzenettel egy vonal mentén is rajzolhatunk, melynek végpontjait a paraméterek adják. A közbülső pontokat a metódus a Bresenham algoritmussal határozza meg, és minden pontra meghívja a copyBits műveletet. Ennek a műveletnek a segítségével könnyen tudunk például különféle vonalstílusokat rajzolni.

Kirajzolható objektumok – a DisplayableObject osztály

A DisplayObject absztrakt osztály a kirajzolható osztályok közös interfésze. Ennek megfelelően a következő szolgáltatásokat biztosítja:

  • Kirajzolás egy DisplayMedium objektumra transzformációval, vagy anélkül. A DisplayMedium osztály írja le azt a protokollt, amely azokat az objektumokat jellemzi, amikre a Smalltalkban rajzolni lehet. Tehát DisplayObject típusú objektumok csak valamely display medium-ra rajzolhatók. Minden display medium maga is display object, így display medium is kirajzolható egy másik display mediumra.

  • Befoglaló doboz kiszámolása.

  • Átskálázás és tükrözés, valamint offset megadása. Érdemes megjegyezni, hogy ezeket a leszármazott osztályok különbözőképpen implementálhatják, egy objektum transzformálásakor bizonyos esetekben új objektum jön létre (ilyenek pl. a Path és leszármazottainak objektumai), míg más esetekben (pl. a Form esetében) az üzenetet fogadó objektum módosul.

Rajzoló műveletek:
#displayOn: aDisplayMedium #at: aDisplayPoint #clippingRectangle: clipRectangle #rule: rulelnteger #mask: aForm – ez a művelet a paraméterként megadott display mediumra, a megadott pontba kirajzolja az üzenet fogadóját. A további paraméterek megegyeznek a BitBlt objektumoknál látottakkal, ezek írják le a blokk másolásának pontos módját. Az egyes paramétereknek implementációtól függően lehetnek alapértelmezett értékeik (vagyis Smalltalk terminológiában az osztály objektumai rendelkeznek egy másik üzenettel is, mely abban különbözik a fentitől, hogy néhány paraméter nincs jelen, implementációja pedig a fenti üzenet valamilyen paraméterértékekkel való hívásával történik). A talán leggyakrabban használt ilyen üzenet a #display, vagy a #displayAt: , melyek a képernyő bal felső sarkába, illetve egy adott pontjára rajzolják az üzenet fogadóját. A kirajzolásnak ezenkívül különböző, transzformációkkal vegyített változatai is vannak, melyek leírják, hogy a kirajzolt kép az eredetihez képest hogyan módosuljon (pl. eltolás, átskálázás stb.).

A display object befoglaló téglalapjának kiszámítására a #computeBoundingBox üzenet szolgál, a már kiszámolt téglalapot a #boundingBox üzenettel kérdezhetjük le. Hasonlóan, a téglalapnak különböző adatai is lekérdezhetőek: a mérete, szélessége és magassága, melyeket rendre az #extent, #width és a #height adnak vissza.

Minden display object rendelkezik egy offsettel, mely azt mondja meg, hogy kirajzoláskor a rajzoló műveletben megadott koordinátához képest hol legyen a kép bal felső sarka. Ez például kurzorok esetén hasznos, mikor a képernyő egy adott pontján szeretnénk megjeleníteni a kurzort oly módon, hogy a képpontra ne a kurzor képének bal felső sarka essen, hanem az ábrázolt kép egy másik pontja, például a nyíl hegye. Ilyenkor nincs más dolgunk, mint beállítani az offsetet a megfelelő értékre, és ezt követően minden kirajzoláskor a célpixelre a kurzor képének bal felső sarka helyett a kívánt pontja kerül. Az offsetet explicit módon is megadhatjuk az #offset: üzenettel, valamint különböző műveletek segítségével módosíthatjuk, például átskálázhatjuk, vagy eltolhatjuk egy adott értékel.

A DisplayObject osztály még egy érdekes művelettel rendelkezik, mely egyszerű animációk készítését hivatott szolgálni. A #follow: locationBlock #while: durationBlock üzenet hatására a kép a locationBlock által leírt pozícióban fog újra meg újra kirajzolódni egészen addig, amíg a durationBlock igaz értéket ad vissza. Továbbá, leginkább ablakok és kurzor mozgatás után történő rajzolásának megkönnyítésére két műveletet biztosít, melyekkel könnyedén visszaállítható a korábbi háttér. A #backgoundAt: location üzenettel lekérdezhetjük a location helyen található hátteret, míg a #moveTo: newLocation #restoring: backgroundForm üzenettel pedig a kép elmozdítása után visszaállítható a korábbi háttér.

 

A DisplayMedium osztály

Az osztálynak azon kívül, hogy a DisplayObject típusú objektumok csak az ilyen típusú objektumokra rajzolhatnak, más szerepe is van.

Az egyik ilyen, hogy műveleteket biztosít a kép kitöltésére, például a #black üzenet hatására a kép minden pixele fekete lesz. Hasonlóan színezhető a kép egy része, ekkor egy téglalap paramétert is meg kell adnunk, amely leírja a kitölteni kívánt részt. Lehetőségünk van még adott mintával (textúrával) való kitöltésre is, ezt implementációtól függően valamely #fill: kezdetű üzenettel tehetjük meg.

Az osztály másik fontos szolgáltatása, hogy a kép köré keretet rajzolhatunk (ami jelen esetben nem tényleges rajzolást, hanem a kép reprezentálására használt kétdimenziós pixeltömb módosítását jelenti). Szintén implementációtól függően, a #border: kezdetű üzenetekkel rajzolhatunk keretet, különböző vastagsággal, színnel, vagy mintával, illetve lehetőség van nem egyforma szegélyű keret rajzolására is.

Az osztály ezenkívül definiál két alacsonyszintű műveletet is, melyek a BitBlt copyBits és drawLine műveletével azonos módon a kép két része között másol, illetve rajzol.

 

A Form osztály

A Smalltalkban a Form osztály reprezentálja a grafikus képeket. A DisplayMedium, és rajta keresztül a DisplayObject leszármazottja, így rendelkezik az ott ismertetett szolgáltatásokkal.

A Form osztály a reprezentált kép leírására adattagokat is bevezet: a height és a width tartalmazza a kép magasságát és szélességét pixelben mérve, a bits tömb pedig a kép pixeleit írja le. Mivel az osztály a DisplayObject leszármazottja, így a reprezentált képek offsettel is rendelkeznek.

A pixelek leírására használt kétdimenziós bits tömb típusa és megvalósítása implementációfüggő. Minden implementáció lehetőséget ad a tömb elemeinek közvetlen elérésére és manipulálására, melyet a Form objektumok műveletein keresztül közvetve is megtehetünk, azaz a Form is biztosít olyan üzeneteket, melyek a bits tömb egyes elemeit külön-külön érik el.

Formok létrehozása kétféleképpen történhet. Az egyik lehetőség, hogy a téglalapoknál látott módon és szintaktikával a felhasználó jelöli ki a képernyőn a kép határait, a bits tömbbe pedig a képernyő megfelelő részének tartalma kerül. A másik lehetőség, hogy a bits tömböt explicit megadjuk. Ez utóbbit segítendő néhány hasznos műveletet is definiál az osztály. Például a #dotOfSize: diameter üzenettel egy új Form jön létre, mely egy diameter átmérőjű kört ábrázoló képet reprezentál. Mindkét, előbb felsorolt módszert már meglévő objektumokra is alkalmazhatjuk, a reprezentált kép lecserélésére.

A képek különféle transzformációira is lehetőségünk van. Az alább felsorolt műveletek mind új formot adnak vissza. A #magnifyBy: scale és a #shrinkBy: scale a kép nagyítására, illetve kicsinyítésére szolgál, míg forgatni a #rotateBy: angleSpecification üzenettel tudunk. A forgatás csak 90 fok többszöröseivel megengedett, a paraméter írja le, hogy hányszor 90 fokkal forgatunk. A #reflect: specificationPoint szolgál a tükrözésre, a paraméter megfelelő koordinátája 0, ha az adott tengely mentén nincs tükrözés, 1 egyébként. A fentieken kívül még kitöltés művelet is van, mely a megadott maszkkal kitölt egy zárt alakzatot, melynek pontja a megadott paraméter. Alakja: #shapeFill: aMask #interiorPoint: interiorPoint.

 

Speciális Form-ok

Az InfiniteForm tulajdonképpen egy végtelen kiterjedésű kép, és leginkább háttér rajzolására használjuk. Új objektumot a #with: üzenettel hozhatunk létre, paramétere a minta, amely a végtelenségig ismétlődik. Fontos megjegyezni, hogy az osztály a neve ellenére nem a Form leszármazottja, hanem közvetlenül a DisplayObject gyereke, így csak az alapvető rajzoló funkciókat örökli (de ez éppen elég, például végtelen kiterjedésű kép esetén nincs értelme szegélyt rajzolni). Viszont az ismétlődő minta Form objektum, tehát erre minden alkalmazható, amit az imént láttunk.

A ColorForm a Form osztály leszármazottja, mely színes képek kezelését is lehetővé teszi.

 

Kurzorok és a felhasználói interakció

A grafikus kurzorok a Smalltalkban a Cursor osztály példányai. Az osztály a Form leszármazottja, így rendelkezik annak minden szolgáltatásával. Ezen felül, mivel a kurzorok a felhasználó irányába történő visszajelzés fontos eszközei, így ennek támogatására további műveleteket is bevezet.

A Smalltalk Cursor minden esetben 16x16 pixel méretű képet jelképez. Rajzoláskor az offset értékének itt különösen fontos szerepe van, az offset segítségével érhető el, hogy a kurzor megfelelő pontja essen a rajzolási pontra.

Kurzorok létrehozása egyrészt a Formnál látott módon, a kép és a további adattagok explicit megadásával történhet, emellett az osztály több osztályszintű konstanst (unáris üzenetet) is definiál, melyek a különböző kurzorok létrehozását teszik lehetővé. Az alábbi ábrán ezek közül néhány látható.



Egy időben csak egyetlen kurzor lehet aktív. Aktív kurzor az, amelyet az egérrel irányíthatunk. Ennek vezérlésére az InputSensor osztály használatos, mely a beviteli eszközök kezelésére létrehozott interfész (a Smalltalk billentyűzet és háromgombos egér meglétét feltételezi). Az InputSensor osztály közvetlenül az Object leszármazottja. A beviteli eszközökhöz való hozzáférés a Sensor globális változón keresztül történik, mely az osztály egyik példányára mutat. Az aktuális kurzor lekérdezésére és beállítására a #currentCursor és a #currentCursor: newCursor üzenetek szolgálnak. A #cursorPoint és a #cursorPoint: aPoint szolgál a kurzor által mutatott pozíció lekérdezésére és beállítására. A felhasználóval való interakciót támogató műveletek között többek között olyanok találhatók, melyek lekérdezik, hogy történt-e egérgomb lenyomás (pl. #anyButtonPressed), vagy amelyek egy adott interakcióra várnak (pl. #waitClickButton egérkattintásra vár, majd visszaadja a kattintás helyét). Az aktív kurzort nem csak az InputSensor, hanem a Cursor műveletein keresztül is beállíthatjuk: a #show művelet a kurzort aktívvá teszi, a #showGridded: gridPoint aktívvá teszi és mindig a megadott rácsra illesztve jeleníti meg, míg a #showWhile: aBlock a blokk terminálásáig teszi aktívvá a fogadó objektum által reprezentált kurzort.

 

A képernyő: a DisplayScreen osztály

A képernyők reprezentálására a DisplayScreen osztály szolgál, mely a Form leszármazottja. A képernyőn aktuálisan látható képet a Display globális változó által mutatott példány reprezentálja. A DisplayScreen osztálynak lehet több példánya is, ami például animációnál lehet hasznos, a dupla pufferelés nagyon könnyen megvalósítható a segítségével (kirajzoljuk a Displayt, majd egy másik DisplayScreen objektumba kirajzoljuk a következő képkockát, végül megcseréljük a két képet).

Az osztály műveletei között szerepel az aktuális képernyő beállítása, melyre a #currentDisplay: aDisplayScreen szolgál. A #displayExtent: aPoint üzenet küldésével beállíthatjuk az objektum képméretét (amely egy logikai méret, vagyis eltérhet a képernyő fizikai méretétől). Valamint érdemes még megemlíteni a #flash: aRectangle üzenetet, amely felvillantja a képernyőnek a paraméterben leírt téglalap méretű részét (ezzel pl. kurzorvillogást könnyű implementálni).

Tollak: a Pen osztály

A Pen egy speciális BitBlt, vagyis rajzolási műveleteket reprezentál. A toll egy olyan rajzeszköz, amelyet bárhová elhelyezhetünk a képernyőn, áthúzhatjuk máshová, vagy adott irányba eltolhatjuk. A mozgások közben, ha a toll nincs felemelve, a mozgás mentén vonalat húz maga után. Ennek megfelelően az osztály a BitBlt attribútumai mellé még a következőket veszi fel:

A Pen rendelkezik a paraméter nélküli #new konstruktorral is. Az így létrehozott toll le van eresztve, a teljes képernyőre rajzol, és észak felé néz. Az osztály a különböző attribútumok állítására természetesen rendelkezik a megfelelő metódusokkal, vagyis a toll felemelése (#up) és leeresztése (#down), mozgatása (#goto: aPoint), elhelyezése (#place: aPoint). Elhelyezés esetén semmiképp sem húz vonalat. Ezenkívül a Smalltalk a Pennel átvette a LOGO műveleteit is, vagyis a különböző irányú elfordulásokat és mozgásokat.

Példaprogram: nagyítós kurzor

Itt található egy példaprogram, mely egy nagyító funkcióval ellátott kurzor elkészítésén keresztül mutatja be a fenti grafikus osztályokat és alkalmazásukat.

Grafikus osztályok – Morphic

A felületről

A Morphic-ot a Smalltalk-80 imént bemutatott Model View Controller grafikus felületét hivatott leváltani. A Squeak környezetben GUI-programozáshoz a Morphic már alapértelmezetten rendelkezésre áll, a 3.8-as verziótól kezdve pedig az MVC-t ki is vették belőle. Eredetileg a Self programozási nyelvhez készült, innen lett átemelve a Squeak-be.

A Morph osztály – a kiindulási pont

A Morphic-ban minden, a grafikus felülethez tartozó objektum a Morph osztályból származik. (Az osztály neve a görög “alak, dolog” szóból ered.) Mint említettem, a Squeak-ben a Morphic az alapértelmezett GUI-programozási módszer – olyannyira, hogy a felület minden megjelenített eleme (ablakok, menük stb.) egy-egy morph. Sőt, maga az egész Squeak ablak is egy, a Morph-ból származtatott osztály: a World. De mindez most csak azért érdekes, hogy látni lehessen, a Morphic egy mindent átfogó, uniform módszer.

Egy morph-hoz hozzá lehet adni al-morph-okat (submorph). Egy submorph ugyanúgy teljes értékű morph, tehát neki is lehetnek al-morph-jai. Egy morph-nak akárhány submorph-ja lehet. Ha egy morph-ot mozgatunk, a submorph-jai is ugyanúgy vele mozognak, de kódból lehet külön egy submorph-ot is mozgatni. Egy morph-hoz bármikor adhatunk és el is vehetünk submorph-ot.

Hogy jobban megértsük, mit is jelent a submorph, vegyünk egy egyszerű példát a TicTacToe példaprogramból (a teljes programot ld. Lentebb). Egy játék végén felugrik egy ablak, amely kiírja, hogy az egyik játékos győzött. Ez az ablak a következőképpen néz ki:

Maga az ablak természetesen egy Morph, amelyen a kiírt szöveg egy submorph, az újraindításhoz használt gomb szintén. Az újraindítás gombon a “Restart” felirat pedig a gombnak egy submorph-ja.

Az osztály fontosabb attribútumai:

Egy morph-ot az openInWorld metódus segítségével jeleníthetünk meg. A neve alapján ez az eljárás a “World”-ben, tehát a Squeak ablakban megnyitja a definiált morph-ot.

Eseménykezelés

Bármely Morph képes eseménykezelésre. A kezelhető események többek között az egérműveletek (kattintás, dupla kattintás, gomb lenyomása, a kurzornak az objektum fölé mozgatása... stb.), “drag n drop” (vonszolás), valamely billentyű lenyomása.

Vegyük példaképp a “mouseDown”, azaz egy egérgomb lenyomása akciót. Először is, ha azt szeretnénk, hogy a morph-unk kezelje ezt az eseményt, felül kell definiálni a handlesMouseDown: anEvent műveletet. Ez a művelet mindössze visszaad egy igaz vagy hamis értéket attól függően, hogy az adott eseményhez létezik-e eseménykezelő. Alapértelmezettként ez a függvény hamis értéket ad vissza. Ha felüldefiniáljuk úgy, hogy igazzal térjen vissza, az akció megtörténtekor automatikusan meg fog hívódni a mouseDown: anEvent művelet, amelyet nyilván szintén a mi dolgunk felüldefiniálni – ez lesz az eseménykezelő. Az anEvent paraméter egy MouseEvent típusú objektum. Az eseménnyel kapcsolatos mindenféle hasznos információt tartalmaz: többek között a kurzor pozíciójának koordinátáit, illetve azt, hogy melyik gomb lett lenyomva.

Beépített osztályok

Az események osztályai:

A Smalltalk-fejlesztők jónéhány, gyakran használt osztályt definiáltak a Morphic segítségével, amelyek a Squeak-ben megtalálhatóak.

Néhány, az ablakok kezeléséhez konkrétan kapcsolódó példa: