App works on wxGTK/Linux but not quite on wxMSW

Hi -

I'm working on an app which uses one of two different input panels.
It works correctly on wxGTK on Gentoo, but fails on wxMSW. The
initial display is correct, but if I try to switch to the alternate
input panel, the input area is blank (it looks like a small window is
overiting part of the staticbox title).

On the left side of the main frame, I create a panel with a vertical
boxsizer that contains a button on the bottom and a vertical
staticboxsizer above. The staticboxsizer contains the two different
input panels and a "Swap" button; only one of the input panels is
visible at any given time. When the user clicks the "Swap" button, I
turn off one and turn on the other then call Layout() on the parent
panel. The two applicable code snippets are below. So, what am I
doing wrong?

Thanks!

-Todd

This routine constructs the panel which contains the two input sub-
panels...

  def _buildLeftSide(self, conv):
    '''
    Construct the data input side of the panel.
    '''
    self.leftPanel = wx.Panel(self, -1)
    self.leftPanel.SetBackgroundColour(self.panelBkGnd)
    # create the input panels
    self.aip = AngularInputPanel(self.leftPanel, conv = conv)
    self.lip = LinearInputPanel(self.leftPanel, conv = conv)
    self.lip.Show(False)
    # create the swap button
    self.swapBtn = wx.Button(self.leftPanel, -1, 'Switch To Linear
Input')
    self.Bind(wx.EVT_BUTTON, self.onSwap, self.swapBtn)
    # create the static box & sizer
    self.leftBox = wx.StaticBox(self.leftPanel, -1, 'Measurements')
    self.lsbSizer = wx.StaticBoxSizer(self.leftBox, wx.VERTICAL)
    self.lsbSizer.Add(self.aip, 93, wx.ALL | wx.EXPAND, 2)
    self.lsbSizer.Add(self.lip, 93, wx.ALL | wx.EXPAND, 2)
    self.lsbSizer.Add(self.swapBtn, 7, wx.ALL | wx.EXPAND, 2)
    # create the calculate button
    self.calcBtn = wx.Button(self.leftPanel, -1, 'Calculate')
    self.Bind(wx.EVT_BUTTON, self.onCalculate, self.calcBtn)
    # create the panel sizer
    self.leftSizer = wx.BoxSizer(wx.VERTICAL)
    self.leftSizer.Add(self.lsbSizer, 93, wx.ALL | wx.EXPAND, 2)
    self.leftSizer.Add(self.calcBtn, 7, wx.ALL | wx.EXPAND, 2)
    self.leftPanel.SetSizer(self.leftSizer)

This is the 'Swap' button callback which swaps the two input sub-
panels...

  def onSwap(self, evt):
    '''
    Swap data input panels.
    '''
    print 'onSwap'
    if self.inputMode == 'Angular':
      self.aip.Show(False)
      self.lip.Show(True)
      self.swapBtn.SetLabel('Switch To Angular Input')
      self.inputMode = 'Linear'
    else:
      self.aip.Show(True)
      self.lip.Show(False)
      self.swapBtn.SetLabel('Switch To Linear Input')
      self.inputMode = 'Angular'
    self.Layout()

Todd wrote:

Hi -

I'm working on an app which uses one of two different input panels.
It works correctly on wxGTK on Gentoo, but fails on wxMSW. The
initial display is correct, but if I try to switch to the alternate
input panel, the input area is blank (it looks like a small window is
overiting part of the staticbox title).

On the left side of the main frame, I create a panel with a vertical
boxsizer that contains a button on the bottom and a vertical
staticboxsizer above. The staticboxsizer contains the two different
input panels and a "Swap" button; only one of the input panels is
visible at any given time. When the user clicks the "Swap" button, I
turn off one and turn on the other then call Layout() on the parent
panel. The two applicable code snippets are below. So, what am I
doing wrong?

Besides not providing a complete runnable sample? (MakingSampleApps - wxPyWiki)

Try creating the static box before you create the widgets that will appear to be within it. If that doesn't help then my guess is that there is some other window being created that is not being managed by a sizer or that has improper parentage. Try adding the WIT to your app so you can use it to find info about that window.

···

--
Robin Dunn
Software Craftsman

Robin Dunn wrote:

Todd wrote:

Hi -

I'm working on an app which uses one of two different input panels.
It works correctly on wxGTK on Gentoo, but fails on wxMSW. The
initial display is correct, but if I try to switch to the alternate
input panel, the input area is blank (it looks like a small window is
overiting part of the staticbox title).

On the left side of the main frame, I create a panel with a vertical
boxsizer that contains a button on the bottom and a vertical
staticboxsizer above. The staticboxsizer contains the two different
input panels and a "Swap" button; only one of the input panels is
visible at any given time. When the user clicks the "Swap" button, I
turn off one and turn on the other then call Layout() on the parent
panel. The two applicable code snippets are below. So, what am I
doing wrong?

Besides not providing a complete runnable sample? (MakingSampleApps - wxPyWiki)

Try creating the static box before you create the widgets that will appear to be within it. If that doesn't help then my guess is that there is some other window being created that is not being managed by a sizer or that has improper parentage. Try adding the WIT to your app so you can use it to find info about that window.

Opps, forgot the link: http://wiki.wxpython.org/Widget_Inspection_Tool

···

--
Robin Dunn
Software Craftsman

Hi Robin -

First, thanks for creating wxpython!

I tried the reorder, no joy. The WIT did not reveal any obvious
smoking guns (at least, not to me :))

