Problem with wxPython Grid Navigation

Hi everyone. I’m new to Python and wxPython. I’ve got a form I use to calculate the Sq In of a leather project.

I’m using python 3.9.13 and wxPython 4.20
This is being developed on a Windows 10 but will be used on both Mac and Windows machines once the project is complete.

I’m having the following issues:

  1. When I come into the form, no grid cell has the focus set - I start typing and nothing happens. I have to click the cell.
    If I hit Tab or Enter, the OnKeyDown fires, but does not move to the appropriate cell - it does nothing but run the update and move off of the current cell.
    The action I’m trying to make is this
    ENTER KEY: Always go down 1 row and to col 0
    TAB, if Col 0 Move to Col 1 on same row, if Col 1 go to Row +1, Col 0
    I also need to have what3ever cell it is supposed to land on to get the focus so I can just type.
    Currently I have to click in each cell I want/need to add.
    There could be up to 20 pieces of leather with differing sizes, so in order to accurately calculate the Sq In, I need to get all the measurements in.
    The form, one of several tabs, comes up and does everything else I’ve coded for great. Just no navigation.
    Can anyone assist? Here is the module with the form
***********************************************************************

'''

Module Name : alwsqin.py

Author      : Chris Anderson

Create Date : 03/10/2023

Description : This module contains the Sq In/MM/CM of leather used

This file is Copyright Anderson Leather Works (c) 2023

 

'''

 

########################################################################

#

# File Last Update and Person

#

#***********************************************************************

 

# Imports

import wx

#from Imports

from alwlogic import leather_sqin

 

