I'm learning kivy by cobbling together a small application to understand the behavior of different widgets.
What works:
The app accepts text and images as input & stores to the database, stored data is correctly displayed on buttons using RecycleView.
Problem:
On pressing the buttons on the RecycleView the app crashes with the error: AttributeError: 'super' object has no attribute 'getattr'
What I've tried:
I understand from this post, that initialization may not be complete and tried scheduling with kivy clock but this throws a new error AttributeError:'float' object has no attribute 'index'.
Expected behavior:
On button click set selected button data (text & image values) in their respective widgets. I have not been able to understand why this is not working in a multiscreen environment.
The full code is as follows.
main.py
import sqlite3
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.button import Button
from kivy.properties import BooleanProperty, ListProperty, StringProperty, ObjectProperty
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.accordion import Accordion
from kivy.clock import Clock
from tkinter.filedialog import askopenfilename
from tkinter import Tk
class Manager(ScreenManager):
screen_one = ObjectProperty(None)
screen_two = ObjectProperty(None)
class ScreenTwo(BoxLayout, Screen, Accordion):
data_items = ListProperty([])
def __init__(self, **kwargs):
super(ScreenTwo, self).__init__(**kwargs)
# Clock.schedule_once(self.populate_fields)
self.create_table()
self.get_table_column_headings()
self.get_users()
def populate_fields(self, instance): # NEW
columns = self.data_items[instance.index]['range']
self.ids.no.text = self.data_items[columns[0]]['text']
self.user_name_text_input.text = self.data_items[columns[1]]['text']
def get_table_column_headings(self):
connection = sqlite3.connect("demo.db")
with connection:
cursor = connection.cursor()
cursor.execute("PRAGMA table_info(Users)")
col_headings = cursor.fetchall()
self.total_col_headings = len(col_headings)
def filechooser(self):
Tk().withdraw()
self.image_path = askopenfilename(initialdir = "/",title = "Select file",filetypes = (("jpeg files","*.jpg"),("all files","*.*")))
self.image.source = self.image_path
image_path = self.image_path
return image_path
def create_table(self):
connection = sqlite3.connect("demo.db")
cursor = connection.cursor()
sql = """CREATE TABLE IF NOT EXISTS Employees(
EmpID integer PRIMARY KEY,
EmpName text NOT NULL,
EmpPhoto blob NOT NULL)"""
cursor.execute(sql)
connection.close()
def get_users(self):
connection = sqlite3.connect("demo.db")
cursor = connection.cursor()
cursor.execute("SELECT * FROM Employees ORDER BY EmpID ASC")
rows = cursor.fetchall()
# create list with db column, db primary key, and db column range
data = []
low = 0
high = self.total_col_headings - 1
# Using database column range for populating the TextInput widgets with values from the row clicked/pressed.
self.data_items = []
for row in rows:
for col in row:
data.append([col, row[0], [low, high]])
low += self.total_col_headings
high += self.total_col_headings
# create data_items
self.data_items = [{'text': str(x[0]), 'Index': str(x[1]), 'range': x[2]} for x in data]
def save(self):
connection = sqlite3.connect("demo.db")
cursor = connection.cursor()
EmpID = self.ids.no.text
EmpName = self.ids.name.text
image_path = self.image_path # -- > return value from fielchooser
EmpPhoto = open(image_path, "rb").read()
try:
save_sql="INSERT INTO Employees (EmpID, EmpName, EmpPhoto) VALUES (?,?,?)"
connection.execute(save_sql,(EmpID, EmpName, EmpPhoto))
connection.commit()
connection.close()
except sqlite3.IntegrityError as e:
print("Error: ",e)
self.get_users() #NEW
class ScreenOne(Screen):
var = ScreenTwo()
class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleGridLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableButton(RecycleDataViewBehavior, Button):
''' Add selection support to the Button '''
var = ScreenTwo()
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableButton, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableButton, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
class OneApp(App):
def build(self):
return Manager()
if __name__ =="__main__":
OneApp().run()
one.kv
#:kivy 1.10.0
#:include two.kv
<Manager>:
id: screen_manager
screen_one: screen_one_id # original name: our set name
screen_two: screen_two_id
ScreenOne:
id: screen_one_id # our set name
name: 'screen1'
manager: screen_manager # telling each screen who its manager is.
ScreenTwo:
id: screen_two_id # our set name
name: 'screen2'
manager: screen_manager
<ScreenOne>:
Button:
text: "On Screen 1 >> Go to Screen 2"
on_press: root.manager.current = 'screen2'
two.kv
#:kivy 1.10.0
<SelectableButton>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
Rectangle:
pos: self.pos
size: self.size
on_press:
root.var.populate_fields(self)
<ScreenTwo>:
user_no_text_input: no
user_name_text_input: name
image: image
AccordionItem:
title: "INPUT FIELDS"
GridLayout:
rows:3
BoxLayout:
size_hint: .5, None
height: 600
pos_hint: {'center_x': 1}
padding: 10
spacing: 3
orientation: "vertical"
Label:
text: "Employee ID"
size_hint: (.5, None)
height: 30
TextInput:
id: no
size_hint: (.5, None)
height: 30
multiline: False
Label:
text: "Employee NAME"
size_hint: (.5, None)
height: 30
TextInput:
id: name
size_hint: (.5, None)
height: 30
multiline: False
Label:
text: "Employee PHOTO"
size_hint: (.5, None)
height: 30
Image:
id: image
allow_stretch: True
keep_ratio: True
Button:
text: "SELECT IMAGE"
size_hint_y: None
height: self.parent.height * 0.2
on_release: root.filechooser()
Button:
id: save_btn
text: "SAVE BUTTON"
height: 50
on_press: root.save()
AccordionItem:
title: "RECYCLE VIEW"
BoxLayout:
orientation: "vertical"
GridLayout:
size_hint: 1, None
size_hint_y: None
height: 25
cols: 2
Label:
text: "Employee ID"
Label:
text: "Employee Name"
# Display only the first two columns Employee ID and Employee Name NOT EmployeePhoto on the RecycleView
BoxLayout:
RecycleView:
viewclass: 'SelectableButton'
data: root.data_items
SelectableRecycleGridLayout:
cols: 2
default_size: None, dp(26)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
Button:
text: "On Screen 2 >> Go to Screen 1"
on_press: root.manager.current = 'screen1'
Apologies for the really long post, I thank you for your time and attention.
Problem - AttributeError
self.ids - Empty
The problem was
self.ids
was empty.Root Cause
There were three instances of class ScreenTwo(). If you apply
id()
function, it will show three different memory addresses/locations.self.ids
is only available when the kv file is parsed. Therefore,self.ids
is only available in the instance instantiated in one.kv file.class ScreenOne(Screen):
,var = ScreenTwo()
class SelectableButton(RecycleDataViewBehavior, Button):
,var = ScreenTwo()
ScreenTwo:
Kv language » self.ids
Solution
In the example provided, I am using a SQLite3 database containing table,
Users
with columns,UserID
andUserName
. Please refer to the example for details.Python Code
var = ScreenTwo()
fromclass ScreenOne(Screen):
andclass SelectableButton(RecycleDataViewBehavior, Button):
because you don't need to instantiate another objects ofScreenTwo()
which are different from the one instantiated in kv file,one.kv
.populate_fields()
method, replaceself.ids.no.text
withself.user_no_text_input.text
because in kv file (two.kv
), there is already an ObjectProperty,user_no_text_input
defined and hooked to TextInput's id,no
i.e.user_no_text_input: no
.filechoser()
method, removeimage_path = self.image_path
andreturn image_path
because self.image_path is a class attributes of classScreenTwo()
.save()
method, replaceself.ids.no.text
andself.ids.name.text
withself.user_no_text_input.text
andself.user_name_text_input.text
respectively because they are defined and hooked-up to the TextInputs in kv file,two.kv
*plus it is generally regarded as ‘best practice’ to use the ObjectProperty. This creates a direct reference, provides faster access and is more explicit.*kv file - one.kv
id: screen_manager
andmanager: screen_manager
because each screen has by default a propertymanager
that gives you the instance of the ScreenManager used.kv file - two.kv
<SelectableButton>:
replaceroot.var.populate_fields(self)
withapp.root.screen_two.populate_fields(self)
Screen default property manager
Accessing Widgets defined inside Kv lang in your python code
Example
main.py
one.kv
two.kv
Output