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