Scrolling grids simultaneously?

classic Classic list List threaded Threaded
5 messages Options
Reply | Threaded
Open this post in threaded view
|

Scrolling grids simultaneously?

Andrea Gavana
Hi NG,

    I am working on a small software based on a wx.aui.AuiNotebook.
Every page of the notebook contains a wx.grid. Using wx.aui is very
useful as I can split the notebook pages and compare 2 or more grids
side by side.
I was wondering, however: is there a way to scroll simultaneously 2 or
more grid (as Excel does)? I mean, if the user drags the scrollbar for
one grid, the other grid should scroll in such a way that the 2 (or
more) grids display the same row/column interval. I hope it is clear
enough...
Thank you for every suggestion.

--
Andrea.

"Imagination Is The Only Weapon In The War Against Reality."
http://xoomer.virgilio.it/infinity77/


Reply | Threaded
Open this post in threaded view
|

Re: Scrolling grids simultaneously?

Grzegorz Adam Hankiewicz-2
Andrea Gavana wrote:

> Hi NG,
>
>     I am working on a small software based on a wx.aui.AuiNotebook.
> Every page of the notebook contains a wx.grid. Using wx.aui is very
> useful as I can split the notebook pages and compare 2 or more grids
> side by side.
> I was wondering, however: is there a way to scroll simultaneously 2 or
> more grid (as Excel does)? I mean, if the user drags the scrollbar for
> one grid, the other grid should scroll in such a way that the 2 (or
> more) grids display the same row/column interval. I hope it is clear
> enough...
I have done this for different widgets in the same frame. I guess you
could do it across notebooks if you are able to send somehow
messages/events between them.

See the attached test case I made. Does it help? Basically I hook on the
scroll event and *replicate* it to another widget. Only tested under MSW.

--
Grzegorz Adam Hankiewicz, Jefe de producto de TeraVial
Rastertech España S.A.    Tel: +34 918 467 390, ext 18.
http://www.rastertech.es/    [hidden email]

#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4

import random
import sys
import wx
import wx.grid

LIGHT_RED = "#ddaaaa"
LIGHT_YELLOW = "#ffffa6"
SPACER_TOP = 0.1
SPACER_MIDDLE = 0.2
SPACER_BOTTOM = 0.1
BAR_SIZE = 0.3
NUM_DATA = 40
ROW_SIZE = 30
MAGIC_LIST_HEIGHT = 5


class CustomDataTable(wx.grid.PyGridTableBase):
        def __init__(self):
                wx.grid.PyGridTableBase.__init__(self)

                self.colLabels = ['January', 'February', "March", "April",
                        "May", "June", "July", "August", "September", "October",
                        "November", "December"]

                self.dataTypes = [wx.grid.GRID_VALUE_FLOAT] * 12

                self.data = []
                for row in range(NUM_DATA):
                        self.data.append([0] * 12)
                        for f in range(12):
                                self.data[row][f] = (random.random(), random.random())

        # Required methods for the wxPyGridTableBase interface
        def GetNumberRows(self):
                return len(self.data)

        def GetNumberCols(self):
                return len(self.data[0])

        def IsEmptyCell(self, row, col):
                try:
                        return not self.data[row][col]
                except IndexError:
                        return True

        # Get/Set values in the table.  The Python version of these
        # methods can handle any data-type, (as long as the Editor and
        # Renderer understands the type too,) not just strings as in the
        # C++ version.
        def GetValue(self, row, col):
                try:
                        return self.data[row][col]
                except IndexError:
                        return ''

        def SetValue(self, row, col, value):
                try:
                        self.data[row][col] = value
                except IndexError:
                        # add a new row
                        self.data.append([''] * self.GetNumberCols())
                        self.SetValue(row, col, value)

                        # tell the grid we've added a row
                        msg = wx.grid.GridTableMessage(self,            # The table
                                wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, # what we did to it
                                1                                       # how many
                                )

                        self.GetView().ProcessTableMessage(msg)


        #--------------------------------------------------
        # Some optional methods

        # Called when the grid needs to display labels
        def GetColLabelValue(self, col):
                return self.colLabels[col]

        # Called to determine the kind of editor/renderer to use by
        # default, doesn't necessarily have to be the same type used
        # natively by the editor/renderer if they know how to convert.
        def GetTypeName(self, row, col):
                return self.dataTypes[col]

        # Called to determine how the data can be fetched and stored by the
        # editor and renderer.  This allows you to enforce some type-safety
        # in the grid.
        def CanGetValueAs(self, row, col, typeName):
                colType = self.dataTypes[col].split(':')[0]
                if typeName == colType:
                        return True
                else:
                        return False

        def CanSetValueAs(self, row, col, typeName):
                return self.CanGetValueAs(row, col, typeName)


