Kirjautuminen

Haku

Tehtävät

Koodit: Python: Valikko

Kirjoittaja: ZcMander

Kirjoitettu: 09.01.2008 – 28.10.2012

Tagit: grafiikka, kirjaston käyttö, kirjasto, koodi näytille, vinkki

Valikkoluokka käsittelemään valikon selausta ja asetuksien muuttamisia. Itse valikkoluokka on toteutettu MVC:llä (ei tosin täydellisellä, koska view ei saa read-only oikeutta modeliin ollenkaan) joten oman valikon piirturin teko pitäisi olla riittävän helppoa. Itse valikko sisältää tietenkin valikon kohtia (MenuItem(s)), joita moduulissa on luotu seuraavia:

- DummyMenuItem - Tulostaa kyseisen valikon kohdan nimen, kun valitaan (choice())
- ChoiceMenuItem - Mahdollisuus antaa lista joiden sisältä käyttäjä voi valita haluamansa (esim. resoluutiolistasta oma resoluutio)
- BackMenuItem - Mahdollisuus mennä valikossa edelliseen valikkoon, voidaan toteuttaa myös näppäimenpainalluksena
- SubMenu - Itse valikko, voidaan lisätä myös toisen valikon sisään

Tietenkin omia valikon kohtia on oikeastaan pakkokin tehdä, mutta valikko-luokka onkin suunniteltu sitä varten.

Esimerkki jäi hieman vähemmälle kommentoinnille, mutta tärkein koodi luokan käytön kanalta on create_menu-funktiossa ja näppäinpainalluksien tarkistuksessa.

Esimerkki ja moduuli vaatii toimiakseen pygame:n.

# -*- coding: utf-8 -*-

import pygame

UP     = 1
DOWN   = 2
CHOICE = 3
BACK   = 4

class BackMenuItem:
  """Takaisin-kohta valikossa"""
  def __init__(self, name, menu):
    self.name = name
    self._menu = menu

  def __call__(self):
    global BACK
    self._menu.send_signal(BACK)



class ChoiceMenuItem:
  """Valikon kohta, jossa pystyy valitsemaan tietyn kohdan joukon sisältä,
  esimerkiksi resoluutiolistasta sopiva resoluutio"""
  def __init__(self, menu, key, name, choices, default=0):
    """
      menu    = CMenu    viittaus luokkaan, jotta voidaan lisätä asetuksien
                         listaan
      key     = string   millä nimellä asetus löytyy asetuksista
      name    = string   millä nimellä valinta löytyy valikosata, huomaa
                         että nimen jälkeen tulee vielä ": " ja valittu kohta
      choices = list     lista mahdollisista valinnoista, tulee olla muotoa:
                          ["avain", arvo, "avain2", arvo2, ...]
    """

    self._base_name = name
    self._choices = choices

    self._choice  = default
    self._set_name()

    menu.attach_to_key(key, self)

  def _set_name(self):
    """Asettaa kohdan nimen"""
    name = str(self._choices[self._choice*2])
    self.name = self._base_name + ": " + name

  def __call__(self):
    """Vaihtaa valittua kohtaa"""
    self._choice += 1
    if self._choice == len(self._choices)/2:
      self._choice = 0
    self._set_name()

  def get_value(self):
    """Palauttaa kohdan arvon"""
    return self._choices[self._choice*2+1]



class SubMenu:
  """Alivalikko"""
  def __init__(self, name):
    """
      name = string  valinnan nimi

    Luo alivalikon, jonka voi lisätä toiseen alivalikkoon
    """
    self.name   = name #Nimi joka näkyy valikossa
    self.title  = name #Nimi joka näkyy otsikkona kun ollaan tässä valikossa
    self.tree   = []  #Itse valikon sisältö
    self.choice = None #Mikä kohta on valittu

  def __call__(self):
    return self

  def add(self, item):
    """Lisää kohdan valikkoon"""
    if self.choice == None:
      self.choice = 0
    self.tree.append(item)

  def choice(self):
    """Palauttaa valitun kohdan"""
    return self.tree[choice]()



