从 db & 中检索数据在 Kivy 中为多屏应用设置文本输入字段和图像小部件!属性错误

2022-01-15 00:00:00 python python-3.x kivy sqlite

我通过拼凑一个小应用程序来了解不同小部件的行为来学习 kivy.

什么有效:

该应用接受文本和图像作为输入和存储到数据库中,存储的数据使用 RecycleView 正确显示在按钮上.

问题:

按下 RecycleView 上的按钮时,应用程序崩溃并出现以下错误:AttributeError: 'super' object has no attribute 'getattr'

我的尝试:

我从这个

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.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 - 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.

  1. At class ScreenOne(Screen):, var = ScreenTwo()
  2. At class SelectableButton(RecycleDataViewBehavior, Button):, var = ScreenTwo()
  3. In one.kv file, ScreenTwo:

Kv language » self.ids

When your kv file is parsed, kivy collects all the widgets tagged with id’s and places them in this self.ids dictionary type property.

Solution

In the example provided, I am using a SQLite3 database containing table, Users with columns, UserID and UserName. Please refer to the example for details.

Python Code

  1. Remove var = ScreenTwo() from class ScreenOne(Screen): and class SelectableButton(RecycleDataViewBehavior, Button): because you don't need to instantiate another objects of ScreenTwo() which are different from the one instantiated in kv file, one.kv.
  2. In populate_fields() method, replace self.ids.no.text with self.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.
  3. In filechoser() method, remove image_path = self.image_path and return image_path because self.image_path is a class attributes of class ScreenTwo().
  4. In save() method, replace self.ids.no.text and self.ids.name.text with self.user_no_text_input.text and self.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

  1. Remove all references of id: screen_manager and manager: screen_manager because each screen has by default a property manager that gives you the instance of the ScreenManager used.

kv file - two.kv

  1. In class rule, <SelectableButton>: replace root.var.populate_fields(self) with app.root.screen_two.populate_fields(self)

Screen default property manager

Each screen has by default a property manager that gives you the instance of the ScreenManager used.

Accessing Widgets defined inside Kv lang in your python code

Although the self.ids method is very concise, it is generally regarded as ‘best practice’ to use the ObjectProperty. This creates a direct reference, provides faster access and is more explicit.

Example

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, 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()

one.kv

#: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'

two.kv

#: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'

Output

相关文章