Refreshing wxGrid with dynamic contents

BRPocock picture BRPocock · Jan 13, 2012 · Viewed 7.8k times · Source

This doesn't seem to be as simple as I'd hoped:

Using wxWidgets (2.8 stable series), I have a wxGrid (not subclassed) with a custom “data adapter” as a wxGridTableBase-derived class.

wxGrid* grid = new wxGrid (this, ID_TABLE);
grid->SetTable (new TableAdapter (foo, bar, baz));
grid->EnableEditing (false);
sizer->Add(grid, wxSizerFlags (1).Expand());

The “simple” thing that I can't find is a way to refresh the grid when the underlying data model changes. Simply calling wxWindow::Update (pGrid->Update()) is apparently insufficient to actually get the grid to call the underlying wxGridTableBase implementation?

wxGrid* const grid = (wxGrid* const) FindWindow (ID_TABLE);
if (NULL != grid) {
     grid->Update ();
     grid->AutoSizeColumns ();
}

In particular, this grid is acting as a list, and will have rows added and removed from it asynchronously, by either the same or (potentially) another process — it's a shared data list that can be updated by any one of several networked systems. The grid/list itself is effectively read-only; other controls are used to add and remove items, and each row has one boolean-type attribute that can be toggled as well.

It seems that new rows aren't added to the view, and deleting rows will cause intermittent SEGV's in the wx drawing code.

Due to the dynamic/asynchronous updating mechanism, I'm hoping to avoid having to delete and re-add the grid to the window constantly, as I'm sure that will cause all sorts of flicker and nastiness… so, I'll fall back on trying something brute-force like that, if I absolutely must, but I'd strongly prefer to avoid it.

Unfortunately, despite being flagged as the “stable version,” the wxGrid documentation appears to mostly consist of Yet to be written tags.

Updated: I'm becoming suspicious that this is a layout-of-container problem. In drawing the grid, the bottom of the grid (last row) can actually overlap both the wxStaticBox frame around its section of the wxFrame window, as well as part of the status line of the frame. Adding and removing rows doesn't seem to force a re-layout of the container; I'm experimenting with trying to call Layout and the like. Ideally, this should be a scrolling region, but the wxGrid should still be “constrained” within its containing Sizer.

The layout consists, effectively, of a static box, containing a vertical box, the first element of which is a horizontal box of buttons, then the grid, as so:

    --[ Static Box ]------------------------
   |                                        |
   | [Button] [Button] [Button]             |
   |                                        |
   |  -----------------------------------   |
   | |      |   A      |   B   |    C    |  |
   | |-----------------------------------|  |
   | |    1 |   1a     |   1b  |    1c   |  |
   |  -----------------------------------   |
   |                                        |
    ----------------------------------------

Unfortunately, company policy prohibits me from posting screenshots :-(

If it matters, this is (presently) wxGTK-2.8.12 on Fedora 16 (x86_64), although I'm seeing identical behaviour on CentOS5/RHEL5 using the EPEL (Fedora) packages.

Answer

BRPocock picture BRPocock · Jan 30, 2012

After much experimentation, it looks like the “correct” way to force a refresh goes something like this:

bool
CDynamicWxGridTable::AppendRows(const size_t IGNORED _)
{
   wxGrid *grid = GetView();

   if (pGrid != NULL)
   {
      const int iNumRecords = GetNumberRows();
      const int iGridRows = grid->GetNumberRows();
      const int iNeedRows = iNumRecords - iGridRows;

      if (iNeedRows)
      {
         grid->BeginBatch();
         grid->ClearSelection();

         if (grid->IsCellEditControlEnabled())
         {
            grid->DisableCellEditControl();
         }

         {
            wxGridTableMessage pop(this,
                 wxGRIDTABLE_NOTIFY_ROWS_DELETED,
                 0, iGridRows);
            grid->ProcessTableMessage(pop);
         }
         {
            wxGridTableMessage push(this,
                 wxGRIDTABLE_NOTIFY_ROWS_APPENDED,
                 iNumRecords);
            grid->ProcessTableMessage(push);
         }
         grid->AutoSize();
         grid->ForceRefresh();
         grid->EndBatch();
      }
   }

   return true;
}

bool
CDynamicWxGridTable::DeleteRows(const size_t IGNORED pos,
      const size_t IGNORED rows)
{
   return AppendRows(0);
}

These are called during my (5Hz) update routine by grabbing grid and calling its ->AppendRows(1) method, which in turn calls the wxTableBase-derived class's ::AppendRows member.

Unfortunately, since I'm drawing from asynchronous, dynamically-updated records, the wxGrid caching system is still “fighting” me as far as row attributes (if a row changes, such that its GetAttr value should change, it doesn't refresh dynamically, since the above only tests the number of rows that there should be versus the number of rows that actually exist). However, this is a relatively minor bug, and I'm hoping to overcome it through other means.

The “critical” part seems to be synthesizing the delete/append rows messages to the wxGridTable system through ProcessTableMessage… without which, the wxGrid cache seems to fail to notice the changes in the table size.

Incidentally, the crashes due to losing rows were alleviated by putting guards in the ::GetValue(const int row, const int column) method to check for valid values:

if (row < 0 || row > GetNumberRows()) { return L"×"; }
if (col < 0 || col > LAST_COLUMN) { return L"×"; }

These “×” values don't ever seem to display, after adding the crazy message-injection logic above, however.