I am on
- wxPython 4.2.3,
- macbook pro m1,
- running Sequoia 15.5 and
- Python version 3.12.9;
I ran into this problem where I used to use a wx.ScrolledWindow as my App top window. I changed to a ShapeCanvas window but it does not scroll when using my Mac mouse middle button scroll (aka virtual wheel)
The ShapeCanvas is a subclass of ScrolledWindow. The following sample program demonstrates the problem
from wx import App
from wx import DEFAULT_FRAME_STYLE
from wx import FRAME_FLOAT_ON_PARENT
from wx import Frame
from wx import ScrolledWindow
from wx import Size
from wx.lib.ogl import Diagram
from wx.lib.ogl import OGLInitialize
from wx.lib.ogl import ShapeCanvas
NOMINAL_SIZE: Size = Size(400, 475)
DEFAULT_WIDTH: int = 3000
A4_FACTOR: float = 1.41
PIXELS_PER_UNIT_X: int = 20
PIXELS_PER_UNIT_Y: int = 20
class NoScrollShapeCanvas(App):
def __init__(self):
super().__init__(redirect=False) # This calls OnInit()
self._wxFrame = None
self._shapeCanvas = None
def OnInit(self):
OGLInitialize()
self._wxFrame = Frame(parent=None, title='Bogus', size=NOMINAL_SIZE, style=DEFAULT_FRAME_STYLE | FRAME_FLOAT_ON_PARENT)
#
# When shapedCanvas is a Scrolled Window, the middle button (wheel) scrolls the window
#
self._shapeCanvas = ScrolledWindow(self._wxFrame, size=NOMINAL_SIZE)
#
# Comment the above
# Remove the comment out for the following 2 lines and the 'ScrolledWindow'
# does not scroll on a middle button (even though it is a 'ScrolledWindow')
#
# self._shapeCanvas = ShapeCanvas(self._wxFrame, size=NOMINAL_SIZE)
# self._shapeCanvas.SetDiagram(diag=Diagram()) # incestuous behavior here
self._setupScrolling(self._shapeCanvas)
self.SetTopWindow(self._wxFrame)
self._wxFrame.SetAutoLayout(True)
self._wxFrame.Show(True)
self._wxFrame.SetMinSize(NOMINAL_SIZE)
return True
def _setupScrolling(self, shapeCanvas):
self.maxWidth: int = DEFAULT_WIDTH
self.maxHeight: int = int(self.maxWidth / A4_FACTOR) # 1.41 is for A4 support
nbrUnitsX: int = int(self.maxWidth / PIXELS_PER_UNIT_X)
nbrUnitsY: int = int(self.maxHeight / PIXELS_PER_UNIT_Y)
initPosX: int = 0
initPosY: int = 0
shapeCanvas.SetScrollbars(PIXELS_PER_UNIT_X, PIXELS_PER_UNIT_Y, nbrUnitsX, nbrUnitsY, initPosX, initPosY, False)
if __name__ == '__main__':
testApp: NoScrollShapeCanvas = NoScrollShapeCanvas()
testApp.MainLoop()
Looking in canvas.py
at the source code for the ShapeCanvas
class, I see that it binds EVT_MOUSE_EVENTS
to its OnMouseEvent()
method.
This causes OnMouseEvent()
to receive all mouse events. However, that handler doesn’t call evt.GetWheelRotation()
so presumably all mouse wheel events are ignored.
Perhaps you could create a subclass of ShapeCanvas
and override its OnMouseEvent()
method to do the scrolling when evt.WheelRotation != 0
, otherwise it should call the base class method.
However, I don’t know if that would interfere with the ShapeCanvas
’ normal behaviour.
Below is some modified code which I used to investigate the question. It needs some more code to actually do the scrolling.
import wx
from wx import App
from wx import DEFAULT_FRAME_STYLE
from wx import FRAME_FLOAT_ON_PARENT
from wx import Frame
from wx import ScrolledWindow
from wx import Size
from wx.lib.ogl import Diagram
from wx.lib.ogl import OGLInitialize
from wx.lib.ogl import ShapeCanvas
NOMINAL_SIZE: Size = Size(400, 475)
DEFAULT_WIDTH: int = 3000
A4_FACTOR: float = 1.41
PIXELS_PER_UNIT_X: int = 20
PIXELS_PER_UNIT_Y: int = 20
class MyShapeCanvas(ShapeCanvas):
def OnMouseEvent(self, evt):
"""The mouse event handler."""
if evt.WheelRotation == 0:
ShapeCanvas.OnMouseEvent(self, evt)
else:
# Get current scroll position
scroll_pos_x = self.GetScrollPos(wx.HORIZONTAL)
scroll_pos_y = self.GetScrollPos(wx.VERTICAL)
# On MaxBook Pro, WheelAxis is 0 for
# vertical axis and 1 for horizontal axis
print(scroll_pos_x, scroll_pos_y, evt.WheelAxis, evt.WheelRotation)
# Calculate x, y - the position to scroll to *in scroll units*
# and pass to self.Scroll(x, y) ?
pass
class NoScrollShapeCanvas(App):
def __init__(self):
super().__init__(redirect=False) # This calls OnInit()
self._wxFrame = None
self._shapeCanvas = None
def OnInit(self):
OGLInitialize()
self._wxFrame = Frame(parent=None, title='Bogus', size=NOMINAL_SIZE, style=DEFAULT_FRAME_STYLE | FRAME_FLOAT_ON_PARENT)
#
# When shapedCanvas is a Scrolled Window, the middle button (wheel) scrolls the window
#
# self._shapeCanvas = ScrolledWindow(self._wxFrame, size=NOMINAL_SIZE)
#
# Comment the above
# Remove the comment out for the following 2 lines and the 'ScrolledWindow'
# does not scroll on a middle button (even though it is a 'ScrolledWindow')
#
self._shapeCanvas = MyShapeCanvas(self._wxFrame, size=NOMINAL_SIZE)
self._shapeCanvas.SetDiagram(diag=Diagram()) # incestuous behavior here
self._setupScrolling(self._shapeCanvas)
self.SetTopWindow(self._wxFrame)
self._wxFrame.SetAutoLayout(True)
self._wxFrame.Show(True)
self._wxFrame.SetMinSize(NOMINAL_SIZE)
return True
def _setupScrolling(self, shapeCanvas):
self.maxWidth: int = DEFAULT_WIDTH
self.maxHeight: int = int(self.maxWidth / A4_FACTOR) # 1.41 is for A4 support
nbrUnitsX: int = int(self.maxWidth / PIXELS_PER_UNIT_X)
nbrUnitsY: int = int(self.maxHeight / PIXELS_PER_UNIT_Y)
initPosX: int = 0
initPosY: int = 0
shapeCanvas.SetScrollbars(PIXELS_PER_UNIT_X, PIXELS_PER_UNIT_Y, nbrUnitsX, nbrUnitsY, initPosX, initPosY, False)
if __name__ == '__main__':
testApp: NoScrollShapeCanvas = NoScrollShapeCanvas()
testApp.MainLoop()
I was just working on a similar idea in that I already have a subclass of ShapeCanvas and was going to insert additionally behavior in the OnMouseEvent handler. Thanks for following up with me on this.
Here is the final demonstration version of the scrolling shape canvas. It does not scroll super smooth in this code. But, when I use the same code in my app I get smoother scrolling. Do not know why.
from enum import Enum
from wx import DEFAULT_FRAME_STYLE
from wx import FRAME_FLOAT_ON_PARENT
from wx import HORIZONTAL
from wx import VERTICAL
from wx import App
from wx import MouseEvent
from wx import Point
from wx import Size
from wx.lib.ogl import Diagram
from wx.lib.ogl import OGLInitialize
from wx.lib.ogl import ShapeCanvas
from wx.lib.sized_controls import SizedFrame
from wx.lib.sized_controls import SizedPanel
NOMINAL_SIZE: Size = Size(1024, 720)
DEFAULT_WIDTH: int = 16000
A4_FACTOR: float = 1.41
PIXELS_PER_UNIT_X: int = 20
PIXELS_PER_UNIT_Y: int = 10
class WheelAxis(Enum):
WHEEL_AXIS_VERTICAL = 0
WHEEL_AXIS_HORIZONTAL = 1
@classmethod
def toEnum(cls, value: int) -> 'WheelAxis':
if value == 0:
return WheelAxis.WHEEL_AXIS_VERTICAL
elif value == 1:
return WheelAxis.WHEEL_AXIS_HORIZONTAL
else:
assert False, f'Unknown wheel axis: {value=}'
class ScrollingShapeCanvas(ShapeCanvas):
def OnMouseEvent(self, mouseEvent: MouseEvent):
rotation: int = mouseEvent.GetWheelRotation()
wheelAxis: WheelAxis = WheelAxis.toEnum(value=mouseEvent.GetWheelAxis())
inverted: bool = mouseEvent.IsWheelInverted()
if not inverted:
rotation = -rotation
yScrollPosition: int = self.GetScrollPos(VERTICAL)
xScrollPosition: int = self.GetScrollPos(HORIZONTAL)
if wheelAxis == WheelAxis.WHEEL_AXIS_VERTICAL:
yScrollPosition += rotation
else:
xScrollPosition += rotation
self.Scroll(Point(x=xScrollPosition, y=yScrollPosition))
super().OnMouseEvent(evt=mouseEvent)
class ScrollingShapeCanvasApp(App):
def __init__(self):
super().__init__(redirect=False) # This calls OnInit()
self._wxFrame = None
self._shapeCanvas = None
def OnInit(self):
OGLInitialize()
self._wxFrame = SizedFrame(parent=None, title='Bogus', size=NOMINAL_SIZE, style=DEFAULT_FRAME_STYLE | FRAME_FLOAT_ON_PARENT)
sizedPanel: SizedPanel = self._wxFrame.GetContentsPane()
sizedPanel.SetSizerProps(expand=True, proportion=1)
self._shapeCanvas = ScrollingShapeCanvas(sizedPanel)
self._shapeCanvas.SetDiagram(diag=Diagram()) # incestuous behavior here
self._setupScrolling(self._shapeCanvas)
# noinspection PyUnresolvedReferences
self._shapeCanvas.SetSizerProps(expand=True, proportion=1)
self.SetTopWindow(self._wxFrame)
self._wxFrame.SetAutoLayout(True)
self._wxFrame.Show(True)
self._wxFrame.SetMinSize(NOMINAL_SIZE)
return True
def _setupScrolling(self, shapeCanvas):
self.maxWidth: int = DEFAULT_WIDTH
self.maxHeight: int = round(self.maxWidth / A4_FACTOR) # 1.41 is for A4 support
nbrUnitsX: int = self.maxWidth // PIXELS_PER_UNIT_X
nbrUnitsY: int = self.maxHeight // PIXELS_PER_UNIT_Y
initPosX: int = 0
initPosY: int = 0
shapeCanvas.SetScrollbars(PIXELS_PER_UNIT_X, PIXELS_PER_UNIT_Y, nbrUnitsX, nbrUnitsY, initPosX, initPosY, False)
if __name__ == '__main__':
testApp: ScrollingShapeCanvasApp = ScrollingShapeCanvasApp()
testApp.MainLoop()