SetSashPosition() bug on windows with nested vertically split SplitterWindow

python-2.6.2, wxpython-2.8.12.1, windows-xp

hi,

i'm experiencing undesirable behaviour when programmatically setting the
sash position of a vertically split SplitterWindow that is nested inside
another vertically split SplitterWindow (but only on windows).

my application saves the sash positions of both SplitterWindows between
invocations so that it can remember where the user left things and
restore them the way they were.

when the nested SplitterWindow is split horizontally, all is ok. on a
mac, it's ok whether the nested SplitterWindow is split vertically or
horizontally. but on windows it only works properly when the nested
SplitterWindow is split horizontally.

on windows, when the nested SplitterWindow is split vertically, and i
call SetSashPosition with a particular number of pixels, the actual
sash position gets set to that number of pixels minus the sash position
of the outer SplitterWindow plus 10 pixels. i have no idea why. it's
as though it's doing a coordinate transformation interpreting the sash
position that i supply as though it were a coordinate relative to the
outer SplitterWindow rather than just a sash position for the nested
SplitterWindow. the 10 pixel offset might have something to do with the
width of the sash and/or border.

i have a workaround in place so i'm ok with it now but i thought i
should mention it and supply a demonstration (below).

when the demo is run multiple times with no arguments (to demonstrate
the undesirable behaviour) and the window is just closed immediately
without changing any sash positions, the nested sash position moves to
the left with each invocation, it outputs:

  Run1:
  default v200 h400
  saving v200 h210

  Run2:
  loading v200 h210
  saving v200 h20

  Run3:
  loading v200 h20
  saving v200 h3

  Run4:
  loading v200 h3
  saving v200 h3

when the demo is run with the argument "f", the workaround is in place and
it works as expected (delete the "delete.me" file first):

  Run1:
  default v200 h400
  saving v200 h400

  Run2:
  loading v200 h400
  saving v200 h400

  Run3:
  loading v200 h400
  saving v200 h400

  Run4:
  loading v200 h400
  saving v200 h400

the demo can also be run with the argument "h" to split the nested SplitterWindow
horizontally rather than vertically to show that the undesirable behaviour is not
exhibited in that situation.

when finished, don't forget to delete the "delete.me" file that the demo creates.

cheers,
raf

#!/usr/bin/env python

# Demonstrate undesirable behaviour on Windows where SetSashPosition
# on a vertically-split SplitterWindow that is nested inside another
# vertically-split SplitterWindow doesn't set the position to the number
# supplied. Instead, it is the expected position minus the width of
# the outer SplitterWindow's sash position minus 10 pixels. This only
# happens when both SplitterWindows are vertical and only on Windows
# (i.e. not on Mac, haven't checked Linux).

···

#
# usage: grr.py [h|f]
# With "h", the nested SpliterWindow is horizontal. Otherwise it is vertical.
# Horizontal works on MacOSX and Windows. Vertical only works on MacOSX.
#
# With "f" (only in vertical mode), a hacky "fix" is applied to ounteract
# the strange behaviour. It seems to work but only by chance.
#
# Run the program multiple times. On MacOSX, it remembers where the sashes were.
# On Windows, the rightmost sash position keeps moving left with each invocation.
# When finished, manually delete the "delete.me" file that is left behind.
#
# 20120314 raf <raf@raf.org>

import wx, sys
class TestApp(wx.App):
  def OnInit(self):
    self.vertical_mode = False if len(sys.argv) == 2 and sys.argv[1] == 'h' else True
    self.fix_mode = True if len(sys.argv) == 2 and sys.argv[1] == 'f' else False
    self.mainframe = MainFrame(self)
    self.SetTopWindow(self.mainframe)
    return True