class LeatherSqInPanel(wx.Panel):

    '''

    Name        : LeatherSqInPanel

    Author      : Chris Anderson

    Create Date : 02/23/2023

    Description : Panel for the 'Leather Sq In Calculator

                  in Leatherworking Cost Estimator app

    '''    

   

    dbTableName = 'None'

 

    def __init__(self, parent):

        wx.Panel.__init__(self, parent)

 

        self.ls = leather_sqin

 

        self.grid = wx.grid.Grid(self, size=(600, 515))

        self.grid.CreateGrid(30, 6)

       

        # Set column labels

        self.grid.SetColLabelValue(0, "Length")

        self.grid.SetColLabelValue(1, "Width")

        self.grid.SetColLabelValue(2, "Total")

        self.grid.SetColLabelValue(3, "Grand Total")

        self.grid.SetColLabelValue(4, "Type")

        self.grid.SetColLabelValue(5, "Select Calc Method")

 

        for col in range(self.grid.GetNumberCols()):

            self.grid.AutoSizeColumn(col)

 

        self.grid.EnableEditing(True)

 

        # Set dropdown choices for column 5, row 0

        types = ["Sq In", "Sq Cm", "Sq Mm"]

        self.type_dropdown = wx.ComboBox(self.grid, choices=types, style=wx.CB_DROPDOWN|wx.CB_READONLY)

        self.type_editor = wx.grid.GridCellChoiceEditor(choices=types)

        self.grid.SetCellEditor(0, 5, self.type_editor)

        self.grid.SetCellRenderer(0, 5, wx.grid.GridCellAutoWrapStringRenderer())

 

        # Set initial value for Type column

        self.grid.SetCellValue(0, 5, types[0])

 

        # Make Total and Grand Total cells read-only

        for i in range(self.grid.GetNumberRows()):

            self.grid.SetReadOnly(i, 2)

            self.grid.SetReadOnly(i, 3)

 

        # Set Type column values

        self.grid.SetCellValue(0, 4, "Sq In")

        self.grid.SetCellValue(1, 4, "Sq Cm")

        self.grid.SetCellValue(2, 4, "Sq Mm")

 

        # Populate grid with data from LeatherSqIn object

        for i, row in enumerate(self.ls.get_data()):

            for j, val in enumerate(row):

                self.grid.SetCellValue(i, j, str(val))

                if j == 0:  # Check if first column

                    self.grid.SetCellValue(i, j+1, "Sq In")  # Set default value for column 2

                    if i == 0 and j == 5:

                        self.grid.SetCellEditor(i, j, wx.grid.GridCellChoiceEditor(choices=["Sq In", "Sq Cm", "Sq Mm"]))

                else:

                    self.grid.SetCellValue(i, j, str(val))

 

        # Calculate totals and grand total

        for i, row in enumerate(self.ls.get_data()):

            self.ls.calculate_total(row)

        grandTotal = 0.0

        total = 0.0

        self.ls.calculate_grand_total(grandTotal, total)

 

        # Bind events

        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.OnCellChange)

        self.grid.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)  # Bind the key down event

 

        # Add grid and button sizers to top sizer

        sizer = wx.BoxSizer(wx.VERTICAL)

        sizer.Add(self.grid, 1, wx.ALL)

        self.SetSizer(sizer)

 

        # Set grid line width for last column to 0

        self.grid.SetCellHighlightPenWidth(0)

 

        # Set grid cursor and focus

        # Select cell R0C0 and set focus

        wx.CallAfter(self.grid.SetGridCursor, 0, 0)

        wx.CallAfter(self.grid.MakeCellVisible, 0, 0)

        wx.CallAfter(self.grid.SetFocus)

 

    def OnCellChange(self, event):

        print("OnCellChange called")

        row, col = event.GetRow(), event.GetCol()

        value = self.grid.GetCellValue(row, col)

 

        # update total for the row

        if col == 0 or col == 1:

            try:

                self.grid.SetCellValue(row, 2, f'{self.ls.calc_sqin(float(self.grid.GetCellValue(row, 0)), float(self.grid.GetCellValue(row, 1)))}')

            except ValueError:

                pass

 

        # update grand total

        grandTotal = 0.0

        for row in range(1, self.grid.GetNumberRows()):

            try:

                grandTotal = self.ls.calculate_grand_total(grandTotal, self.grid.GetCellValue(row, 2))

            except ValueError:

                pass

        self.grid.SetCellValue(0, 3, f'{grandTotal:.2f}')

 

        # handle key events

        if isinstance(event, wx.grid.GridEvent) and event.GetEventType() == wx.EVT_KEY_DOWN:

            print("Key event captured")

            print(event)

            keycode = event.GetKeyCode()

            if keycode == wx.WXK_TAB:

                print("OnCellChange:TAB called")

                if col == 0:

                    if event.ShiftDown():

                        self.grid.SetGridCursor(row, col-1)

                    else:

                        self.grid.SetGridCursor(row, col+1)

                else:

                    self.grid.SetGridCursor(row+1, 0)

            elif keycode == wx.WXK_RETURN:

                print("OnCellChange:Enter called")

                self.grid.SetGridCursor(row+1, 0)

 

        # update dropdown options for type column

        if col == 5 and row == 0:

            # update the total and grand total when the dropdown is changed

            try:

                length = float(self.grid.GetCellValue(row, 0))

                width = float(self.grid.GetCellValue(row, 1))

                type_ = self.grid.GetCellValue(row, 5)

                sqin = self.ls.calc_sqin(length, width, type_)

                self.grid.SetCellValue(row, 2, f'{sqin:.2f}')

 

                grandTotal = 0.0

                for row in range(1, self.grid.GetNumberRows()):

                    try:

                        grandTotal = self.ls.calculate_grand_total(grandTotal, self.grid.GetCellValue(row, 2))

                    except ValueError:

                        pass

                self.grid.SetCellValue(0, 3, f'{grandTotal:.2f}')

            except ValueError:

                pass

 

    def OnClearButtonClick(self, event):

       self.grid.ClearGrid()

 

    def OnKeyDown(self, event):

        print ("Key was hit")

        keyCode = event.GetKeyCode()

        row, col = self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()

 

        if keyCode == wx.WXK_RETURN:

            # If ENTER is pressed, move to next row, column 0

            if row < self.grid.GetNumberRows() - 1:

                self.grid.SetGridCursor(row + 1, 0)

        elif keyCode == wx.WXK_TAB:

            # If TAB is pressed, move to the next column or row

            if col == 0:

                # If on column 0, move to column 1

                self.grid.SetGridCursor(row, col + 1)

            elif col == 1:

                # If on column 1, move to next row, column 0

                if row < self.grid.GetNumberRows() - 1:

                    self.grid.SetGridCursor(row + 1, 0)

            else:

                # If on any other column, move to next row, column 0

                if row < self.grid.GetNumberRows() - 1:

                    self.grid.SetGridCursor(row + 1, 0)

 

    def OnQuitButtonClick(self, event):

        self.GetParent().Close()

