11

UPDATE:

Ryan P's answer provided the solution. However, I took that solution and changed it up a bit, throwing all of the data that wasn't being properly initialised into the on_enter method of the RootWidget Screen. This has worked nicely.

My RootWidget class was subclassing Widget until today, and I had no problem accessing it's ids to get the value of "grid". However, I just changed it to subclass Screen, and now it says the ids is empty for some reason... Screen does have an ids and all that, but for some reason, it isn't registering that I assigned a GridLayout to the id ''grid'' in the kv file. Can anyone tell me why?

The traceback:

[INFO   ] [Logger      ] Record log in /home/yerman/.kivy/logs/kivy_14-11-13_201.txt
[INFO   ] Kivy v1.9.0-dev
[INFO   ] [Python      ] v2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2]
[INFO   ] [Factory     ] 172 symbols loaded
[INFO   ] [Image       ] Providers: img_tex, img_dds, img_pygame, img_pil, img_gif (img_sdl2, img_ffpyplayer ignored)
[INFO   ] [Window      ] Provider: pygame(['window_egl_rpi'] ignored)
[WARNING] [WinPygame   ] Video: failed (multisamples=2)
[WARNING] [WinPygame   ] trying without antialiasing
[INFO   ] [GL          ] OpenGL version <2.1 Mesa 10.1.3>
[INFO   ] [GL          ] OpenGL vendor <Intel Open Source Technology Center>
[INFO   ] [GL          ] OpenGL renderer <Mesa DRI Intel(R) Ironlake Mobile >
[INFO   ] [GL          ] OpenGL parsed version: 2, 1
[INFO   ] [GL          ] Shading version <1.20>
[INFO   ] [GL          ] Texture max size <8192>
[INFO   ] [GL          ] Texture max units <16>
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
[INFO   ] [Text        ] Provider: pygame(['text_sdl2'] ignored)
{}                     #<<< note the emtpy ids I printed out
 Traceback (most recent call last):
   File "main.py", line 169, in <module>
     MineSweeperApp().run()
   File "/usr/lib/python2.7/dist-packages/kivy/app.py", line 799, in run
     root = self.build()
   File "main.py", line 163, in build
     return Manager()
   File "/usr/lib/python2.7/dist-packages/kivy/uix/screenmanager.py", line 844, in __init__
     super(ScreenManager, self).__init__(**kwargs)
   File "/usr/lib/python2.7/dist-packages/kivy/uix/floatlayout.py", line 66, in __init__
     super(FloatLayout, self).__init__(**kwargs)
   File "/usr/lib/python2.7/dist-packages/kivy/uix/layout.py", line 66, in __init__
     super(Layout, self).__init__(**kwargs)
   File "/usr/lib/python2.7/dist-packages/kivy/uix/widget.py", line 269, in __init__
     Builder.apply(self)
   File "/usr/lib/python2.7/dist-packages/kivy/lang.py", line 1837, in apply
     self._apply_rule(widget, rule, rule)
   File "/usr/lib/python2.7/dist-packages/kivy/lang.py", line 1942, in _apply_rule
     child = cls(__no_builder=True)
   File "main.py", line 43, in __init__
     self.grid = self.ids["grid"]
 KeyError: 'grid'

kv file:

#:kivy 1.8.0

<RootWidget>:
    GridLayout:
        id: grid
        size: root.size
        cols: root.sides

<Blank>:
    background_color: 1, 1, 1, 1
    background_disabled_down: "kivy_white_bg.png"
    on_press: self.parent.parent.sweep(self)

<Mine>:
    background_color: 1, 1, 1, 1
    background_disabled_down: "kivy_white_bg.png"
    on_press: self.parent.parent.sweep(self)