class MMenu:
  def __init__(self):
    """Alustaa luokan"""
    self._tree = []
    self._depth = [] #Nykyinen syvyys
    self._curmenu = None
    self._settings = {} #Valikon asetuksia varten

  def attach_to_key(self, key, item):
    """
      key  = string  asetuksen avain, jolla asetus tunnistetaan
      item = class   luokka, josta arvo avaimelle haetaan

    Lisää aetuksen asetuksien listaan
    """
    #Varmistetaan ettei korvata jo olemassa olevaa avainta
    if not self._settings.has_key(key):
      self._settings[key] = item.get_value
      return True
    else:
      return False

  def set_tree(self, tree):
    """Asettaa juuren valikolle"""
    self._tree = tree
    self._curmenu = self._tree

  def send_signal(self, signal):
    """
      signal = int  singaali

    Vastaanottaa signaalin ja toimii sen mukaan
    """
    global UP, DOWN, CHOICE, BACK

    if signal == UP:
      if self._curmenu.choice == 0:
        self._curmenu.choice = len(self._curmenu.tree)-1
      else:
        self._curmenu.choice -= 1

    if signal == DOWN:
      if self._curmenu.choice == len(self._curmenu.tree)-1:
        self._curmenu.choice = 0
      else:
        self._curmenu.choice += 1

    if signal == CHOICE:
      #Jos kyseessä on alivalikko niin mennää sinne
      a = self._curmenu.tree[self._curmenu.choice]()
      if a != None:
        self._curmenu = a

    if signal == BACK:
      #Jos ei olla jo juuressa
      if self._tree != self._curmenu:
        #Haetaan valikko jonka sisällä nykyinen valikko on ja laitetaan se
        #nykyiseksi valikoksi
        a = self._tree
        while a.tree[a.choice] != self._curmenu:
          a = a.tree[a.choice]
        self._curmenu = a

  def get_current_menu(self):
    """Palauttaa nykyisen valikon"""
    return self._curmenu

  def get_settings(self):
    """Palauttaa asetukset"""
    r = {}
    for key in self._settings:
      r[key] = self._settings[key]()
    return r



class VMenu:
  def __init__(self):
    self._font = pygame.font.Font(None, 50)
    self._header_font = pygame.font.Font(None, 300)

  def draw(self, menu):
    """Piirtää valikon"""
    screen = pygame.display.get_surface()

    #Piirretään otsikko
    surf = self._header_font.render(menu.title, True, [70,70,70])
    x = screen.get_width()/2-surf.get_width()/2
    screen.blit(surf, [x,10])

    #Ja valikon kohdat
    for m in range(len(menu.tree)):
      #Haetaan nimi
      name = menu.tree[m].name

      #Vaihdetaan väriä jos on valittu kyseinen kohta
      color = [32,32,32]
      if menu.choice == m:
        color = [128,128,128]

      #Piirretään teksti
      surf = self._font.render(name, True, color)

      #Lasketaan paikka
      x = screen.get_width()/2-surf.get_width()/2
      y = screen.get_height()/2-100 + 50*m

      #Ja piirretään se ruudulle
      screen.blit(surf, [x,y])



class CMenu:
  """"Nitoo" yhteen modelin ja viewin"""
  def __init__(self, tree, model=None, view=None):
    """
      model, view = instance   !HUOM! model ja view pitää olla instanseja

    Alustaa luokan
    """
    #Jotta viewi voidaan korvata omalla
    if view != None:
      self._view = view
    else:
      self._view = VMenu()

    #Jotta modelli voidaan korvata omalla
    if model != None:
      self._model = model
    else:
      self._model = MMenu()

    #Asetetaan valikon juuri
    self._model.set_tree(tree)

  def draw(self):
    """Piirtää valikon"""
    self._view.draw(self._model.get_current_menu())

  def send_signal(self, signal):
    """Lähettää signaalin model:n käsiteltäväksI"""
    self._model.send_signal(signal)

  def get_settings(self):
    """Palauttaa asetukset"""
    return self._model.get_settings()

  def attach_to_key(self, key, item):
    """Asettaa asetuksen asetuksien listaan"""
    self._model.attach_to_key(key, item)
# -*- coding: utf-8 -*-

import pygame
import menu

class Renderer:
  def __init__(self, bgcolor=[128,64,255]):
    pygame.init()
    self._bgcolor = bgcolor

  def set_display(self, resolution, fullscreen=False, flags=0):
    """
      resolution = list    asetettava resoluutio
      fullscreen = bool    onko kokoruudussa
      flags      = int     muita pygame:n flagejä

    Alustaa näytön lähimmälle sopivalle resoluutiolle
    """

    flags = flags | pygame.DOUBLEBUF
    if fullscreen:
      flags = flags | pygame.FULLSCREEN
    pygame.display.set_mode(resolution, flags)

  def start(self):
    """Aloittaa piirtämisen"""
    pygame.display.get_surface().fill(self._bgcolor)

  def end(self):
    """Lopettaa piirtämisen"""
    pygame.display.flip()



class StateMachine:
  def __init__(self):
    self._states = {"game": 1,
                    "menu": 0,
                    "quit": -1,
                    }
    self._state = self._states["menu"]

  def set_state(self, state):
    if state in self._states.keys():
      self._state = self._states[state]

  def get_state(self, name=""):
    if name == "":
      return self._state
    else:
      return self._states[name]



