Я изучаю киви, объединяя небольшое приложение, чтобы понять поведение разных виджетов.
Что работает:
Приложение принимает текст и изображения в качестве входных и хранилищ в базу данных, сохраненные данные корректно отображаются на кнопках с помощью RecycleView.
Проблема:
При нажатии кнопок на RecycleView приложение выходит из строя с ошибкой: AttributeError: объект 'super' не имеет атрибута ' getattr '
Что я пробовал:
Я понимаю из этого сообщения, что инициализация может быть неполной и попробоваться при расписании с часами kivy, но это порождает новую ошибку AttributeError: объект "float" не имеет атрибута "index".
Ожидаемое поведение:
На кнопке щелкните набор данных выбранной кнопки (значения текста и изображения) в соответствующих виджетах. Я не мог понять, почему это не работает в многоэкранной среде.
Полный код выглядит следующим образом.
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'
Извиняюсь за действительно длинный пост, я благодарю вас за ваше время и внимание.
self.ids.no.text = self.data_items[columns[0]]['text'] File "kivy/properties.pyx", line 841, in kivy.properties.ObservableDict.__getattr__ AttributeError: 'super' object has no attribute '__getattr__'
Проблема была в том, что self.ids
был пуст.
Было три экземпляра класса ScreenTwo(). Если вы примените функцию id()
, она отобразит три разных адреса/расположения памяти. self.ids
доступен только при self.ids
файла kv. Поэтому self.ids
доступен только в экземпляре, созданном в файле one.kv.
class ScreenOne(Screen):
var = ScreenTwo()
class SelectableButton(RecycleDataViewBehavior, Button):
var = ScreenTwo()
ScreenTwo:
Когда ваш файл kv разбирается, kivy собирает все виджеты с тегами с идентификаторами и помещает их в это свойство типа слова self.ids.
В приведенном примере я использую базу данных SQLite3, содержащую таблицу, Users
с столбцами, UserID
и UserName
. Подробные сведения см. В этом примере.
var = ScreenTwo()
из class ScreenOne(Screen):
и class SelectableButton(RecycleDataViewBehavior, Button):
потому что вам не нужно создавать экземпляры других объектов ScreenTwo()
которые отличаются от экземпляра, one.kv
в файле kv, one.kv
,self.ids.no.text
populate_fields()
замените self.ids.no.text
на self.user_no_text_input.text
потому что в файле kv (two.kv
) уже существует ObjectProperty, user_no_text_input
определенный и подключенный к идентификатору TextInput, no
ie user_no_text_input: no
.filechoser()
удалите image_path = self.image_path
и return image_path
потому что self.image_path - это атрибуты класса класса ScreenTwo()
.save()
замените self.ids.no.text
и self.ids.name.text
с self.user_no_text_input.text
и self.user_name_text_input.text
соответственно, потому что они определены и подключены к TextInputs в файле kv, two.kv
* plus обычно рассматривается как "наилучшая практика использования ObjectProperty. Это создает прямую ссылку, обеспечивает более быстрый доступ и более явственно. *id: screen_manager
и manager: screen_manager
потому что каждый экран по умолчанию имеет manager
свойств, который дает вам экземпляр используемого ScreenManager.<SelectableButton>:
замените root.var.populate_fields(self)
на app.root.screen_two.populate_fields(self)
Диспетчер свойств по умолчанию
Каждый экран по умолчанию имеет менеджер свойств, который дает вам экземпляр используемого ScreenManager.
Доступ к виджетам, определенным внутри Kv lang, в вашем коде на Python
Хотя метод
self.ids
очень краток, он обычно считается "лучшей практикой использования ObjectProperty. Это создает прямую ссылку, обеспечивает более быстрый доступ и более явственно.
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, 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 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)
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.user_no_text_input.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
def create_table(self):
connection = sqlite3.connect("demo.db")
cursor = connection.cursor()
sql = """CREATE TABLE IF NOT EXISTS Users(
UserID integer PRIMARY KEY,
UserName text NOT NULL)"""
cursor.execute(sql)
connection.close()
def get_users(self):
connection = sqlite3.connect("demo.db")
cursor = connection.cursor()
cursor.execute("SELECT * FROM Users ORDER BY UserID 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()
UserID = self.user_no_text_input.text
UserName = self.user_name_text_input.text
EmpPhoto = open(self.image_path, "rb").read()
try:
save_sql = "INSERT INTO Users (UserID, UserName) VALUES (?,?)"
connection.execute(save_sql, (UserID, UserName))
connection.commit()
connection.close()
except sqlite3.IntegrityError as e:
print("Error: ", e)
self.get_users() #NEW
class ScreenOne(Screen):
pass
class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleGridLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableButton(RecycleDataViewBehavior, Button):
''' Add selection support to the Button '''
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()
#:kivy 1.11.0
#:include two.kv
<Manager>:
screen_one: screen_one_id
screen_two: screen_two_id
ScreenOne:
id: screen_one_id
name: 'screen1'
ScreenTwo:
id: screen_two_id
name: 'screen2'
<ScreenOne>:
Button:
text: "On Screen 1 >> Go to Screen 2"
on_press: root.manager.current = 'screen2'
#:kivy 1.11.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:
app.root.screen_two.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'
fetchone()
для EmpPhoto для demo.db и конвертируйте BLOB с помощью Kivy CoreImage .