Testi

Testi: testi-undo-za-zelvo.py

Ogrevalna naloga

Želvi dodaj metodo square(self, a), ki nariše kvadrat s stranico a. Risanje naj začne v smeri, v katero je obrnjena, v vsakem kotu pa naj se obrne v levo. Po risanju naj stoji želva tam, kot v začetku in naj bo obrnjena v isto smer.

Rešitev

Naloga je zahtevala, da znate razredu dodati metodo.

Znotraj razreda Turtle je bilo potrebno dopisati

def square(self, a): for i in range(4): self.forward(a) self.left()

Obvezna naloga

Želvi dodaj metodo undo(self), ki pobriše zadnjo narisano črto ter postavi želvo na koordinate in smer, ki jih je imela pred risanje te, zadnje črte. Če po tem ponovno pokličemo undo(self), naj se ne zgodi nič.

Za brisanje uporabljajte risar.odstrani(stvar), kjer je argument (stvar) črta, ki jo hočete pobrisati.

Tako pri obvezni kot pri dodatni nalogi naj se undo ukvarja le z narisanimi črtami, ne pa z ostalimi akcijami, kot so obrati in leti.

Rešitev

Da ste lahko uporabili odstrani, ste morali razumeti, kaj je potrebno dati tej funkciji kot argument. Naloga je povedala, da je argument tista stvar, ki jo hočete pobrisati. Torej sama črta. Nekateri ste se ubijali s tem, da ste funkciji odstrani poskušali podati koordinate - tega ni marala rekoč, da ste ji dali preveč argumento - ko to ni šlo, pa ste ji kot argument dali risar.crta(...). Tudi to ni dobro, saj s tem narišete novo črto (in jo pobrišete), stara pa ostane. Razumeti ste torej morali, da metoda risar.crta vrača objekt, ki predstavlja črto. O tem smo govorili na koncu predavanj, na katerih sem predstavljal risarja - spomnite se, kako smo premikali tiste črte po oknu.

Druga stvar, ki jo je bilo potrebno razumeti je, da mora želva za to, da lahko pobriše neko črto in se postavi, kjer je bila pred risanjem, to črto shraniti, poleg tega pa mora shraniti tudi svoje koordinate in smer pred njenim risanjem. Te reči, črto, koordinate in smer, shranimo - kam drugam kot v self. Predstavljajte si, da imamo več želv: vsaka želva se "undoja" po svoje, torej mora imeti tudi svoje podatke za undo.

Ker se "undoja" samo risanje črte - in to le, kadar je pero aktivno, saj sicer ne rišemo - bo potrebno podatke za undo shranjevati znotraj forward, točneje znotraj if self.pen_active:. Ta je po novem takšen:

if self.pen_active: line = risar.crta(self.x, self.y, nx, ny) self.undo_data = (line, self.x, self.y, self.angle)

Rezultat klica funkcije risar.crta shranimo v line, potem pa v self.undo_data zabeležimo to črto, koordinate in kot. Namesto tlačenja v eno samo terko bi lahko uporabili tudi štiri atribute, pisali bi lahko

if self.pen_active: self.last_line = risar.crta(self.x, self.y, nx, ny) self.last_x = self.x self.last_y = self.y self.last_angle = self.angle

Za terko sem se odločil preprosto zato, ker bo skrajšala pot do dodatne naloge.

Da undo_data v začetku ne bi visela "v zraku", v konstruktor dodamo self.undo_data = None.

Undo je potem takšen:

def undo(self): if self.undo_data is not None: crta, x, y, angle = self.undo_data risar.odstrani(crta) self.fly(x, y, angle) self.undo_data = None

Začnimo pri koncu: self.undo_data = None smo napisali zato, da zaporedni klici undo ne bi "undojali" nečesa, kar je že "undojano".

Na začetku preverimo if self.undo_data is not None:; self.undo_data bo enak None, če še nismo ničesar narisali, ali pa smo zadnjo narisano reč že "undojali". Namesto if self.undo_data is not None bi lahko pisali tudi if self.undo_data: ali if self.undo_data != None. Ko gre za primerjanje z None, se svetuje uporaba is; vsi Nonei so isti (ne le enaki). Z različico self.undo_data ni tule nič narobe (alternativa Noneu je terk dolžine 4 in ta je gotovo resnična), vseeno pa se je ne navadite, ker se vam bo kdaj, v kakšnem drugem programu zgodilo, da bo neka spremenljivka vsebovala int ali None in v tem primeru si boste morda želeli razlikovati med 0 in None, čeprav sta oba neresnična. V tem primeru if x: in if x is not None: ni eno in isto.

Nato razpakiramo terko v posamezne reči, odstranimo črto in prestavimo želvo, kjer je bila.

