Finding the row a user selects in a KivyMD MDDataTable

I continually search for methods to finding the row a user selects in a KivyMD MDDataTable. There are not as many useful results in my Google searches to assist in finding a solution. Most of the results only show printing the incoming variable objects in the call back function. The on_press action for the table forwards these objects. Parsing the kivymd.uix.datatables.py file I see a private internal method for obtaining the Index of a row. This gives me a few ideas to obtain which row a user is selecting in a MDDataTable. So let’s fetch some data when you press a row in a MDDataTable in KivyMD

Setting up the Datatable

You can add a MDDataTable inside of your KV builder file if you import the MDDataTable from the kivymd.uix.datatables package. I am dynamically adding data to my table, so I choose to add the table inside my main python file. I add the table using the build() function for my MDApp class. In a later iteration, I likely will move table into the proper KV builder file. This will help me keep my View separate from my Controller file.

The build() method

I initialize a MDDataTable and set it to the class variable data_tables for easy reference in other functions in the class. This is where we set up column data headers, as well as pagination and number of rows in the table. I then populate the row_data with an internal class function I calI get_all_data(). Next we will bind the on_row_press action for the table to an internal class function i call row_selected(). Here is the portion of the records.py file showing the build() function.

class RecordsApp(MDApp):
    def build(self):
        screen = Builder.load_file('records.kv')
        self.data_tables = MDDataTable(
            column_data = [
                ("Id", dp(15)),
                ("Matter", dp(30)),
                ("File Name", dp(30)),
                ("Description", dp(30)),
                ("Location", dp(30))
            ],
            use_pagination = True,
	    rows_num = 5,
        )
        self.data_tables.row_data = self.get_all_data()   
        self.data_tables.bind(on_row_press=self.row_selected)   
        screen.ids.main_layout.add_widget(self.data_tables)
        return screen

Important note about populating the row_data

To help us find the row a user selects in the MDDataTable, we need a unique identifier for this row. As you see in my sample above, I am using an “ID” field as the first column in the table. The ID is a unique number that our database is generating automatically. For my example, I am using a SQLite3 database which python supports automatically and our data structures are very simple. Each time we update data in the database, we call an internal class function for the App to get_all_data() and reset the tables row_data which will cause the MDDataTable to automatically refresh in the GUI with the new data. Here is the CREATE statement for my SQLite3 database:

CREATE TABLE IF NOT EXISTS records (id INTEGER PRIMARY KEY, matter text, filename text, description text, location text)

Underlining MDDataTables view

The MDDataTable is setup to display a row of data with each column being its own button. The button text is the data entry for that field. For example, suppose our row of data has three fields “id”, “name” and “phone”. The table displays a row of data using three buttons laid out horizontally with the first button displaying the “id”. The second button displays the “name” and the third button displaying the “phone” data. This is how the designers of KivyMD decided to implement the datatable view. Clicking on any row in the table provides the index of the button, rather the index of the row.

Take for example again the same data fields above and lets imagine three rows of data:

100, "Bob", "555-1234"
200, "Sue", "555-2345"
245, "Jane", "555-4567"

Clicking on the word “Sue”, the on_row_press action will send the index of ‘4’ to your call_back function. The CellRow data includes the button index, and a field holding the “range” of indexes that belong to the row. So for in the case of clicking “Sue” there is a “range” field value of [ 3, 5 ]. This is very helpful! The first item in the range will be the first column of data for our row. In our case this will be the “id” field. This is why I choose to use the “id” as our first data column. This will be the unique data value we can use to retrieve data from our database for use elsewhere.

How to fetch data when you press a row in a MDDataTable in KivyMD

Now let’s get to fetching the information we want to help us determine the row selected by the user. The datatable sends two parameters to our call back function, a MDDataTable object and a CellRow object. This is defined in the kivy.uix.datatables module. You can use this first object to manipulate the visual table. We want to use the second value which is the CellRow. The properties in a CellRow object include an Index value for the button, as well as a table object. This table object holds the underlining Kivy Recycle Data object. The recycle data object holds various data properties for the underlining table. This includes not only information about the field in the row, but a “range” of indexes pertaining to the row. See the example above about clicking “Sue” in the table. Here is a sample call back function that determines the row index:

def row_selected(self, table_view, cell_row):
        # get start index from selected row item range
        start_index, end_index = cell_row.table.recycle_data[cell_row.index]["range"]

        # populate form with selected record
        self.populate_form(cell_row.table.recycle_data[start_index]["text"])

The first thing we want to do is use the “range” value (which holds data in similar format to [3,5]) and expand that out to two variables. We can then use the starting index value to grab the first column data for that row of data. I then pass the “text” value of the underlining table referenced by start_index (which should be the numeric Id field value in the row) into another internal class function called populate_form() that will poll the database and retrieve the data by the id number and then fill in the form on the screen with that data that was selected.

Review

The on_row_press action for the MDDataTable will forward enough information for us in finding which row the user selects. The backend of the datatable is really the Kivy RecycleView UIX. Using the start_index in the range we can easily poll the database for the information we need. This works by using a unique ID in our database to use in our table as the first data field.

There may be other ways to determine which row a user is selecting. I look forward to hearing ways you determine the row a user is selecting. The KivyMD MDDataTable code is still in flux. There may be new and better ways to get the data we need in the future versions of KivyMD.

My KivyMD example files for this project

records.py

from kivymd.app import MDApp
from kivymd.uix.screen import Screen
from kivymd.uix.datatables import MDDataTable
from kivy.lang import Builder
from kivy.metrics import dp

from recordsdb import Database
db = Database('records.db')

class RecordsApp(MDApp):
    def build(self):
        screen = Builder.load_file('records.kv')
        self.data_tables = MDDataTable(
            column_data = [
                ("Id", dp(15)),
                ("Matter", dp(30)),
                ("File Name", dp(30)),
                ("Description", dp(30)),
                ("Location", dp(30))
            ],
            use_pagination = True,
			rows_num = 5,
        )
        self.data_tables.row_data = self.get_all_data()   
        self.data_tables.bind(on_row_press=self.row_selected)   
        screen.ids.main_layout.add_widget(self.data_tables)
        return screen
    
    def get_all_data(self):
        data = []
        for row in db.fetch_all():
            data.append(row)
        return data

    def row_selected(self, table, row):
        # get start index from selected row item range
        start_index, end_index = row.table.recycle_data[row.index]["range"]

        # populate form with selected record
        self.populate_form(row.table.recycle_data[start_index]["text"])

    def clear_form(self):
        self.root.ids.record_id.text = ''
        self.root.ids.matter_name.text = ''
        self.root.ids.file_name.text = ''
        self.root.ids.description.text = ''
        self.root.ids.location.text = ''

    def populate_form(self, row_id):
        # retrieve row data from db using record_id
        # returned dataset tuple: (record_id, matter_name, file_name, description, location)
        row_data = db.get_record_by_id(row_id)

        self.root.ids.record_id.text = str(row_data[0])
        self.root.ids.matter_name.text = row_data[1]
        self.root.ids.file_name.text = row_data[2]
        self.root.ids.description.text = row_data[3]
        self.root.ids.location.text = row_data[4]

    def add_record(self):
        self.root.ids.record_id.text = ''
        if self.root.ids.matter_name.text == '' or self.root.ids.file_name.text == '' or self.root.ids.description.text == '' or self.root.ids.location.text == '':
            self.root.ids.record_id.text = 'Please enter ALL fields'
        else:
            matter_name = self.root.ids.matter_name.text
            file_name = self.root.ids.file_name.text
            description = self.root.ids.description.text
            location = self.root.ids.location.text
            db.insert(matter_name, file_name, description, location)
            self.data_tables.row_data = self.get_all_data()
            self.clear_form()

    def update_record(self):
        if self.root.ids.record_id.text == '':
            pass
        else:
            id = self.root.ids.record_id.text
            matter_name = self.root.ids.matter_name.text
            file_name = self.root.ids.file_name.text
            description = self.root.ids.description.text
            location = self.root.ids.location.text
            db.update(id, matter_name, file_name, description, location)
            self.data_tables.row_data = self.get_all_data()

    def delete_record(self):
        if self.root.ids.record_id.text == '':
            pass
        else:
            id = self.root.ids.record_id.text
            db.delete(id)
            self.data_tables.row_data = self.get_all_data()
            self.clear_form()
        
if __name__ == '__main__':
    RecordsApp().run()

records.kv

Screen:
    MDBoxLayout:
        id: main_layout
        orientation: 'vertical'
        padding: dp(20)

        MDLabel:
            text: "Records Manager"
            font_size: dp(20)
            halign: 'center'
            padding_y: dp(20)
            size_hint_y: None
            height: self.texture_size[1]
        
        MDGridLayout:
            adaptive_height: True
            cols: 2
            MDBoxLayout:
                MDLabel:
                    text: "Record Id"
                MDLabel:
                    id: record_id 
                    text: ''
            Widget:

            MDTextField:
                id: matter_name
                hint_text: "Matter Name"

            MDTextField:
                id: file_name
                hint_text: "File name"

            MDTextField:
                id: description
                hint_text: "Description"

            MDTextField:
                id: location
                hint_text: "Location"

        MDBoxLayout:
            adaptive_height: True
            spacing: dp(50)
            padding: dp(20)

            MDRectangleFlatButton:
                text: "Clear Form"
                on_press: app.clear_form()

            MDRectangleFlatButton:
                text: "Add Record"
                on_press: app.add_record()

            MDRectangleFlatButton:
                text: "Update Record"
                on_press: app.update_record()

            MDRectangleFlatButton:
                text: "Delete Record"
                on_press: app.delete_record()

recordsdb.py

import sqlite3

class Database:
    def __init__(self, db):
        self.conn = sqlite3.connect(db)
        self.cur = self.conn.cursor()
        self.cur.execute("CREATE TABLE IF NOT EXISTS records (id INTEGER PRIMARY KEY, matter text, filename text, description text, location text)")
        self.conn.commit()

    def fetch_all(self):
        self.cur.execute("SELECT * FROM records")
        rows = self.cur.fetchall()
        return rows

    def get_record_by_id(self, id):
        self.cur.execute("SELECT * FROM records WHERE id=?", (id,))
        row = self.cur.fetchone()
        return row

    def insert(self, matter, file_name, description, location):
        self.cur.execute("INSERT INTO records VALUES (NULL, ?, ?, ?, ?)", (matter, file_name, description, location))
        self.conn.commit()

    def delete(self, id):
        self.cur.execute("DELETE FROM records where id=?", (id,))
        self.conn.commit()

    def update(self, id, matter, file_name, descripton, location):
        self.cur.execute("UPDATE records SET matter=?, filename=?, description=?, location=? WHERE id=?", (matter, file_name, descripton, location, id))
        self.conn.commit()

    def __del__(self):
        self.conn.close()

Add a Comment

Your email address will not be published. Required fields are marked *