<TryAgain>:
    anchor_x: 'center'
    anchor_y: 'center'
    BoxLayout:
        size: root.size
        orientation: 'vertical'
        padding_bottom: '20dp'

        Label:
            font_size: '20dp'
            text: root.text

        BoxLayout:
            size_hint: 1, .3
            spacing: 10
            padding: 10
            Button:
                size_hint: .4, 1
                font_size: '20dp'
                text: "yes"
                on_press: app.stop(); app.run()
            Button:
                size_hint: .4, 1
                font_size: '20dp'
                text: "no"
                on_press: root.quit()

<Menu>:
    GridLayout:
        rows: 2
        Button:
            text: "8x8"
            on_press: root.manager.current = 'game_screen'
        Button:
            text: "16x16"
            on_press: root.manager.current = 'game_screen'
        Button:
            text: "30x16"
            on_press: root.manager.current = 'game_screen'
        Button:
            text: "custom"
            on_press: root.manager.current = 'game_screen'

<Manager>:
    id: _manager
    menu: menu
    game: game
    current: menu_screen

    Menu:
        id: menu
        manager: _manager
        name: 'menu_screen'

    RootWidget:
        id: game
        manager: _manager
        name: 'game_screen'

main.py:

#!/usr/bin/env python

from random import sample
import sys
import kivy
kivy.require('1.8.0')
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.properties import NumericProperty, ListProperty, StringProperty, ObjectProperty
from kivy.uix.gridlayout import GridLayout
from kivy.uix.modalview import ModalView
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock

class Blank(Button):

    index = ListProperty([0, 0])
    count = NumericProperty(0)

    def __init__(self, **kwargs):
        super(Blank, self).__init__(**kwargs)


class Mine(Button):

    index = ListProperty([0, 0])
    count = NumericProperty(0) # not really necessary

    def __init__(self, **kwargs):
        super(Mine, self).__init__(**kwargs)


class RootWidget(Screen):

    sides = NumericProperty(10)
    mine_count = NumericProperty(20)

    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        self.grid = self.ids["grid"]

        # generate random mine indices
        mines = sample(xrange(self.sides**2), self.mine_count)

        x, y = -1, 0
        for i in xrange(self.sides**2):
            if x == self.sides - 1:
                x = 0
                y += 1
            else:
                x += 1

            if i not in mines:
                b = Blank(index=[x, y])

            else:
                b = Mine(index=[x, y])
            self.grid.add_widget(b)

        # record mine, blank and safe blank indices
        self.all_btns = [c.index for c in self.grid.children]
        self.mines = [c.index for c in self.grid.children if isinstance(c, Mine)]
        self.blanks = [c.index for c in self.grid.children if isinstance(c, Blank)]
        # a safe blank has no adjacent mines
        self.safe_blanks = [c.index for c in self.grid.children if self.is_safe(c)]

        # give each btn an 'adjacent mines count'
        for x, y in self.all_btns:
            btn = self.get_child_by_index([x, y])
            for index in self.field(x, y):
                if index in self.mines:
                    btn.count += 1

    def field(self, x, y):
        """ the minefield surrounding a btn """
        field = [[x-1, y], [x+1, y], [x, y+1], [x, y-1],
            [x+1, y+1], [x-1, y-1], [x+1, y-1], [x-1, y+1]]

        get = self.get_child_by_index        
        return [i for i in field if i in self.all_btns and get(i).disabled == False]

    def sweep(self, instance):
        instance.disabled = True

        if instance.index in self.mines: 
            print "Boom!"              # It's a mine! You lose
            instance.text = "Boom!"
            self.game_over() 

        pressed = sum(1 for c in self.grid.children if c.disabled == True)
        print pressed
        if self.sides**2 - pressed == self.mine_count:
            self.game_over(win=True)

        if instance.count > 0:
            instance.text = str(instance.count)
            instance.disabled = True
            return
        else:
            x, y = instance.index

            for index in self.field(x, y):
                if index not in self.mines:
                    blank = self.get_child_by_index(index)
                    blank.disable = True
                    if blank.count > 0:
                        blank.text = str(blank.count)
                    self.sweep(blank)

    def is_safe(self, btn):
        x, y = btn.index
        for index in self.field(x, y):
            if index in self.mines:
                return False
        return True

    def get_child_by_index(self, index):
        for child in self.grid.children:
            if child.index == index:
                return child

    def game_over(self, q=False, win=False):
        if q == True:
            sys.exit()

        if win == True:
            result = "Win"
        elif win == False:
            result = "lost"

        view = TryAgain(
            size_hint = (None, None),
            width = self.width/2,
            height = self.height/2,
            center = self.center,
            text = "You {}! Try Again?".format(result))
        view.open()