class DummyMenuItem:
  def __init__(self, name):
    self.name = name

  def __call__(self):
    print "%s pressed" % self.name



class StateMenuItem(DummyMenuItem):
  def __init__(self, name, state, statemachine):
    DummyMenuItem.__init__(self, name)
    self._state = state
    self._statemachine = statemachine

  def __call__(self):
    self._statemachine.set_state(self._state)



def create_backitems(m, tree):
  for item in tree:
    try:
      item.add(menu.BackMenuItem("<- Takaisin", m))
      create_backitems(m, item.tree)
    except:
      pass

def create_menu(statemachine):
  mainmenu = menu.SubMenu("")
  m = menu.CMenu(mainmenu)

  #------------ Alkuvalikon kohdat
  ng      = StateMenuItem("New game", "game", statemachine)
  options = menu.SubMenu("Options")
  quit    = StateMenuItem("Quit", "quit", statemachine)

  mainmenu.add(ng)
  mainmenu.add(options)
  mainmenu.add(quit)

  #------------ Asetusvalikon kohdat
  d4 = DummyMenuItem("Sound")
  video = menu.SubMenu("Video")
  d6 = DummyMenuItem("Game")

  options.add(d4)
  options.add(video)
  options.add(d6)

  #------------ Näytönasetuksien kohdat
  reso = pygame.display.list_modes()
  r = []
  for i in reso:
    #Suodatetaan liian pienet resoluutiot pois
    if i[0] > 600 and i[1] > 400:
      key = str(i[0]) + "x" + str(i[1])
      r.append(key)
      r.append(i)
  default = str(reso[0][0]) + "x" + str(reso[0][0])

  resolutions = menu.ChoiceMenuItem(m, "resolution", "Resolution",r)
  fs = menu.ChoiceMenuItem(m, "fullscreen", "Fullscreen", ["True", True,
                                                           "False", False])
  video.add(fs)
  video.add(resolutions)

  #Luodaan takaisin-kohdat
  create_backitems(m, mainmenu.tree)
  return m

def main():
  #Alustetaan renderöiä
  render = Renderer([64,64,64])
  render.set_display((800,600))

  #Alustetaan tilakone
  statemachine = StateMachine()

  #Luodaan valikko
  m = create_menu(statemachine)

  done = False
  while not done:
    #Käsitellään näppäimenpainallukset
    for e in pygame.event.get():
      if e.type == pygame.QUIT or \
                  ( e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE ):
        done = True

      if e.type == pygame.KEYDOWN:
        if e.key == pygame.K_UP:
          m.send_signal(menu.UP)
        elif e.key == pygame.K_DOWN:
          m.send_signal(menu.DOWN)
        elif e.key == pygame.K_RETURN:
          m.send_signal(menu.CHOICE)
        elif e.key == pygame.K_BACKSPACE:
          m.send_signal(menu.BACK)

    #Jos valikon kautta joko mentiin pois tai aloitettiin uusi peli, niin
    #tulostetaan asetukset
    if statemachine.get_state() in [statemachine.get_state("game"),
                                    statemachine.get_state("quit")]:
      done = True
      print m.get_settings()

    #Piirretään valikko
    render.start()
    m.draw()
    render.end()

if __name__ == "__main__": main()

Kommentit

tsuriga [10.01.2008 11:43:03]

#

Toiminnan perusteella tykkään. Tuossa voisi listauksissa ja ehkä jopa kuvauksessa mainita, että nuo menut sisältävä tiedosto on nimeltään menu.py, säästyypähän muut testailijat erheilmoitukselta :). Muutama typo, "renderöiä" ja "#Piirretänn". Tulee mieleen TextWidget , hiirellä käytettävä menuluokka.

ZcMander [10.01.2008 15:26:03]

#

"Piirretänn"-korjattu, mutta miten "renderöiä" pitäisi korjata? Yksvaihtoehto ois "renderi" tai ihan suomeksi "piirtäjä", mutta luulis selviän kaikista (kolmesta)

Mobel [10.01.2008 18:51:30]

#

Äidinkielellisesti oikein lienee sanoa "renderöijä", mutta tuskinpa sillä on tajuamisen kannalta suurta merkitystä.

Pekka Karjalainen [11.01.2008 18:36:49]

#

Kohdassa

global UP, DOWN, CHOICE

lienee jäänyt BACK pois, kun sitä kuitenkin testataan alla olevista if-lausekkeista viimeisessä.

ZcMander [17.01.2008 16:50:58]

#

Humm, ilmeisesti, kuitenkaan tulkki ei siitä mitään virhettä antanut, joten onko koko global-rivi turha? Noh, lisäänpä kyseisen BACK-ympäristömuuttujan tuonne.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta