Undo za želvo
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
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:
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
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:
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 None
i so isti (ne le enaki). Z različico
self.undo_data
ni tule nič narobe (alternativa
None
u 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
forward
dopolnili z
metoda undo
pa bo
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
Š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:
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.