I've created a stripped version of the panel in question; problem
persists... code below.

The wxMSW version is 2.8.9.1. The wxGTK version which works is
2.8.10.1.

Thanks for any insight you can provide!!!

-Todd

···

================

'''
Wx Panel setup for calibrating a servo
'''

import wx
import wx.grid as wg
import wx.lib.mixins.inspection as wxInspector

#from angularInputPanel import AngularInputPanel
#from linearInputPanel import LinearInputPanel

class ServoPanel(wx.Panel):
  '''
  ServoPanel class derived from wx.Panel.

  A panel with data input and plot areas. The user inputs servo
  excitation measurements, presses the Calculate button and the
  required cubic constants are calculated and data is plotted for
  operator inspection.
  '''
  def __init__(self, parent, name = None, statusBar = None, conv =
None):
    '''
    Build the panel, initialize state variables.
    '''
    wx.Panel.__init__(self, parent, name = name)
    self.parent = parent
    self.statusBar = statusBar
    self.k = None
    self.inputMode = 'Angular'
    self.first = True
    self.panelBkGnd = wx.Colour(228, 172, 32)
    self._buildLeftSide(conv)
    self._buildRightSide()
    self.mainSizer = wx.BoxSizer(wx.HORIZONTAL)
    self.mainSizer.Add(self.leftPanel, 1, wx.ALL | wx.EXPAND, 1)
    self.mainSizer.Add(self.rightPanel, 4, wx.ALL | wx.EXPAND, 1)
    self.SetSizer(self.mainSizer)
    self.Layout()

  def _buildLeftSide(self, conv):
    '''
    Construct the data input side of the panel.
    '''
    self.leftPanel = wx.Panel(self, -1, name = 'Left')
    self.leftPanel.SetBackgroundColour(self.panelBkGnd)
    # create the static box & sizer
    self.leftBox = wx.StaticBox(self.leftPanel, -1, 'Servo
Measurements',
                                name = 'Servo Measurements')
    self.lsbSizer = wx.StaticBoxSizer(self.leftBox, wx.VERTICAL)
    # create the input panels
    #self.aip = AngularInputPanel(self.leftPanel, conv = conv)
    self.aip = wx.Panel(self.leftPanel, -1, name = 'AIP - red')
    self.aip.SetBackgroundColour(wx.Colour(128, 0, 0))
    self.lsbSizer.Add(self.aip, 93, wx.ALL | wx.EXPAND, 2)
    #self.lip = LinearInputPanel(self.leftPanel, conv = conv)
    self.lip = wx.Panel(self.leftPanel, -1, name = 'LIP - blue')
    self.lip.SetBackgroundColour(wx.Colour(0, 0, 128))
    self.lip.Show(False)
    self.lsbSizer.Add(self.lip, 93, wx.ALL | wx.EXPAND, 2)
    # create the swap button
    self.swapBtn = wx.Button(self.leftPanel, -1, 'Switch To Linear
Input',
                             name = 'Swap')
    self.Bind(wx.EVT_BUTTON, self.onSwap, self.swapBtn)
    self.lsbSizer.Add(self.swapBtn, 7, wx.ALL | wx.EXPAND, 2)
    # create the calculate button
    self.calcBtn = wx.Button(self.leftPanel, -1, 'Gonculate', name =
'Calc')
    self.Bind(wx.EVT_BUTTON, self.onGonculate, self.calcBtn)
    # create the panel sizer
    self.leftSizer = wx.BoxSizer(wx.VERTICAL)
    self.leftSizer.Add(self.lsbSizer, 93, wx.ALL | wx.EXPAND, 2)
    self.leftSizer.Add(self.calcBtn, 7, wx.ALL | wx.EXPAND, 2)
    self.leftPanel.SetSizer(self.leftSizer)

  def _buildRightSide(self):
    '''
    Construct the graph side of the panel.
    '''
    self.rightPanel = wx.Panel(self, -1, name = 'Right')
    self.rightPanel.SetBackgroundColour(self.panelBkGnd)
    # create the plots box
    self.rightBox1 = wx.StaticBox(self.rightPanel, -1, 'Data Plots',
                                  name = 'Data Plots')
    self.rsb1Sizer = wx.StaticBoxSizer(self.rightBox1, wx.VERTICAL)
    self.plots = wx.Panel(self.rightPanel, -1, name = 'Dummy Plot')
    self.plots.SetBackgroundColour(wx.Colour(96, 96, 96))
    self.rsb1Sizer.Add(self.plots, 1, wx.ALL | wx.EXPAND, 1)
    # create the coeff output
    self.rightBox2 = wx.StaticBox(self.rightPanel, -1, 'Cubic Fit
Coefficients',
                                  name = 'Cubic Fit Coefficients')
    self.rsb2Sizer = wx.StaticBoxSizer(self.rightBox2, wx.HORIZONTAL)
    f = wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD)
    self.k_lbl = [None, None, None, None]
    for i in range(4):
      lbl = wx.StaticText(self.rightPanel, -1, 'K%d = ' % i,
                          style = wx.ALIGN_RIGHT, name = 'K%d Label' %
i)
      lbl.SetFont(f)
      self.rsb2Sizer.Add(lbl, 1, wx.ALL | wx.EXPAND, 1)
      if self.k is None:
        k_val = 0.0
      else:
        k_val = self.k[3 - i]
      self.k_lbl[i] = wx.StaticText(self.rightPanel, -1, '%f' % k_val,
                                    style = wx.ALIGN_LEFT, name = 'K
%d' % i)
      self.rsb2Sizer.Add(self.k_lbl[i], 1, wx.ALL | wx.EXPAND, 1)
    # create the panel sizer
    self.rightSizer = wx.BoxSizer(wx.VERTICAL)
    self.rightSizer.Add(self.rsb1Sizer, 93, wx.ALL | wx.EXPAND, 2)
    self.rightSizer.Add(self.rsb2Sizer, 7, wx.ALL | wx.EXPAND, 2)
    self.rightPanel.SetSizer(self.rightSizer)

  def _errorMessage(self, msg):
    '''
    Display an error message in a modal dialog box.
    '''
    d = wx.MessageDialog(self, msg, 'Data Input Error',
                         wx.OK | wx.ICON_ERROR)
    d.ShowModal()
    d.Destroy()

  def _testData(self):
    '''
    Create a test data set for verification of the class.
    '''
    pass

  def getInputMode(self):
    '''
    Return the current input mode.
    '''
    return self.inputMode

  def onGonculate(self, evt):
    '''
    Validate input, calculate the fit poly, generate the graphs.
    '''
    print 'onGonculate'

  def onSwap(self, evt):
    '''
    Swap data input panels.
    '''
    print 'onSwap'
    if self.inputMode == 'Angular':
      self.aip.Show(False)
      self.lip.Show(True)
      self.swapBtn.SetLabel('Switch To Angular Input')
      self.inputMode = 'Linear'
    else:
      self.aip.Show(True)
      self.lip.Show(False)
      self.swapBtn.SetLabel('Switch To Linear Input')
      self.inputMode = 'Angular'
    self.first = True
    self.Layout()

class ServoFrame(wx.Frame):
  '''
  Test frame for the ServoPanel class.
  '''
  def __init__(self, doTest = False, name = 'Main Frame'):
    '''
    Create the custom frame and status bar.
    Generate the ServoPanel test data if requested.
    '''
    wx.Frame.__init__(self, None, -1, 'ServoFrame', size = (1200,
760),
                      name = name)
    self.statusBar = wx.StatusBar(self, -1, name = name + '
StatusBar')
    self.statusBar.SetFieldsCount(2)
    self.SetStatusBar(self.statusBar)
    self.panel = ServoPanel(self, statusBar = self.statusBar,
                            name = 'Debug Panel')
    if doTest:
      self.panel._testData()
    self.Bind(wx.EVT_CLOSE, self.onCloseWindow)
    self.CenterOnScreen()

  def onCloseWindow(self, event):
    print 'ServoFrame onCloseWindow'
    self.Destroy()

class ServoTestApp(wx.App, wxInspector.InspectionMixin):
  '''
  Test application for the ServoPanel class.
  '''
  def OnInit(self):
    '''
    Create the main window and insert the custom frame.
    '''
    self.Init() # Initialize the inspection tool
    frame = ServoFrame(doTest = True, name = 'Debug Main Frame')
    self.SetTopWindow(frame)
    frame.Show(True)
    return True

if __name__=='__main__':
  '''
  Run the test application class if directly executed.
  '''
  app = ServoTestApp(0)
  app.MainLoop()

On Aug 24, 5:01 pm, Robin Dunn <ro...@alldunn.com> wrote:

Robin Dunn wrote:
> Todd wrote:
>> Hi -

>> I'm working on an app which uses one of two different input panels.
>> It works correctly on wxGTK on Gentoo, but fails on wxMSW. The
>> initial display is correct, but if I try to switch to the alternate
>> input panel, the input area is blank (it looks like a small window is
>> overiting part of the staticbox title).

>> On the left side of the main frame, I create a panel with a vertical
>> boxsizer that contains a button on the bottom and a vertical
>> staticboxsizer above. The staticboxsizer contains the two different
>> input panels and a "Swap" button; only one of the input panels is
>> visible at any given time. When the user clicks the "Swap" button, I
>> turn off one and turn on the other then call Layout() on the parent
>> panel. The two applicable code snippets are below. So, what am I
>> doing wrong?

> Besides not providing a complete runnable sample?
> (MakingSampleApps - wxPyWiki)

> Try creating the static box before you create the widgets that will
> appear to be within it. If that doesn't help then my guess is that
> there is some other window being created that is not being managed by a
> sizer or that has improper parentage. Try adding the WIT to your app so
> you can use it to find info about that window.

Opps, forgot the link:http://wiki.wxpython.org/Widget_Inspection_Tool

--
Robin Dunn
Software Craftsmanhttp://wxPython.org

Todd wrote:

Hi Robin -

First, thanks for creating wxpython!

I tried the reorder, no joy. The WIT did not reveal any obvious
smoking guns (at least, not to me :))

It did to me! :slight_smile: With the WIT it took me about 45 seconds to find the problem, without even looking at the code. Here's what I did:

* Start the app and use the hot-key to open the WIT.
* Click the Find button and then on the red panel, to select that item in the widget tree.
* Noticed the 2 panels, AIP and LIP, and that LIP is not shown.
* Clicked the Switch button in your UI, and saw that LIP did not fill the space vacated by AIP, but was stuck behind the static box instead. (The highlight button helped with this.)
* Selected the parent window (the panel named "Left") in the widget tree
* clicked down in the PyShell and typed "obj.Layout()" and, TaDa! it resized LIP where it was supposed to be.
* Finally looked at your source code and in onSwap changed self.Layout() to self.leftPanel.Layout() and retested.

Here is the reason why this change fixes the problem: Sizers do their work either in the EVT_SIZE handler of the window that has a sizer, or explicitly when window.Layout or sizer.Layout is called. When a sizer does this work it queries each managed widget or sizer what their best size is and then lays them out as needed to fulfill the purpose of the sizer. And that is it. There isn't anything that the sizer does explicitly to cause a recursive descent of the sizers that belong to the child widgets, and it won't happen *unless* the sizer in the parent window happens to resize the child widget. Then the child's sizer will be triggered when it gets its own EVT_SIZE event. (This is actually a good thing in most cases as it optimizes the layout by not doing a lot of needless sizer calculations when nothing has actually changed.) In your case the change made by hiding one panel and showing another did not cause the left panel's best or min size to change, so calling self's Layout method did not cause the left panel to resize, so it did not get the EVT_SIZE event that would have caused the layout to be modified.

Because of this optimization it is usually necessary to call the Layout method of the parent window or containing sizer when a change is made to the UI that requires a new layout be done.

···

--
Robin Dunn
Software Craftsman

Robin Dunn wrote:

Because of this optimization it is usually necessary to call the Layout method of the parent window or containing sizer when a change is made to the UI that requires a new layout be done.

I should add this to the sentence above, ", and not a Layout() further up the containment hierarchy."

···

--
Robin Dunn
Software Craftsman

Hi Robin -

Thank you for the excellent and detailed explanation! I did, indeed,
assume that Layout would cause a recursive decent so it's great to
understand what really happens.

Hopefully, I'll learn how to use the WIT to more benefit.

Out of curiosity,any clue as to why it worked on wxGTK?

Thanks again for a most excellent toolkit! I'm starting to look at
AUI and AGW stuff... very cool!

-Todd

···

On Aug 26, 4:33 pm, Robin Dunn <ro...@alldunn.com> wrote:

Todd wrote:
> Hi Robin -

> First, thanks for creating wxpython!

> I tried the reorder, no joy. The WIT did not reveal any obvious
> smoking guns (at least, not to me :))

It did to me! :slight_smile: With the WIT it took me about 45 seconds to find the
problem, without even looking at the code. Here's what I did:

* Start the app and use the hot-key to open the WIT.
* Click the Find button and then on the red panel, to select that item
in the widget tree.
* Noticed the 2 panels, AIP and LIP, and that LIP is not shown.
* Clicked the Switch button in your UI, and saw that LIP did not fill
the space vacated by AIP, but was stuck behind the static box instead.
(The highlight button helped with this.)
* Selected the parent window (the panel named "Left") in the widget tree
* clicked down in the PyShell and typed "obj.Layout()" and, TaDa! it
resized LIP where it was supposed to be.
* Finally looked at your source code and in onSwap changed self.Layout()
to self.leftPanel.Layout() and retested.

Here is the reason why this change fixes the problem: Sizers do their
work either in the EVT_SIZE handler of the window that has a sizer, or
explicitly when window.Layout or sizer.Layout is called. When a sizer
does this work it queries each managed widget or sizer what their best
size is and then lays them out as needed to fulfill the purpose of the
sizer. And that is it. There isn't anything that the sizer does
explicitly to cause a recursive descent of the sizers that belong to the
child widgets, and it won't happen *unless* the sizer in the parent
window happens to resize the child widget. Then the child's sizer will
be triggered when it gets its own EVT_SIZE event. (This is actually a
good thing in most cases as it optimizes the layout by not doing a lot
of needless sizer calculations when nothing has actually changed.) In
your case the change made by hiding one panel and showing another did
not cause the left panel's best or min size to change, so calling self's
Layout method did not cause the left panel to resize, so it did not get
the EVT_SIZE event that would have caused the layout to be modified.

Because of this optimization it is usually necessary to call the Layout
method of the parent window or containing sizer when a change is made to
the UI that requires a new layout be done.

--
Robin Dunn
Software Craftsmanhttp://wxPython.org

Todd wrote:

Hi Robin -

Thank you for the excellent and detailed explanation! I did, indeed,
assume that Layout would cause a recursive decent so it's great to
understand what really happens.

Hopefully, I'll learn how to use the WIT to more benefit.

Out of curiosity,any clue as to why it worked on wxGTK?

There's probably an extra size event in there someplace.

···

--
Robin Dunn
Software Craftsman