Propagate EVT_MOUSEWHEEL from class derived from Grid to parent (ancestor) ScrolledWindow

Hello,

I have a class MyGrid derived from wx.grid.Grid that through standard layout managers is inside a wx.ScrolledWindow.

I would like MyGrid not to react to mouse wheel events and instead propagate them up to the ScrolledWindow.

Currently I achieve this by catching the event in MyGrid self.Bind(wx.EVT_MOUSEWHEEL, self._on_mouse_wheel) and then sending appropriate commands to the ScrolledWindow:

    def _on_mouse_wheel(self, event):
        ''' Event listener on mouse scroll when the grid is embedded inside a Scrolled Window. It
        makes the scroll window scroll, instead of the grid itself '''

        delta = event.GetWheelDelta()
        rotation = event.GetWheelRotation()
        linesPer = event.GetLinesPerAction()

        wheelAxis = 0 if event.GetWheelAxis() == wx.MOUSE_WHEEL_HORIZONTAL else 1

        mws = self._mouseWheelScroll[wheelAxis]
        mws = mws + rotation
        lines = mws / delta
        mws = mws - lines * delta
        self._mouseWheelScroll[wheelAxis] = mws
        if lines != 0:
            lines = lines * linesPer
            viewStart = self.scrollWin.GetViewStart()
            if wheelAxis == 0:  # notice the different sign in horizontal and vertical logic
                self.scrollWin.Scroll(int(viewStart[wheelAxis] + lines), -1)
            else:
                self.scrollWin.Scroll(-1, int(viewStart[wheelAxis] - lines))

This works but I feel it would be cleaner to implement _on_mouse_wheel in a way that it just forwarded the received mouse wheel event (or an equivalent event) to the scrolled window instead. But I have not succeeded.

I have tried self.scrollWin.AddPendingEvent(event) but it does nothing. I have tried passing event.Clone() instead of the original event as well as wx.PostEvent but none of them does anything.

What is the right way to forward the event to the ancestor window? Thank you

to which control do you Bind the event? something like self.scrollWin.Bind(EVT_MOUSEWHEEL, …) :face_with_hand_over_mouth:
what you call propagate should be done by proper binding (I think)

Thank you. I bind the event to the grid and not the scrolled window. I do it this way because I want the scrolled window (and not the grid) to scroll when the grid has focus. In this case it is the grid that receives the event. When the grid does not have focus, the Scrolled Window is already the window that receives the event and it does scroll without having to do anything.

I have also tried event.ResumePropagation(wx.EVENT_PROPAGATE_MAX) followed by event.Skip() but that also doesn’t work. That this last approach does not work does not surprise me since grid derives from scrolled window, so its event handler does not Skip so the event does not propagate past it.

I have also tried self.scrollWin.ProcessEvent(event) but it also does nothing regardless of whether I also add any or all of the following:

event.ResumePropagation(wx.EVENT_PROPAGATE_MAX),
event.Skip(),
event.SetEventObject(self.scrollWin).

something in this direction :thinking:

import wx
import wx.grid

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent)
        self.SetTitle('grid on scrollable..')

        scrwin = wx.ScrolledWindow(self)                # scrolled
        scrwin.SetScrollRate(1, 60)
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(scrwin, 1, wx.EXPAND)
        self.SetSizer(vbox)

        grid = wx.grid.Grid(scrwin)                         # grid
        grid.CreateGrid(100, 10)
        grid.SetRowSize(0, 60)
        grid.SetColSize(0, 120)
        grid.SetCellValue(0, 0, 'wxGrid is good')
        grid.SetCellValue(0, 3, 'This is read.only')
        grid.SetReadOnly(0, 3)
        grid.SetCellValue(3, 3, 'green on grey')
        grid.SetCellTextColour(3, 3, wx.GREEN)
        grid.SetCellBackgroundColour(3, 3, wx.LIGHT_GREY)
        grid.SetColFormatFloat(5, 6, 2)
        grid.SetCellValue(0, 6, '3.1415')
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(grid, 1, wx.EXPAND)
        scrwin.SetSizer(vbox)

        self.Show()