class CustomTableGrid(wx.grid.Grid):
        def __init__(self, parent, bound_widget = None):
                self.bound_widget = bound_widget
                wx.grid.Grid.__init__(self, parent)

                self.table = CustomDataTable()

                # The second parameter means that the grid is to take ownership of the
                # table and will destroy it when done.  Otherwise you would need to
                # keep a reference to it and call it's Destroy method later.
                self.SetTable(self.table, True)

                #self.SetRowLabelSize(0)
                self.SetMargins(0, 0)
                self.AutoSizeColumns(True)
                # Disallow rows stretching vertically and set a fixed height.
                self.DisableDragRowSize()
                self.SetRowMinimalAcceptableHeight(ROW_SIZE)
                self.SetDefaultRowSize(ROW_SIZE, True)
                self.SetScrollRate(self.GetScrollLineX(), ROW_SIZE)
                # Don't let the user change the values.
                self.EnableEditing(False)

                # Create a scrollbar handler.
                self.Bind(wx.EVT_SCROLLWIN, self.did_scroll)
                self.v = 0
                self.locked = False


        def did_scroll(self, event):
                try:
                        if event.GetOrientation() != wx.VERTICAL or self.locked:
                                return

                        print "%4d, grid pos was %d, will be %d" % (self.v,
                                self.GetScrollPos(wx.VERTICAL), event.GetPosition())
                        self.bound_widget.scroll_to(event.GetPosition())
                        self.v += 1
                finally:
                        event.Skip()


        def scroll_to(self, position):
                self.locked = True
                self.Scroll(-1, position)
                self.locked = False


class List_control(wx.ListCtrl):
        def fill(self, bound_widget = None):
                self.bound_widget = bound_widget
                # Create the images with the desired height.
                self.image_list = wx.ImageList(1, ROW_SIZE - 1)
                self.SetImageList(self.image_list, wx.IMAGE_LIST_SMALL)

                self.InsertColumn(0, "Col 1")
                self.InsertColumn(1, "Col 2")
                self.InsertColumn(2, "Col 3")
                for f in range(NUM_DATA):
                        self.Append(("id%4d" % (f + 1), "Ref %d" % f, "total"))

                self.Bind(wx.EVT_SCROLLWIN, self.did_scroll)
                self.v = 0
                self.locked = False


        def did_scroll(self, event):
                try:
                        if event.GetOrientation() != wx.VERTICAL or self.locked:
                                return

                        print "%4d, list pos was %d, will be %d" % (self.v,
                                self.GetScrollPos(wx.VERTICAL), event.GetPosition())
                        self.bound_widget.scroll_to(event.GetPosition())
                        self.v += 1
                finally:
                        event.Skip()


        def scroll_to(self, position):
                self.locked = True
                dif = position - self.GetScrollPos(wx.VERTICAL)
                self.ScrollList(-1, dif * ROW_SIZE)
                #self.Scroll(-1, position)
                self.locked = False



class App(wx.App):
        """Application class."""

        def OnInit(self):
                self.frame = wx.Frame(None, title = "My title", size = (700, 500))
                panel = wx.Panel(self.frame)
                self.SetTopWindow(self.frame)

                hbox = wx.BoxSizer(wx.HORIZONTAL)
                vbox1 = wx.BoxSizer(wx.VERTICAL)
                vbox2 = wx.BoxSizer(wx.VERTICAL)

                # The filler panel allows us to set the 'height' of the list.
                self.filler = wx.Panel(panel)
                self.list = List_control(panel, size = (300, -1),
                        style=wx.LC_REPORT | wx.ALWAYS_SHOW_SB | wx.VSCROLL)
                self.list.fill()
                vbox1.Add(self.filler, 0, wx.EXPAND | wx.ALL, 0)
                vbox1.Add(self.list, 1, wx.EXPAND | wx.ALL, 5)

                self.grid = CustomTableGrid(panel)
                vbox2.Add(self.grid, 1, wx.EXPAND | wx.ALL, 5)
                hbox.Add(vbox1, 0, wx.EXPAND)
                hbox.Add(vbox2, 1, wx.EXPAND)

                # Bind the scrollbars of the widgets.
                self.grid.bound_widget = self.list
                self.list.bound_widget = self.grid

                self.resize_filler()
                panel.SetSizer(hbox)
                self.frame.Show()
                return True


        def resize_filler(self):
                """f() -> None

                Resizes the filler panel to adapt the list control height. The position
                of the list control is moved in such a way that its entries correspond
                to the height of the grids entries.
                """
                # Get the sizes of the list and grid top labels.
                grid_size = ROW_SIZE
                # Unfortunately the height of the list labels is not possible to get,
                # hence the use of the magic 4 number.
                list_size = self.list.GetCharHeight() + MAGIC_LIST_HEIGHT
                self.filler.SetSize((-1, grid_size - list_size))


def main():
        """Main entry point."""
        app = App(redirect = False)
        app.MainLoop()
        print "main() finished running."


if __name__ == "__main__":
        main()

Reply | Threaded
Open this post in threaded view
|

Re: Scrolling grids simultaneously?

Andrea Gavana
Hi Gregorz,

On 1/22/07, Grzegorz Adam Hankiewicz wrote:

> Andrea Gavana wrote:
> > Hi NG,
> >
> >     I am working on a small software based on a wx.aui.AuiNotebook.
> > Every page of the notebook contains a wx.grid. Using wx.aui is very
> > useful as I can split the notebook pages and compare 2 or more grids
> > side by side.
> > I was wondering, however: is there a way to scroll simultaneously 2 or
> > more grid (as Excel does)? I mean, if the user drags the scrollbar for
> > one grid, the other grid should scroll in such a way that the 2 (or
> > more) grids display the same row/column interval. I hope it is clear
> > enough...
>
> I have done this for different widgets in the same frame. I guess you
> could do it across notebooks if you are able to send somehow
> messages/events between them.
>
> See the attached test case I made. Does it help? Basically I hook on the
> scroll event and *replicate* it to another widget. Only tested under MSW.
>

Thank you for the sample! I'll try to adapt it to my needs, but it
seems to work very well :-D

Thanks again.

Andrea.

"Imagination Is The Only Weapon In The War Against Reality."
http://xoomer.virgilio.it/infinity77/


Reply | Threaded
Open this post in threaded view
|

Re: Scrolling grids simultaneously?

Grzegorz Adam Hankiewicz-2
Andrea Gavana wrote:
>> See the attached test case I made. Does it help? Basically I hook on the
>> scroll event and *replicate* it to another widget. Only tested under MSW.
>
> Thank you for the sample! I'll try to adapt it to my needs, but it
> seems to work very well :-D

Actually that example is incomplete. Both widgets bind only to
EVT_SCROLLWIN. Not only this is incomplete but inaccurate: if you press
the arrows instead of dragging event.GetPosition() will always return a
zero. Whether this is a bug or feature I don't know.

However, you can add handlers for EVT_MOUSEWHEEL, EVT_SCROLLWIN_LINEUP,
EVT_SCROLL_LINEDOWN, EVT_SCROLLWIN_PAGEUP, EVT_SCROLLWIN_PAGEDOWN,
EVT_SCROLLWIN_TOP, EVT_SCROLLWIN_BOTTOM. Something I found a mistery is
why these don't get generated when you select an entry in the list and
use the keys to navigate. Apparently to trigger some of this you can
right click on the scrollbar and select one option of the menu. Bizarre.