class MainFrame(wx.Frame):
  def __init__(self, app):
    wx.Frame.__init__(self, None, title='Test', size=(800, 600), name='mainframe')
    self.app = app
    self.build_panel()
    self.Bind(wx.EVT_CLOSE, self.OnClose)
    self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnSplitterChange)
    self.Show()
  def build_panel(self):
    splitter_flags = wx.SP_LIVE_UPDATE | wx.SP_3D
    self.vsplit = wx.SplitterWindow(self, style=splitter_flags)
    self.vsplit.SetMinimumPaneSize(1)
    self.vsplit.SetSashGravity(0.0)
    self.hsplit = wx.SplitterWindow(self.vsplit, style=splitter_flags)
    self.hsplit.SetMinimumPaneSize(1)
    self.hsplit.SetSashGravity(1.0)
    def create_panel(parent, label):
      p = wx.Panel(parent, style=wx.BORDER_SUNKEN)
      sz = wx.BoxSizer(wx.VERTICAL)
      t = wx.StaticText(p, label=label)
      sz.Add(t, 1, wx.EXPAND | wx.ALL, 3)
      p.SetSizer(sz)
      return p, t
    vsplitpos, hsplitpos = self.load_pos(200, 400)
    p1, self.t1 = create_panel(self.vsplit, 'P1 V%s' % vsplitpos)
    p2, self.t2 = create_panel(self.hsplit, 'P2 H%s' % hsplitpos)
    p3, self.t3 = create_panel(self.hsplit, 'P3')
    if self.app.vertical_mode:
      self.hsplit.SplitVertically(p2, p3, hsplitpos)
    else:
      self.hsplit.SplitHorizontally(p2, p3, hsplitpos)
    # Initial sash position for a nested SplitterWindow doesn't seem to work so...
    wx.CallAfter(self.hsplit.SetSashPosition, hsplitpos)
    self.vsplit.SplitVertically(p1, self.hsplit, vsplitpos)
  def OnClose(self, event):
    self.save_pos(self.vsplit.GetSashPosition(), self.hsplit.GetSashPosition())
    event.Skip()
  def OnSplitterChange(self, event):
    self.t1.SetLabel('P1 V%s' % self.vsplit.GetSashPosition())
    self.t2.SetLabel('P2 H%s' % self.hsplit.GetSashPosition())
    event.Skip()
  def save_pos(self, vpos, hpos):
    print('saving v%s h%s' % (vpos, hpos))
    f = open('delete.me', 'w')
    f.write('%s\n' % vpos)
    f.write('%s\n' % hpos)
    f.close()
  def load_pos(self, default_vpos, default_hpos):
    try:
      f = open('delete.me', 'r')
      vpos = int(f.readline()[:-1])
      hpos = int(f.readline()[:-1])
      f.close()
      print('loading v%s h%s' % (vpos, hpos))
    except IOError:
      print('default v%s h%s' % (default_vpos, default_hpos))
      vpos, hpos = default_vpos, default_hpos
    # Why does this fix it?
    if self.app.fix_mode and wx.Platform == '__WXMSW__' and self.app.vertical_mode:
      hpos += vpos - 10
      print('fixed v%s h%s' % (vpos, hpos))
    return vpos, hpos
def main(redirect=False):
  TestApp(redirect=redirect).MainLoop()
if __name__ == '__main__':
  main()
# vi:set ts=4 sw=4:

Splitters can sometimes have a problem with setting the sash position too early. When you create a splitter its initial default size is very small, and then if you set the sash position to some "normal" size the size of the window is too small to accommodate that and usually the splitter will try to reduce it so it is within the size of the window so the sash will then be at a valid position.

Since setting the sash right away is a common thing for splitter windows then the class will try to defer the setting of the sash position until the first EVT_SIZE event because presumably the splitter window has then been sized to what its final initial size will be and the requested sash position will be valid. This works most of the time, but there are a few edge cases where it doesn't and apparently you've found one of them. The typical workaround for this would be to not restore the sash positions right away, but to use wx.CallAfter to call a function that does it later, after the parent window has been shown and the initial layouts have happened.

···

On 3/13/12 5:54 PM, raf wrote:

python-2.6.2, wxpython-2.8.12.1, windows-xp

hi,