app = wx.App()
app.TopWindow = Gui(None)
app.MainLoop()

Thank you again Georg!

Your program on my computer behaves just like mine. That is, the mouse wheel does nothing. Does the mouse wheel (or whichever gesture you use to make windows scroll, in my case it is dragging two fingers in the touchpad) make the window scroll for you?

To be clear, the scroll bars are there, and if I select them, they work as expected.

Hello Georg. Thank you again!

To replicate my problem more clearly I modified your program (I simply added three text controls on top of the grid)

import wx
import wx.grid

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent)
        self.SetTitle('grid on scrollable..')

        scrwin = wx.ScrolledWindow(self)                # scrolled
        scrwin.SetScrollRate(1, 60)
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(scrwin, 1, wx.EXPAND)
        self.SetSizer(vbox)

        grid = wx.grid.Grid(scrwin)                         # grid
        grid.CreateGrid(100, 10)
        grid.SetRowSize(0, 60)
        grid.SetColSize(0, 120)
        grid.SetCellValue(0, 0, 'wxGrid is good')
        grid.SetCellValue(0, 3, 'This is read.only')
        grid.SetReadOnly(0, 3)
        grid.SetCellValue(3, 3, 'green on grey')
        grid.SetCellTextColour(3, 3, wx.GREEN)
        grid.SetCellBackgroundColour(3, 3, wx.LIGHT_GREY)
        grid.SetColFormatFloat(5, 6, 2)
        grid.SetCellValue(0, 6, '3.1415')
        vbox = wx.BoxSizer(wx.VERTICAL)

        vbox.Add(wx.TextCtrl(scrwin), 0, 0)
        vbox.Add(wx.TextCtrl(scrwin), 0, 0)
        vbox.Add(wx.TextCtrl(scrwin), 0, 0)
        
        vbox.Add(grid, 1, wx.EXPAND)
        scrwin.SetSizer(vbox)

        self.Show()

app = wx.App()
app.TopWindow = Gui(None)
app.MainLoop()

If the focus is on one of the three text controls (for example by clicking on it), the scroll wheel on the mouse works (the whole scrollwin works) but if the focus is on the grid (for example by clicking on one of its cells) then the scroll wheel on the mouse does nothing.

Does the program behave differently for you? I am on linux, and the latest wxPython compiled from source.

try this :sweat_smile:

import wx
import wx.grid

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent)
        self.SetTitle('grid on scrollable..')

        self.scrwin = wx.ScrolledWindow(self)                # scrolled
        self.scrwin.SetScrollRate(1, 60)
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(self.scrwin, 1, wx.EXPAND)
        self.SetSizer(vbox)

        grid = wx.grid.Grid(self.scrwin)                         # grid
        grid.Bind(wx.EVT_MOUSEWHEEL, self.evt_mouse)
        grid.CreateGrid(100, 10)
        grid.SetRowSize(0, 60)
        grid.SetColSize(0, 120)
        grid.SetCellValue(0, 0, 'wxGrid is good')
        grid.SetCellValue(0, 3, 'This is read.only')
        grid.SetReadOnly(0, 3)
        grid.SetCellValue(3, 3, 'green on grey')
        grid.SetCellTextColour(3, 3, wx.GREEN)
        grid.SetCellBackgroundColour(3, 3, wx.LIGHT_GREY)
        grid.SetColFormatFloat(5, 6, 2)
        grid.SetCellValue(0, 6, '3.1415')
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(grid, 1, wx.EXPAND)
        self.scrwin.SetSizer(vbox)

        self.Show()

    def evt_mouse(self, evt):
        if evt.GetWheelRotation() > 0:
            self.scrwin.ScrollLines(-1 * evt.GetLinesPerAction())
        else:
            self.scrwin.ScrollLines(evt.GetLinesPerAction())
        evt.Skip()

app = wx.App()
app.TopWindow = Gui(None)
app.MainLoop()

Thank you for the time you are putting into this, Georg! Yes, implementing scrolling myself is the solution I came up with (see my first post).

Eventually I realized I also needed to deal with horizontal scrolling and variable speed and the algorithm inside evt_mouse got slightly more complicated (see my first post).

My question is how to avoid implementing the scrolling algorithm at all by instead forwarding the scrolling event received by evt_mouse to scrollwin. I feel we should not have to re-implement the scrolling algorithm since scrollwin already has an implementation and knows how to process scrolling events.

Hi Jorge,

This works for me:

        def evt_mouse(evt):
            evt.ResumePropagation(1)
            evt.Skip()
        grid.Bind(wx.EVT_MOUSEWHEEL, evt_mouse)

But only vertical scroll.

I’m not sure if ScrolledWindow can handle horizontal scrolling. :roll_eyes:

Hello Kazuya. Thank you. Very interesting. That does not work for me. I had tried it (per a post above) in my larger code and I just tried it again in the small sample Georg posted.

I am on Linux. What platform did you try it on?

I tested with the last snapshot:
wxPython 4.2.1a1.dev5545+a3b6cfec msw (phoenix) wxWidgets 3.2.1,
Python 3.10.4 on Windows 10.

EDIT
It also works with wx version 4.1.1 msw (phoenix) wxWidgets 3.1.5 / Python 3.8.6 on Windows 10.

I believe this depends on the platform. @da-dada If you have time, could you confirm that this evt.ResumePropagation(1) works on your platform (windows)?

EDIT2 The test code:

import wx
import wx.grid

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent)
        self.SetTitle('grid on scrollable..')

        scrwin = wx.ScrolledWindow(self)                # scrolled
        scrwin.SetScrollRate(1, 60)
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(scrwin, 1, wx.EXPAND)
        self.SetSizer(vbox)

        grid = wx.grid.Grid(scrwin)                         # grid
        grid.Bind(wx.EVT_MOUSEWHEEL, self.evt_mouse)
        grid.CreateGrid(100, 10)
        grid.SetRowSize(0, 60)
        grid.SetColSize(0, 120)
        grid.SetCellValue(0, 0, 'wxGrid is good')
        grid.SetCellValue(0, 3, 'This is read.only')
        grid.SetReadOnly(0, 3)
        grid.SetCellValue(3, 3, 'green on grey')
        grid.SetCellTextColour(3, 3, wx.GREEN)
        grid.SetCellBackgroundColour(3, 3, wx.LIGHT_GREY)
        grid.SetColFormatFloat(5, 6, 2)
        grid.SetCellValue(0, 6, '3.1415')
        vbox = wx.BoxSizer(wx.VERTICAL)

        vbox.Add(wx.TextCtrl(scrwin), 0, 0)
        vbox.Add(wx.TextCtrl(scrwin), 0, 0)
        vbox.Add(wx.TextCtrl(scrwin), 0, 0)
        
        vbox.Add(grid, 1, wx.EXPAND)
        scrwin.SetSizer(vbox)

        self.Show()

    def evt_mouse(self, evt):
        evt.ResumePropagation(1)
        evt.Skip()

app = wx.App()
app.TopWindow = Gui(None)
app.MainLoop()

it works on windows 10 & 11 (and actually value-added :smiling_face_with_three_hearts:)

windows 10 can by tilting the wheel evt.GetWheelAxis() but on windows 11 :stuck_out_tongue_closed_eyes: (is difficult to handle anyway)

my feeling too :cowboy_hat_face: you just have to indicate somehow which orientation the scroll should be (here on grid it’s horizontal & on horizontal scrollbar it’s vertical, easily remembered I think :rofl:)

    def evt_mouse(self, evt):
        if evt.GetWheelRotation() > 0:
            self.scrwin.Scroll(self.scrwin.GetViewStart()[0] -\
            self.scrwin.GetScrollLines(wx.VERTICAL), -1)
        else:
            self.scrwin.Scroll(self.scrwin.GetViewStart()[0] +\
            self.scrwin.GetScrollLines(wx.VERTICAL), -1)
        evt.Skip()

Thank you everyone for helping with this. I opened a bug report on wxWidgets reporting the discrepancy between platforms: https://github.com/wxWidgets/wxWidgets/issues/23221

By the way the version of the custom handler that I have been using to work around the limitation on GTK also measures the change of rotation of the wheel to scroll more or less. This requires persisting the previous amount of rotation. The complete code is is in the first message of this thread.

frankly speaking I still don’t fathom your objective and the limitation having to work around (maybe Windows is to easy) :see_no_evil: :hear_no_evil: :speak_no_evil:

since wx.grid.Grid is derived from (some sort of) Scrolled the extra Scrolled looks pretty superfluous (and ruins the header altogether :face_with_raised_eyebrow:)
I’m not a ‘gridder’ but I think scrolling a grid is just by the scroll wheel & for changing the orientation a modifier is used (here Ctrl) :sunglasses:

import wx
import wx.grid

class Gui(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)

        grid = wx.grid.Grid(self, -1)
        grid.CreateGrid(100, 10)
        grid.SetRowSize(0, 60)
        grid.SetColSize(0, 120)
        grid.SetCellValue(0, 0, 'wxGrid is good')
        grid.SetCellValue(0, 3, 'This is read.only')
        grid.SetReadOnly(0, 3)
        grid.SetCellValue(3, 3, 'green on grey')
        grid.SetCellTextColour(3, 3, wx.GREEN)
        grid.SetCellBackgroundColour(3, 3, wx.LIGHT_GREY)
        grid.SetColFormatFloat(5, 6, 2)
        grid.SetCellValue(0, 6, '3.1415')
        grid.Bind(wx.EVT_MOUSEWHEEL, self.evt_mouse)
        self.grid = grid

        self.Show()

    def evt_mouse(self, evt):
        if wx.GetMouseState().cmdDown:
            if evt.GetWheelRotation() > 0:
                self.grid.Scroll(self.grid.GetViewStart()[0] -\
                self.grid.GetScrollThumb(wx.VERTICAL), -1)
            else:
                self.grid.Scroll(self.grid.GetViewStart()[0] +\
                self.grid.GetScrollThumb(wx.VERTICAL), -1)
        else:
            evt.Skip()

app = wx.App(0)
frame = Gui(None)
app.MainLoop()

Thank you very much for the good news, Georg!

TBH I feel that the tilting-wheel interface was terrible, S-wheel (or Ctrl-wheel) is acceptable, and the touchpad interface would be great!

I think that @jmoraleda’s problems appears when some controls and grid are laid on the same panel.
So one workaround, albeit an easy one, is to separate them.

frankly speaking I still don’t fathom your objective and the limitation having to work around (maybe Windows is to easy)

I am trying to avoid having to reimplement the scroll logic in the grid event handler. In particular I am trying to replace

def _on_mouse_wheel(self, event):
        ''' Event listener on mouse scroll when the grid is embedded inside a Scrolled Window. It
        makes the scroll window scroll, instead of the grid itself '''

        delta = event.GetWheelDelta()
        rotation = event.GetWheelRotation()
        linesPer = event.GetLinesPerAction()

        wheelAxis = 0 if event.GetWheelAxis() == wx.MOUSE_WHEEL_HORIZONTAL else 1

        mws = self._mouseWheelScroll[wheelAxis]
        mws = mws + rotation
        lines = mws / delta
        mws = mws - lines * delta
        self._mouseWheelScroll[wheelAxis] = mws
        if lines != 0:
            lines = lines * linesPer
            viewStart = self.scrollWin.GetViewStart()
            if wheelAxis == 0:  # notice the different sign in horizontal and vertical logic
                self.scrollWin.Scroll(int(viewStart[wheelAxis] + lines), -1)
            else:
                self.scrollWin.Scroll(-1, int(viewStart[wheelAxis] - lines))

with

def _on_mouse_wheel(self, event):
        ''' Event listener on mouse scroll when the grid is embedded inside a Scrolled Window. It
        makes the scroll window scroll, instead of the grid itself '''
       event.ResumePropagation(wx.EVENT_PROPAGATE_MAX)
       event.Skip()

or another equally short way of forwarding the event from the grid to the scrolled window that works on all platforms.

I am trying to do this as a matter of principle, not for extra functionality.

Indeed! The touchpad interface (at least on GTK) can scroll both horizontally and vertically both in the default handler and with my custom logic.