class TryAgain(ModalView):

    text = StringProperty('')

    def quit(self):
        sys.exit()

class Menu(Screen):
    pass


class Manager(ScreenManager):

    menu = ObjectProperty(None)
    game = ObjectProperty(None)


class MineSweeperApp(App):

    def build(self):
        return Manager()



if __name__ == "__main__":

    MineSweeperApp().run()
Totem
  • 7,189
  • 5
  • 39
  • 66
  • From the log it looks like you're using v1.9.0_dev. There's no problem with adding an id to a screen in v1.8.0. Have you tried the same code with that version? – tiktok Nov 13 '14 at 19:30
  • I haven't. I'm not sure how to downgrade though – Totem Nov 13 '14 at 19:40
  • Can you post more of the code so that it's executable so and I'll try it on my machine? – tiktok Nov 13 '14 at 19:41
  • I've included the full program, watch out for the one texture used though in the kv file.. a lot of the code is still part experiment, part just plain not finished – Totem Nov 13 '14 at 20:22
  • Also because I started getting this error, I never got to test the whole screen implementation, so beware. – Totem Nov 13 '14 at 20:25
  • It's worth noting that the exception is not that there is no `ids` attribute, that would be an `AttributeError`. Instead, you're getting a `KeyError`, because the `ids` dictionary does not contain a `grid` key. I don't know enough about kivy to tell you what that means. Perhaps `Screen` isn't compatible with `GridLayout` (or requires different syntax to set one up)? – Blckknght Nov 13 '14 at 20:38
  • Thanks, I'm aware that a KeyError is the issue. I just don't see why, as Screen.ids does exist and I do set an id in the kv file which should appear in the ids. I guess I'm going to have to just find a way around it for the moment. – Totem Nov 13 '14 at 20:41

2 Answers2

14

kv rules are not applied until the original Widget has finished instantiating. In this case, your Manager widget is the initial widget - it, in turn, creates the other widgets including RootWidget. This means that in your RootWidget.__init__ the ids are not yet populated! They will be as soon as Manager finishes instantiating - so the best approach is to just delay the rest of your initialization, like so:

class RootWidget(Screen):
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        Clock.schedule_once(self._finish_init)

    def _finish_init(self, dt):
        self.grid = self.ids.grid
        # etc
kitti
  • 14,663
  • 31
  • 49
  • 1
    It worked! And I feel I should have seen it, the order in which things were happening. Thanks! I have to say though, this issue irks me, I don't like that it has to be done in what feels like a patched up sort of way. – Totem Nov 13 '14 at 21:49
  • 2
    That seems like an improvement we could definitely make. Feel free to submit a feature request on https://github.com/kivy/kivy/issues or find a solution and make a pull request :) – kitti Nov 13 '14 at 21:56
1

Since Kivy 1.11.0, you can use the on_kv_post event:

Fired after all the kv rules associated with the widget and all other widgets that are in any of those rules have had all their kv rules applied. base_widget is the base-most widget whose instantiation triggered the kv rules (i.e. the widget instantiated from Python, e.g. MyWidget()).

class RootWidget(Screen):
    def on_kv_post(self, base_widget):
        # self.ids will be populated here
Scott Rowley
  • 486
  • 1
  • 7
  • 30
Juraj Fiala
  • 170
  • 1
  • 12