i'm experiencing undesirable behaviour when programmatically setting the
sash position of a vertically split SplitterWindow that is nested inside
another vertically split SplitterWindow (but only on windows).

my application saves the sash positions of both SplitterWindows between
invocations so that it can remember where the user left things and
restore them the way they were.

when the nested SplitterWindow is split horizontally, all is ok. on a
mac, it's ok whether the nested SplitterWindow is split vertically or
horizontally. but on windows it only works properly when the nested
SplitterWindow is split horizontally.

on windows, when the nested SplitterWindow is split vertically, and i
call SetSashPosition with a particular number of pixels, the actual
sash position gets set to that number of pixels minus the sash position
of the outer SplitterWindow plus 10 pixels. i have no idea why. it's
as though it's doing a coordinate transformation interpreting the sash
position that i supply as though it were a coordinate relative to the
outer SplitterWindow rather than just a sash position for the nested
SplitterWindow. the 10 pixel offset might have something to do with the
width of the sash and/or border.

i have a workaround in place so i'm ok with it now but i thought i
should mention it and supply a demonstration (below).

when the demo is run multiple times with no arguments (to demonstrate
the undesirable behaviour) and the window is just closed immediately
without changing any sash positions, the nested sash position moves to
the left with each invocation, it outputs:

--
Robin Dunn
Software Craftsman

Robin Dunn wrote:

>python-2.6.2, wxpython-2.8.12.1, windows-xp
>
>hi,
>
>i'm experiencing undesirable behaviour when programmatically setting the
>sash position of a vertically split SplitterWindow that is nested inside
>another vertically split SplitterWindow (but only on windows).
>
>my application saves the sash positions of both SplitterWindows between
>invocations so that it can remember where the user left things and
>restore them the way they were.
>
>when the nested SplitterWindow is split horizontally, all is ok. on a
>mac, it's ok whether the nested SplitterWindow is split vertically or
>horizontally. but on windows it only works properly when the nested
>SplitterWindow is split horizontally.
>
>on windows, when the nested SplitterWindow is split vertically, and i
>call SetSashPosition with a particular number of pixels, the actual
>sash position gets set to that number of pixels minus the sash position
>of the outer SplitterWindow plus 10 pixels. i have no idea why. it's
>as though it's doing a coordinate transformation interpreting the sash
>position that i supply as though it were a coordinate relative to the
>outer SplitterWindow rather than just a sash position for the nested
>SplitterWindow. the 10 pixel offset might have something to do with the
>width of the sash and/or border.
>
>i have a workaround in place so i'm ok with it now but i thought i
>should mention it and supply a demonstration (below).
>
>when the demo is run multiple times with no arguments (to demonstrate
>the undesirable behaviour) and the window is just closed immediately
>without changing any sash positions, the nested sash position moves to
>the left with each invocation, it outputs:
>

Splitters can sometimes have a problem with setting the sash
position too early. When you create a splitter its initial default
size is very small, and then if you set the sash position to some
"normal" size the size of the window is too small to accommodate
that and usually the splitter will try to reduce it so it is within
the size of the window so the sash will then be at a valid position.

Since setting the sash right away is a common thing for splitter
windows then the class will try to defer the setting of the sash
position until the first EVT_SIZE event because presumably the
splitter window has then been sized to what its final initial size
will be and the requested sash position will be valid. This works
most of the time, but there are a few edge cases where it doesn't
and apparently you've found one of them. The typical workaround for
this would be to not restore the sash positions right away, but to
use wx.CallAfter to call a function that does it later, after the
parent window has been shown and the initial layouts have happened.

You might not have noticed that I am already calling SetSashPosition
via CallAfter in the demo:

  # Initial sash position for a nested SplitterWindow doesn't seem to work so...
  wx.CallAfter(self.hsplit.SetSashPosition, hsplitpos)

So I don't think that this problem is necessarily an instance of what
you are describing.

Cheers,
raf

···

On 3/13/12 5:54 PM, raf wrote: