Is It Possible To Embed Controls In wxgrid?

I was wondering it it was possible to embed wxpython controls directly into a wx,grid cell. An example would be a wx,Choice control.

Yes. Have a look at the wxPython demo. The Grid demo includes such examples.

If you don’t have the demo, have a look here: Index of /wxPython4/extras
Go to the subdirectory matching your wxPython version and download the demo archive.
Then unpack the archive and run demo.py.

1 Like

Thanks for the tip. I downloaded the demo. I saw examples of using the built-in wx.grid cell renderers (like float and bool). I did not see and example where you can embed a GUI control like wx.choice directly into a grid cell. Am I missing something? Thanks.

E.g. GridCustEditor.py: this uses a text control, but you can use others as well:

class MyCellEditor(gridlib.GridCellEditor):
    ...
    def Create(self, parent, id, evtHandler):
        self._tc = wx.TextCtrl(parent, id, "")
        ...
        self.SetControl(self._tc)

Ah ok, Thanks so much, I’ll play around with it. I’m writing an app to control dozens of radios remotely, so I opted for a grid monitoring design. I have it running fine as a prototype.

Well, if the number of radios does not change a lot, you still may use grid sizer in a scrolled window.
Depending on your requirements, that could be easier.

I’ll send you a screen shot when I have it running. Could be hundreds of radio that need to be monitored. In order to do this I had to design a new type of radio driver architecture. It’s works well talking to wxpython.

I would say, like Dietmar, wx.ScrolledWindow as a base and on that any mix of sizers/controls works; if the underlying window manager gets too full the load can be ‘blocked’ and scrolling is still ok (almost like virtual scroll)

I have the basic wx.grid / Editor code working to display a list of embedded wx.choice controls. I was wondering if there was a way to force the grid cell to display the first choice in the wx.choice widget. It seems to require a click into the cell first to activate the control.

Source code:

import wx
import wx.grid as gridlib

import string
#---------------------------------------------------------------------------
class MyCellEditor(gridlib.GridCellEditor):
#class MyCellEditor(wx.grid.GridCellFloatEditor):
    """
    This is a sample GridCellEditor that shows you how to make your own custom
    grid editors.  All the methods that can be overridden are show here.  The
    ones that must be overridden are marked with "*Must Override*" in the
    docstring.
    The sample custom editor is a choice editor.

    Notice that in order to call the base class version of these special
    methods we use the method name preceded by "base_".  This is because these
    methods are "virtual" in C++ so if we try to call wx.grid.GridCellEditor. Create
    for example, then when the wx.Python extension module tries to call
    ptr->Create(...) then it actually calls the derived class version which
    looks up the method in this class and calls it, causing a recursion loop.
    If you don't understand any of this, don't worry, just call the "base_"
    version instead.

    NB: It comes from GridCustEditor.py available in the demo.
    """
    def __init__(self, anotherparent,controldata):
        self.superParent = anotherparent
        self.controlData = controldata
        self.choicesList = self.controlData['choices']
        print("Choices List ", self.choicesList) 
        wx.grid.GridCellEditor.__init__(self)


    def Create(self, parent, id, evtHandler):
        """
        Called to create the control, which must derive from wxControl.
        *Must Override*
        """
        self._tc = wx.Choice(parent, id, (100, 50), choices = self.choicesList)
        # display top of the lchoice list element
        self._tc.SetSelection(0) # added by rtg - no effect on grid display

        self.SetControl(self._tc)
        if evtHandler:
            self._tc.PushEventHandler(evtHandler)


    def SetSize(self, rect):
        """
        Called to position/size the edit control within the cell rectangle.
        If you don't fill the cell (the rect) then be sure to override
        PaintBackground and do something meaningful there.
        """
        #self.log.write("MyCellEditor: SetSize %s\n" % rect)
        self._tc.SetSize(rect.x, rect.y, rect.width+2, rect.height+2,
                               wx.SIZE_ALLOW_MINUS_ONE)


    def Show(self, show, attr):
        """
        Show or hide the edit control.  You can use the attr (if not None)
        to set colours or fonts for the control.
        """
        #self.log.write("MyCellEditor: Show(self, %s, %s)\n" % (show, attr))
        #self.base_Show(show, attr)
        super(MyCellEditor, self).Show(show, attr)


    def PaintBackground(self, rect, attr):
        """
        Draws the part of the cell not occupied by the edit control.  The
        base  class version just fills it with background colour from the
        attribute.  In this class the edit control fills the whole cell so
        don't do anything at all in order to reduce flicker.
        """
        pass
       # self.log.write("MyCellEditor: PaintBackground\n")


    def BeginEdit(self, row, col, grid):
        """
        Fetch the value from the table and prepare the edit control
        to begin editing.  Set the focus to the edit control.
        *Must Override*
        """
        #self.log.write("MyCellEditor: BeginEdit (%d,%d)\n" % (row, col))
        self.startValue = grid.GetTable().GetValue(row, col)
        print("Start Value ", self.startValue)
        self._tc.SetStringSelection(self.startValue)
        self._tc.SetFocus()


    def EndEdit(self, row, col, grid, oldVal):
        """
        Complete the editing of the current cell. Returns True if the value
        has changed.  If necessary, the control may be destroyed.
        *Must Override*
        """
        val = ''
        print("In ENDEDIT")
        #self.log.write("MyCellEditor: EndEdit (%d,%d)\n" % (row, col))
        changed = False
        #val = self._tc.GetValue()
        val = grid.GetTable().GetValue(row, col) # update the table
        
        valueIndex = self._tc.GetCurrentSelection() # this works
        textSelection = self.choicesList[valueIndex]
        print("Get choice current selection ", textSelection)
        # update the cell with the new value
        self.superParent.setCellValue(row,col,textSelection)
        #self.SetCellValue(0, 0, textSelection)
        
        #if val != oldVal:   #self.startValue:
         #   return val

    def ApplyEdit(self, row, col, grid):
        """
        This function should save the value of the control into the
        grid or grid table. It is called only after EndEdit() returns
        a non-None value.
        *Must Override*
        """
        print("In APPLYEDIT")
        #self.log.write("MyCellEditor: ApplyEdit (%d,%d)\n" % (row, col))
        #val = self._tc.SetSelection()
        #print("Got ApplyEdit value", val)
        grid.GetTable().SetValue(row, col, val) # update the table
        val = grid.GetTable().GetValue(row, col) # update the table
        val = self._tc.GetCurrentSelection()
        print("ApplyEdit ", val)

        self.startValue = ''
        self._tc.SetValue('')


    def Reset(self):
        """
        Reset the value in the control back to its starting value.
        *Must Override*
        """
        pass
        #self.log.write("MyCellEditor: Reset\n")
        #self._tc.SetStringSelection(self.startValue)
        #self._tc.SetInsertionPointEnd()


    def IsAcceptedKey(self, evt):
        """
        Return True to allow the given key to start editing: the base class
        version only checks that the event has no modifiers.  F2 is special
        and will always start the editor.
        """
        
        #self.log.write("MyCellEditor: IsAcceptedKey: %d\n" % (evt.GetKeyCode()))

        ## Oops, there's a bug here, we'll have to do it ourself..
        ##return self.base_IsAcceptedKey(evt)

        return (not (evt.ControlDown() or evt.AltDown()) and
                evt.GetKeyCode() != wx.WXK_SHIFT)


    def StartingKey(self, evt):
        """
        If the editor is enabled by pressing keys on the grid, this will be
        called to let the editor do something about that first key if desired.
        """
        self.log.write("MyCellEditor: StartingKey %d\n" % evt.GetKeyCode())
        key = evt.GetKeyCode()
        ch = None
        if key in [wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, wx.WXK_NUMPAD4,
                   wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8, wx.WXK_NUMPAD9]:
            ch = ch = chr(ord('0') + key - wx.WXK_NUMPAD0)

        elif key < 256 and key >= 0 and chr(key) in string.printable:
            ch = chr(key)
            if not evt.ShiftDown():
                ch = ch.lower()

        if ch is not None:
            self._tc.SetStringSelection(ch)
        else:
            evt.Skip()


    def StartingClick(self):
        """
        If the editor is enabled by clicking on the cell, this method will be
        called to allow the editor to simulate the click on the control if
        needed.
        """
        pass
        #self.log.write("MyCellEditor: StartingClick\n")


    def Destroy(self):
        """final cleanup"""
        #self.log.write("MyCellEditor: Destroy\n")
        self.base_Destroy()


    def Clone(self):
        """
        Create a new object which is the copy of this one
        *Must Override*
        """
        self.log.write("MyCellEditor: Clone\n")
        return MyCellEditor(self.log)