Nekateri ste tu pisali self.x = x in tako naprej, na koncu pa poklicali update. Nekateri niste niti klicali update, temveč ste v svoji metodi undo še "ročno" premikali želvo tako, da ste spreminjali pozicijo self.head in self.body. To je sicer preživelo teste, je pa grda praksa. Kaj bi se zgodilo, če bi tej želvi dodali še noge? Če uporabimo fly, bo že fly poskrbela zato, da bodo šle tudi želvine noge tja, kamor morajo. Če sami, ročno, premikamo želvin trup in glavo v posamičnih funkcijah, pa bi morali potem v vseh teh funkcijah ročno premikati tudi noge. Da ne govorimo o tem, kaj bi se zgodilo, če bi iz razreda Turtle izpeljali nov razred, nov želvo, ki bi se risala na kak popolnoma drugačen način.

Dodatna naloga

Podobno kot obvezna, vendar tako, da lahko undo(self) pokličemo večkrat. Črte naj odstranjuje v obratnem vrstnem redu dodajanja - najprej zadnjo, nato predzadnjo... tako, kot bi delal pravi undo. Če oddate dodatno nalogo, vam seveda ni potrebno posebej oddajati obvezne.

Rešitev

Tu očitno ni dovolj, da shranimo le podatke pred zadnjim risanjem temveč cel seznam podatkov. V konstruktor bomo torej dodali

self.undo_list = []

forward dopolnili z

if self.pen_active: line = risar.crta(self.x, self.y, nx, ny) self.undo_list.append((line, self.x, self.y, self.angle))

metoda undo pa bo

def undo(self): if self.undo_seznam: crta, x, y, angle = self.undo_list.pop() risar.odstrani(crta) self.fly(x, y, angle) self.update()

Seznamova metoda pop() vrne in odstrani zadnji element - v našem primeru podatke, ki se nanašajo na zadnjo črto.

Tu ste se mnogi ubijali z reševanjem v slogu

def undo(self): if self.undo_seznam: risar.odstrani(self.undo_seznam[-1][0]) self.fly(self.undo_seznam[-1][1], self.undo_seznam[-1][2], self.undo_seznam[-1][3]) del self.undo_seznam[-1] self.update()

Še bolj nepotrebno je bilo, da ste imeli več seznamov - enega s koordinatami drugega s črtami... Terke so kul, uporabljajmo jih. Sploh pa bi bil konec semestra že čas, da se jih naučimo razpakirati. Pa tudi pop prezirate po krivici.

"Me zanima, če se da rešiti tako, da ne spreminjaš forward"

Da, vendar ob predpostavki, da imamo samo eno želvo. Zahteva pa, kot sem odgovoril na vprašanje iz naslova, ki ga je nekdo zastavil na forumu, nekaj poznavanja Qt-ja (in pogled v risarja).

Risar uporablja Qt-jev objekt za postavljanje grafičnih objektov (črte, slike, liki, besedilo) na "sceno". Scena je shranjena v risar.widget.scene. Metoda risar.widget.scene.items() vrne seznam vseh objektov na sliki; seznam je urejen tako, da so zadnji objekti postavljeni na začetek. V tem seznamu sta tudi, recimo, kroga, ki predstavljata želvo, poleg tega pa vse črte, ki jih je potrebno pobrisati.

Metoda undo, ki ne zahteva, da karkoli dodajamo v konstruktor in forward, je takšna:

def undo(self): for obj in risar.widget.scene.items(): if isinstance(obj, risar.QGraphicsLineItem): line = obj.line() angle = atan2(line.dy(), line.dx()) self.fly(obj.x(), obj.y(), 90 - degrees(angle)) risar.odstrani(obj) return

Gremo prek seznama objektov, dokler ne naletimo na objekt vrste risar.QGraphicsLineItem, to je, na črto. obj.line vsebuje opis črte (začetne koordinate in tako naprej); njena atributa dx in dy povesta, za koliko gre črta desno in gor. Iz njiju s funkcijo atan2 izračunamo kot. Zakaj ne bi bil primeren običajen atan, preberite na Wikipediji; to je ena od stvari, ki se jih splača vedeti v življenju. Želvo zdaj prestavimo na koordinate začetka črte in jo obrnemo v pravo smer (pri čemer moramo kot pretvoriti v obratno smer, kot ga pretvarja forward. Nato še odstranimo to črto in z return (enako dober bi bil tudi break) končamo funkcijo.

Če bi istočasno risalo več želv, tole ne bi delovalo, ker bi undo ene želve lahko pobrisal zadnjo črto, ki jo je morda narisala kaka druga želva.

Zadnja sprememba: četrtek, 25. marec 2021, 21.33