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()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.
"Piirretänn"-korjattu, mutta miten "renderöiä" pitäisi korjata? Yksvaihtoehto ois "renderi" tai ihan suomeksi "piirtäjä", mutta luulis selviän kaikista (kolmesta)
Äidinkielellisesti oikein lienee sanoa "renderöijä", mutta tuskinpa sillä on tajuamisen kannalta suurta merkitystä.
Kohdassa
global UP, DOWN, CHOICE
lienee jäänyt BACK pois, kun sitä kuitenkin testataan alla olevista if-lausekkeista viimeisessä.
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.