#---------------------------------------------------------------------------
class GridEditorTest(wx.grid.Grid):
    def __init__(self, parent, metadatalist):
        wx.grid.Grid.__init__(self, parent, -1)
        metaDataList = metadatalist
        anotherParent = self
        self.log = None
        self.CreateGrid(10, 3)

        # task is to create pop-up controls for first row of GUI
        row = 0
        column = 0
        for control in metaDataList:
            # but for this example, we'll just set the custom editor on one cell
            #self.SetCellEditor(row, column, MyCellEditor(anotherParent,control))
            # and on a column
            print("Control data ", control)
            self.SetColLabelValue(column, control['columnname'])
            self.SetCellEditor(0, column, MyCellEditor(anotherParent,control))
            attr = wx.grid.GridCellAttr()
            attr.SetEditor(MyCellEditor(anotherParent,control))
            self.SetColAttr(column, attr)
            #self.SetCellValue(row, column, control['columnname'])
            self.SetColSize(row, 150)
            #self.SetColSize(1, 150)
            #self.SetColSize(2, 75)
            # need cell refresh command ????
            # refresh a cell stub
            column += 1

    def setCellValue(self,row, col,textString):
        print("setCellValue ",textString)
        self.SetCellValue(row, col, textString)


#---------------------------------------------------------------------------

class TestFrame(wx.Frame):
    def __init__(self, parent, log):
        wx.Frame.__init__(self, parent, -1, "Custom Grid Cell Editor Test",
                         size=(640,480))
        # create GUI pop-up controls
        metaDataList =[]
        MODE = {}
        MODE['row'] = 0
        MODE['column'] =0
        MODE['columnname'] ="Mode Control"
        MODE['controltype'] = 'wxchoice'
        MODE['choices'] =   ["Select Mode", "CW", "CW1", "LSB", "USB", "ISB", "AM", "SYNC", "FM", "DIG"]
        metaDataList.append(MODE)
        BW = {}
        BW['row'] = 0
        BW['column'] =1
        BW['columnname'] ="Bandwidth Control"
        BW['controltype'] = 'wxchoice'
        BW['choices'] = ['Select Bandwidth','16000', '15200', '14400', '13600', '12800','12000','11200','10400','9600', '8800','8000','7600','7200','6800','6400','6000','5600','5200','4800','4400','4000', '3800', '3600', '3400', '3200','3000','2800', '2600', '2400', '2200', '2000', '1900', '1800', '1700','1600', '1500', '1400','1300','1200','1100','1000', '900', '800','700','600', '500','450', '400','350', '300','250','220','200','170','150','120','100']
        metaDataList.append(BW)

        AGC = {}
        AGC['row'] = 0
        AGC['column'] =2
        AGC['columnname'] ="AGC Control"
        AGC['controltype'] = 'wxchoice'
        AGC['choices'] =  ['Select Agc' ,'Slow', 'Medium', 'Fast']
        metaDataList.append(AGC)
     
        grid = GridEditorTest(self, metaDataList)

#---------------------------------------------------------------------------

if __name__ == '__main__':
    import sys
    app = wx.App(False)
    frame = TestFrame(None, sys.stdout)
    frame.Show(True)
    app.MainLoop()

well, is not quite what I thought of but, anyway, I rather like it (wouldn’t have thought anything like that was possible) :ghost:

To define what is showing in the cell when the editor is not active you need to create a cell renderer class which will draw the content of the cell. There are examples of this in the demo.