Here is the alwlogic.py


#***********************************************************************
'''
    Module Name :  alwlogic.py
    Author      : Chris Anderson
    Create Date : 02/23/2023
    Description : Contains the business logic for the Leatherworking Cost Estimator application
    
    This file is Copyright Anderson Leather Works (c) 2023
'''

# Imports
import sqlite3

# From Imports
from commons.dbCommon import DBCommon

#***********************************************************************
# File Last Update and Person
# 02/23/2023 Chris Anderson Initial file
# 03/01/2023 Chris Anderson - Added class getLabels
# 03/02/2003 Chris Anderson - Added class dbDataPull
#***********************************************************************

# Instatiate the DB call before any classes so I can use them in all classes
dbName ='./database/alw-lwc.db'
db = DBCommon(dbName)

class dbDataPull:
    def __init__(self):
        dbName ='./database/alw-lwc.db'

    def _getFormattedTableFieldNames(self, dbTableName):
        field_names = db._dbGetTableFieldNames(dbTableName)  
        field_names = [name.replace('_', ' ').title() for name in field_names] 
        return field_names

    def _getRawTableFieldNames(self, dbTableName):
        field_names = db._dbGetTableFieldNames(dbTableName)  
        return field_names
    
    def _GetTableFieldNameAndType(self, dbTableName):
        field_data = db._dbGetTableFieldNamesAndTypes(dbTableName)  
        return field_data   

    def _GetTableFieldData(self, dbTableName):
        field_data = db._dbGetTableFieldAllData(dbTableName)  
        return field_data   

    def _GetTableFieldData(self, dbTableName):
        field_data = db._dbGetTableFieldAllData(dbTableName)  
        return field_data   
    