Since you can't catch these events through key interaction you would
have to process all EVT_KEY_DOWN events and detect movement there,
checking the current position and so forth. For my needs this is not
needed, so I just put a dummy EVT_KEY_DOWN to disable keyboard
navigation, but maybe you have to take care of that.

Given the number increase in events to process and the complexity of the
key down events processing, I think the best would be to create a base
class and use it as a mixin to avoid polluting the code of individual
controls.

Might be nice for a wxpython demo to show off if the mixin can be
improved to allow multiple bound widgets and each of them being able to
scroll to the same line, or a line relative to the total amount of
elements in that widget.

--
Grzegorz Adam Hankiewicz, Jefe de producto de TeraVial
Rastertech España S.A.    Tel: +34 918 467 390, ext 18.
http://www.rastertech.es/    [hidden email]


Reply | Threaded
Open this post in threaded view
|

Re: Scrolling grids simultaneously?

Grzegorz Adam Hankiewicz-2
Grzegorz Adam Hankiewicz wrote:
> Andrea Gavana wrote:
>> Thank you for the sample! I'll try to adapt it to my needs, but it
>> seems to work very well :-D
>
> Actually that example is incomplete [...]

Here's an improved version using the mixin I thought of. Still doesn't
handle keyboard data, and is vertical scrollbar specific. I don't think
I'll improve it more, though.

--
Grzegorz Adam Hankiewicz, Jefe de producto de TeraVial
Rastertech España S.A.    Tel: +34 918 467 390, ext 18.
http://www.rastertech.es/    [hidden email]

#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4

import random
import sys
import wx
import wx.grid

NUM_DATA = 200
ROW_SIZE = 30
MAGIC_LIST_HEIGHT = 5


class Scroll_binder():
        """Inherit to be able to bind vscrolling to another widget."""
        def __init__(self):
                """f() -> Scroll_binder

                Initialises the internal data required for vertical scrolling.
                """
                self._locked = False
                self._bound_widget = None
                self._is_list_control = hasattr(self, "ScrollList")

                self.Bind(wx.EVT_SCROLLWIN, self._did_scroll)
                self.Bind(wx.EVT_MOUSEWHEEL, self._mousewheel)
                self.Bind(wx.EVT_SCROLLWIN_LINEUP, self._lineup)
                self.Bind(wx.EVT_SCROLLWIN_LINEDOWN, self._linedown)
                self.Bind(wx.EVT_SCROLLWIN_PAGEUP, self._pageup)
                self.Bind(wx.EVT_SCROLLWIN_PAGEDOWN, self._pagedown)
                self.Bind(wx.EVT_SCROLLWIN_TOP, self._top)
                self.Bind(wx.EVT_SCROLLWIN_BOTTOM, self._bottom)
                self.Bind(wx.EVT_KEY_DOWN, self._key_down)


        def bind_scroll(self, to):
                self._bound_widget = to


        def _key_down(self, event):
                pass


        def _mousewheel(self, event):
                """Mouse wheel scrolled. Up or down, give or take."""
                if event.m_wheelRotation > 0:
                        do_scroll = self._lineup
                else:
                        do_scroll = self._linedown

                for r in range(event.m_linesPerAction):
                        do_scroll()


        def _pageup(self, event):
                """Clicked on a scrollbar space, performing a page up."""
                if event.GetOrientation() != wx.VERTICAL:
                        event.Skip()
                        return

                pos = self.GetScrollPos(wx.VERTICAL)
                if self._is_list_control:
                        amount = self.GetCountPerPage()
                else:
                        amount = self.GetScrollPageSize(wx.VERTICAL)

                print "PageUp %d from %d" % (amount, pos)
                self._bound_widget.scroll_to(max(0, pos - amount))
                self.scroll_to(max(0, pos - amount))


        def _pagedown(self, event):
                """Clicked on a scrollbar space, performing a page down."""
                if event.GetOrientation() != wx.VERTICAL:
                        event.Skip()
                        return

                pos = self.GetScrollPos(wx.VERTICAL)
                if self._is_list_control:
                        amount = self.GetCountPerPage()
                else:
                        amount = self.GetScrollPageSize(wx.VERTICAL)

                print "PageDown %d from %d" % (amount, pos)
                self._bound_widget.scroll_to(pos + amount)
                self.scroll_to(pos + amount)


        def _top(self, event):
                """Event handler for going to the top."""
                if event.GetOrientation() != wx.VERTICAL:
                        event.Skip()
                        return

                print "Top"
                self._bound_widget.scroll_to(0)
                self.scroll_to(0)


        def _bottom(self, event):
                """Event handler for going to the bottom."""
                if event.GetOrientation() != wx.VERTICAL:
                        event.Skip()
                        return

                print "Bottom"
                pos = 10000000
                self._bound_widget.scroll_to(pos)
                self.scroll_to(pos)


        def _lineup(self, event = None):
                """Event handler for pressing the up arrow."""
                if event and event.GetOrientation() != wx.VERTICAL:
                        event.Skip()
                        return

                pos = self.GetScrollPos(wx.VERTICAL)
                print "LineUp from", pos
                self._bound_widget.scroll_to(pos - 1)
                self.scroll_to(pos - 1)


        def _linedown(self, event = None):
                """Event handler for pressing the down arrow."""
                if event and event.GetOrientation() != wx.VERTICAL:
                        event.Skip()
                        return

                pos = self.GetScrollPos(wx.VERTICAL)
                print "LineDown from", pos
                self._bound_widget.scroll_to(pos + 1)
                self.scroll_to(pos + 1)


        def _did_scroll(self, event):
                """Event handler for manual scrolling."""
                try:
                        if event.GetOrientation() != wx.VERTICAL or self._locked:
                                return

                        print "list pos was %d, will be %d" % (
                                self.GetScrollPos(wx.VERTICAL), event.GetPosition())
                        self._bound_widget.scroll_to(event.GetPosition())
                finally:
                        event.Skip()


        def scroll_to(self, position):
                """f(int) -> None

                Scrolls to a specific vertical position.
                """
                self._locked = True

                if self._is_list_control:
                        dif = position - self.GetScrollPos(wx.VERTICAL)
                        self.ScrollList(-1, dif * ROW_SIZE)
                else:
                        # Presume we are a grid.
                        self.Scroll(-1, position)

                self._locked = False


class CustomDataTable(wx.grid.PyGridTableBase):
        def __init__(self):
                wx.grid.PyGridTableBase.__init__(self)

                self.colLabels = ['January', 'February', "March", "April",
                        "May", "June", "July", "August", "September", "October",
                        "November", "December"]

                self.dataTypes = [wx.grid.GRID_VALUE_FLOAT] * 12

                self.data = []
                for row in range(NUM_DATA):
                        self.data.append([0] * 12)
                        for f in range(12):
                                self.data[row][f] = (random.random(), random.random())

        # Required methods for the wxPyGridTableBase interface
        def GetNumberRows(self):
                return len(self.data)

        def GetNumberCols(self):
                return len(self.data[0])

        def IsEmptyCell(self, row, col):
                try:
                        return not self.data[row][col]
                except IndexError:
                        return True

        # Get/Set values in the table.  The Python version of these
        # methods can handle any data-type, (as long as the Editor and
        # Renderer understands the type too,) not just strings as in the
        # C++ version.
        def GetValue(self, row, col):
                try:
                        return self.data[row][col]
                except IndexError:
                        return ''

        def SetValue(self, row, col, value):
                try:
                        self.data[row][col] = value
                except IndexError:
                        # add a new row
                        self.data.append([''] * self.GetNumberCols())
                        self.SetValue(row, col, value)

                        # tell the grid we've added a row
                        msg = wx.grid.GridTableMessage(self,            # The table
                                wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, # what we did to it
                                1                                       # how many
                                )

                        self.GetView().ProcessTableMessage(msg)


        #--------------------------------------------------
        # Some optional methods

        # Called when the grid needs to display labels
        def GetColLabelValue(self, col):
                return self.colLabels[col]

        # Called to determine the kind of editor/renderer to use by
        # default, doesn't necessarily have to be the same type used
        # natively by the editor/renderer if they know how to convert.
        def GetTypeName(self, row, col):
                return self.dataTypes[col]

        # Called to determine how the data can be fetched and stored by the
        # editor and renderer.  This allows you to enforce some type-safety
        # in the grid.
        def CanGetValueAs(self, row, col, typeName):
                colType = self.dataTypes[col].split(':')[0]
                if typeName == colType:
                        return True
                else:
                        return False

        def CanSetValueAs(self, row, col, typeName):
                return self.CanGetValueAs(row, col, typeName)


class CustomTableGrid(wx.grid.Grid, Scroll_binder):
        def __init__(self, parent):
                wx.grid.Grid.__init__(self, parent)
                Scroll_binder.__init__(self)

                self.table = CustomDataTable()

                # The second parameter means that the grid is to take ownership of the
                # table and will destroy it when done.  Otherwise you would need to
                # keep a reference to it and call it's Destroy method later.
                self.SetTable(self.table, True)

                #self.SetRowLabelSize(0)
                self.SetMargins(0, 0)
                self.AutoSizeColumns(True)
                # Disallow rows stretching vertically and set a fixed height.
                self.DisableDragRowSize()
                self.SetRowMinimalAcceptableHeight(ROW_SIZE)
                self.SetDefaultRowSize(ROW_SIZE, True)
                self.SetScrollRate(self.GetScrollLineX(), ROW_SIZE)
                # Don't let the user change the values.
                self.EnableEditing(False)


class List_control(wx.ListCtrl, Scroll_binder):
        def __init__(self, parent, size, style):
                wx.ListCtrl.__init__(self, parent = parent, size = size, style = style)
                Scroll_binder.__init__(self)

        def fill(self):
                # Create the images with the desired height.
                self.image_list = wx.ImageList(1, ROW_SIZE - 1)
                self.SetImageList(self.image_list, wx.IMAGE_LIST_SMALL)

                self.InsertColumn(0, "Col 1")
                self.InsertColumn(1, "Col 2")
                self.InsertColumn(2, "Col 3")
                for f in range(NUM_DATA):
                        self.Append(("id%4d" % (f + 1), "Ref %d" % f, "total"))


class App(wx.App):
        """Application class."""

        def OnInit(self):
                self.frame = wx.Frame(None, title = "My title", size = (700, 500))
                panel = wx.Panel(self.frame)
                self.SetTopWindow(self.frame)

                hbox = wx.BoxSizer(wx.HORIZONTAL)
                vbox1 = wx.BoxSizer(wx.VERTICAL)
                vbox2 = wx.BoxSizer(wx.VERTICAL)

                # The filler panel allows us to set the 'height' of the list.
                self.filler = wx.Panel(panel)
                self.list = List_control(panel, (300, -1),
                        wx.LC_REPORT | wx.ALWAYS_SHOW_SB | wx.VSCROLL)
                self.list.fill()
                vbox1.Add(self.filler, 0, wx.EXPAND | wx.ALL, 0)
                vbox1.Add(self.list, 1, wx.EXPAND | wx.ALL, 5)

                self.grid = CustomTableGrid(panel)
                vbox2.Add(self.grid, 1, wx.EXPAND | wx.ALL, 5)
                hbox.Add(vbox1, 0, wx.EXPAND)
                hbox.Add(vbox2, 1, wx.EXPAND)

                # Bind the scrollbars of the widgets.
                self.grid.bind_scroll(self.list)
                self.list.bind_scroll(self.grid)

                self.resize_filler()
                panel.SetSizer(hbox)
                self.frame.Show()
                return True


        def resize_filler(self):
                """f() -> None

                Resizes the filler panel to adapt the list control height. The position
                of the list control is moved in such a way that its entries correspond
                to the height of the grids entries.
                """
                # Get the sizes of the list and grid top labels.
                grid_size = ROW_SIZE
                # Unfortunately the height of the list labels is not possible to get,
                # hence the use of the magic 4 number.
                list_size = self.list.GetCharHeight() + MAGIC_LIST_HEIGHT
                self.filler.SetSize((-1, grid_size - list_size))


def main():
        """Main entry point."""
        app = App(redirect = False)
        app.MainLoop()
        print "main() finished running."


if __name__ == "__main__":
        main()