ThirdProphecy
ThirdProphecy

Reputation: 31

Scrolled Panel MouseWheel Scroll

I'm hoping you can help me out :) I'm banging my head against the wall, I'm not able to get a Scrolled Panel to scroll up and down when I'm using the mousewheel. It works when I click it or drag it though.

What I'm doing is populating a panel with a bunch of textctrls and using the scrolled panel to scroll through them. I'm trying to set the focus of the panel when mouse over and even tried when clicked but nothing happens when I mousewheel to scroll up and down on the panel.

I've looked at the wxPython example for ScrolledPanels but I'm not able to see why theirs is working while mine is not.

Any help given would be amazing!

EDIT: I've discovered that when a textctrl is selected it activates the scrolling - but not when TE_MULTILINE is used. The Expando Text Ctrls use the TE_MULTILINE style and so I'm able to acvitate the scrollbars only when clicking single line text ctrls.

How do I get control of the scrollbar to use the mousewheel when clicking a multi_line text control?

import wx
import random
import wx.lib.platebtn as pbtn
import wx.lib.agw.gradientbutton as gbtn
import wx.lib.scrolledpanel as spanel
import requests
import json
import sys
import wx.lib.mixins.listctrl  as  listmix
import wx.lib.expando as etc


ticketInfo = """
*************************\n
TEST TICKET\n
*************************\n
BLAH BLAH BLAH BLAH\n
BLAH BLAH BLAH BLAH\n
BLAH BLAH BLAH BLAH\n
BLAH BLAH BLAH BLAH\n
BLAH BLAH BLAH BLAH\n
"""


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, title="Learning List Controls")
        self.SetTopWindow(self.frame)
        self.frame.Show()

        return True
class TicketViewer(wx.lib.scrolledpanel.ScrolledPanel):
    def __init__(self, parent):
        super(TicketViewer, self).__init__(parent, name="panel", style=wx.TE_AUTO_SCROLL)

        #Attributes
        vSizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(vSizer)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.PopulateTicketMessages(ticketInfo)
        self.SetSizer(vSizer)
        self.Layout()
        self.SetupScrolling()
        panelID = self.GetId()
    def PopulateTicketMessages(self, ticketInfo):
        msgBox = etc.ExpandoTextCtrl(self,
                         id=-1,
                         value=ticketInfo,
                         style=wx.TE_READONLY|wx.TE_WORDWRAP)
        sizer = self.GetSizer()
        sizer.Add(msgBox)
        msgBox.Bind(wx.EVT_LEFT_DOWN, self.OnMouseOver)
    def OnMouseOver(self, event):
        panel = wx.FindWindowByName("panel")
        panel.SetFocus()
        print(panel)
class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MyFrame, self).__init__(*args, **kwargs)

        #Attribues
        self.tktViewer = TicketViewer(self)


        frameSizer = wx.BoxSizer(wx.HORIZONTAL)
        frameSizer.Add(self.tktViewer, 1, wx.EXPAND)
        self.SetSizer(frameSizer)

        #self.SetSizerAndFit(frameSizer)
        self.CreateStatusBar()
        self.SetInitialSize()

    def OnMouseOver(self, event):
        panel = wx.FindWindowById(tktViewerID)
        panel.SetFocus()
        print(self)

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

Upvotes: 1

Views: 683

Answers (1)

sesenmaister
sesenmaister

Reputation: 669

The reason for this is that ScrolledPanel derives from wx.Panel, wich has code that everytime it receives the focus will try to reset the focus to its first child. Furthermore in Windows the wheel events are only sent to the widget that has the focus. (Useful thread with Robin Dunn remarks)

The solution when this happens:

(1)Where to catch the event? At app level. As said in the wxpython docs about the order of event table searching:How Events are Processed

Finally, i.e., if the event is still not processed, the App object itself (which derives from EvtHandler) gets a last chance to process it.

So wx.GetApp().Bind(wx.EVT_MOUSEWHEEL, your_handler)

(2)And how to manually process the event? Taking the idea from this post (written by Doug Anderson according to comments inside pygcodeviewer/pygcodeviewer.py)

it's needed to call GetHandler().ProcessEvent(event) on the wxWindow of our interest (the scrolled panel)

..And considering we are catching at the app level: we must ensure the event is from a child of our wxWindow [the scrolled panel] or if not, Skip() it and let it go.

So, here is a solution as a mixin class:

class TeMultilineScrollAwareMixin(object):
    """ Mixin class for wx.lib.scrolledpanel.ScrolledPanel so it's
    aware of scroll events from childs with the TE_MULTILINE style
    """

    def __init__(self, *args, **kwargs):
        self._pLastEvtObj = None        
        wx.GetApp().Bind(wx.EVT_MOUSEWHEEL, self.OnUnhandledMouseWheel)

    def OnUnhandledMouseWheel(self, event):
        # stops the same event object being processed repeatedly
        if self._pLastEvtObj == event.GetEventObject():
            return False

        self._pLastEvtObj = event.GetEventObject()

        def is_child_evt():
            parent = event.GetEventObject().GetParent()
            while parent:
                if parent == self: 
                    return True
                parent = parent.GetParent() 
            return False

        # as we are listening at app level, must ensure that event is from a widget of us
        if is_child_evt():
            print 'event from %s' % event.GetEventObject().__class__.__name__
            # if the event was not handled this window will handle it,
            # which is why we need the protection code at the beginning
            # of this method
            self.GetEventHandler().ProcessEvent(event)            
        else:
            # it's not of our interest, let it go
            event.Skip()

        self._pLastEvtObj = None
        return

remember to init the mixin after adding it and you're done:

class TicketViewer(wx.lib.scrolledpanel.ScrolledPanel, TeMultilineScrollAwareMixin):
    def __init__(self, parent):
        ... your init code ...
        TeMultilineScrollAwareMixin.__init__(self)

Upvotes: 1

Related Questions