class General:
    def __init__(self):
        pass

    '''
        Function Name : getOffset
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : returns a tuple containing the cell_name and new_cell_label, where
                        cell_name is the cell label with spaces replaced by underscores and
                        new_cell_label is the cell label with added offset spaces and a colon.
                        offsetlen is an optional parameter with a default value of 10.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def getOffset(cell_label, offsetlen=10):
        offset = (offsetlen - len(cell_label)) * ' '
        new_cell_label = cell_label + offset + " : "
        cell_name = cell_label.replace(" ", "_")    
        return cell_name, new_cell_label

class LeatherSqIn:
    def __init__(self):
        self.rows = [[0, 0, 0, 0] for _ in range(20)]

    '''
        Function Name : calc_sqin
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : calculates the square inches for a given length and width.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def calc_square(self, length, width):
        return round(length * width,2)

    '''
        Function Name : sqin2sqcm
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : Converts a value from square inches to square centimeters.
        Parameters    :
            - squaredNumber: The value to be converted in square inches.
        Returns       : The value in square centimeters.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def sqin2sqcm(self, squaredNumber):
        return round(squaredNumber * 6.4516,2)
        
    '''
        Function Name : sqin2sqmm
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : Converts a value from square inches to square millimeters.
        Parameters    :
            - squaredNumber: The value to be converted in square inches.
        Returns       : The value in square millimeters.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def sqin2sqmm(self, squaredNumber):
        return round(squaredNumber * 645.16,2)

    '''
        Function Name : sqcm2sqin
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : Converts a value from square centimeters to square inches.
        Parameters    :
            - squaredNumber: The value to be converted in square centimeters.
        Returns       : The value in square inches.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def sqcm2sqin(self, squaredNumber):
        return round(squaredNumber / 6.4516,2)
        
    '''
        Function Name : sqcm2sqmm
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : Converts a value from square centimeters to square millimeters.
        Parameters    :
            - squaredNumber: The value to be converted in square centimeters.
        Returns       : The value in square millimeters.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def sqcm2sqmm(self, squaredNumber):
        return round(squaredNumber / 100,2)

    '''
        Function Name : sqmm2sqin
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : Converts a value from square millimeters to square inches.
        Parameters    :
            - squaredNumber: The value to be converted in square millimeters.
        Returns       : The value in square inches.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def sqmm2sqin(self, squaredNumber):
        return round(squaredNumber / 645.16,2)
        
    '''
        Function Name : sqmm2sqcm
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : Converts a value from square millimeters to square centimeters.
        Parameters    :
            - squaredNumber: The value to be converted in square millimeters.
        Returns       : The value in square centimeters.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def sqmm2sqcm(self, squaredNumber):
        return round(squaredNumber / 1000,2)
        
    '''
        Function Name : calculate_grand_total
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : calculates the grand total for a given grandtotal and total. If the total
                        is None, it is set to 0.0 before calculating the grand total.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def calculate_grand_total(self, grandtotal, total):
        if total is None:
            total = 0.0
        return round(float(grandtotal) + float(total),2)

    '''
        Function Name : calculate_total
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : calculates the total square inches for a given row in the table by multiplying
                        the length and width columns. If either length or width is 0 or empty, the total
                        for that row is set to 0.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def calculate_total(self, row):
        length, width = row[0], row[1]
        if length and width:
            total = length * width
        else:
            total = 0
        row[2] = total

    '''
        Function Name : get_data
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : returns the current rows data of the LeatherSqIn instance.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def get_data(self):
        return self.rows

    '''
        Function Name : get_grand_total_row
        Author        : Chris Anderson
        Create Date   : 02/23/2023
        Description   : returns the grand total row of the LeatherSqIn instance.
        This file is Copyright Anderson Leather Works (c) 2023
    '''
    def get_grand_total_row(self):
        return self.rows[0]


leather_sqin = LeatherSqIn()

Hi Chris,

I can’t run your code as it has a dependency on the alwlogic module which you don’t supply.

Would it be possible for you to post the complete code for a simpler example that demonstrates the issue?

I will have to get the file this weekend and upload the alwlogic as well - sry - wife saying dinner time. :slight_smile:

Ok - added to OP

Thank you. Unfortunately, that introduces another dependency:

from commons.dbCommon import DBCommon

Which in turn may pull in other modules, or even the whole application!

This is why it is better to show a simple, runnable, stand-alone example that reproduces the issue.

Here is a very quick example. I took one of Mike Driscoll’s grid tutorial programs and added your OnKeyDown() event handler to it.

One thing I noticed was that you need to call event.Skip() in an else clause, otherwise you can’t edit the cells.

import wx
import wx.grid as gridlib

class MyForm(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="Grid Tutorial Two", size=(650,320))
        panel = wx.Panel(self)

        self.grid = gridlib.Grid(panel)
        self.grid.CreateGrid(15, 6)

        # RPT: try to make selected text visible when the grid loses focus
        self.grid.SetSelectionForeground(wx.BLACK)


        self.grid.SetCellValue(0,0, "Hello")
        self.grid.SetCellFont(0, 0, wx.Font(12, wx.ROMAN, wx.ITALIC, wx.NORMAL))
        print(self.grid.GetCellValue(0,0))

        self.grid.SetCellValue(1,1, "I'm in red!")
        self.grid.SetCellTextColour(1, 1, wx.RED)

        self.grid.SetCellBackgroundColour(2, 2, wx.CYAN)


        self.grid.SetCellValue(3, 3, "This cell is read-only")
        self.grid.SetReadOnly(3, 3, True)

        self.grid.SetCellEditor(5, 0, gridlib.GridCellNumberEditor(1,1000))
        self.grid.SetCellValue(5, 0, "123")
        self.grid.SetCellEditor(6, 0, gridlib.GridCellFloatEditor())
        self.grid.SetCellValue(6, 0, "123.34")
        self.grid.SetCellEditor(7, 0, gridlib.GridCellNumberEditor())

        self.grid.SetCellSize(11, 1, 3, 3)
        self.grid.SetCellAlignment(11, 1, wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
        self.grid.SetCellValue(11, 1, "This cell is set to span 3 rows and 3 columns")

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.grid)
        panel.SetSizer(sizer)

        self.grid.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)  # Bind the key down event


    def OnKeyDown(self, event):

        print("Key was hit")

        keyCode = event.GetKeyCode()

        row, col = self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()

        if keyCode == wx.WXK_RETURN:

            # If ENTER is pressed, move to next row, column 0

            if row < self.grid.GetNumberRows() - 1:
                self.grid.SetGridCursor(row + 1, 0)

        elif keyCode == wx.WXK_TAB:

            # If TAB is pressed, move to the next column or row

            if col == 0:

                # If on column 0, move to column 1

                self.grid.SetGridCursor(row, col + 1)

            elif col == 1:

                # If on column 1, move to next row, column 0

                if row < self.grid.GetNumberRows() - 1:
                    self.grid.SetGridCursor(row + 1, 0)

            else:

                # If on any other column, move to next row, column 0

                if row < self.grid.GetNumberRows() - 1:
                    self.grid.SetGridCursor(row + 1, 0)

        else:
            event.Skip()



if __name__ == "__main__":
    app = wx.App()
    frame = MyForm()
    frame.Show()
    app.MainLoop()

This form doesn’t use the database, so any references can be commented out.

I can add that, but the issue isn;t editing. I can click on a cell and edit it just fine (Or maybe I misunderstood). The issue is that TAB and ENTER/RETRUN do not go to the next cell and set focus.

Apologies, I’m obviously not helping. Please disregard my comments. Hopefully someone more knowledgable than me will be able to assist you.

No, please don’t apologize. I was just making sure my issue is clear.
I can edit the cells, but Tab/Return leaves the current cell but niether goes to the nex cell or rowX,col0 - depending on where it is at. It just leaves the current cell. And nothing else.

here is the demo-grid & non of such behaviour :stuck_out_tongue:
could it be that the massive comments subdue the coding :face_with_hand_over_mouth:

import wx
import wx.grid

class GridFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)

        # Create a wxGrid object
        grid = wx.grid.Grid(self, -1)

        # Then we call CreateGrid to set the dimensions of the grid
        # (100 rows and 10 columns in this example)
        grid.CreateGrid(100, 10)

        # We can set the sizes of individual rows and columns
        # in pixels
        grid.SetRowSize(0, 60)
        grid.SetColSize(0, 120)

        # And set grid cell contents as strings
        grid.SetCellValue(0, 0, 'wxGrid is good')

        # We can specify that some cells are read.only
        grid.SetCellValue(0, 3, 'This is read.only')
        grid.SetReadOnly(0, 3)

        # Colours can be specified for grid cell contents
        grid.SetCellValue(3, 3, 'green on grey')
        grid.SetCellTextColour(3, 3, wx.GREEN)
        grid.SetCellBackgroundColour(3, 3, wx.LIGHT_GREY)

        # We can specify the some cells will store numeric
        # values rather than strings. Here we set grid column 5
        # to hold floating point values displayed with width of 6
        # and precision of 2
        grid.SetColFormatFloat(5, 6, 2)
        grid.SetCellValue(0, 6, '3.1415')

        self.Show()

app = wx.App(0)
GridFrame(None)
app.MainLoop()