Always open wxPython GUI with scroll bar selected - python

I have several GUIs which open progressively that are all experiencing the same issue. When I open them, the first object which is selected is a TextCtrl object. These GUIs are rather large and have scroll bars. Since a TextCtrl object is selected, scrolling with the mouse wheel does nothing and makes it appear as if the scroll bars are broken. To demonstrate this, I made the following code:
import wx
class Tester(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Window", size=(500, 500))
self.panel = wx.ScrolledWindow(self,wx.ID_ANY)
self.panel.SetScrollbars(30,30,600,400)
textBox = wx.TextCtrl(panel, -1, "", size=(200, 150), style=wx.TE_MULTILINE|wx.TE_LEFT)
textStuff = wx.StaticText(panel, -1, "A\nbunch\nof\nlines\nto\nmake\nthis\nlong\nenough\nto\nhave\nscroll\nbars\n\n\n\n\n\n\n\n\n\nIts lonely down here\n\n\n\n:(")
lonelyBtn = wx.Button(panel, -1, "So Lonely")
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(textBox, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, border=10)
vbox.Add(textStuff, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
vbox.Add(lonelyBtn, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
panel.SetSizer(vbox)
panel.Layout()
app = wx.PySimpleApp()
Tester().Show()
app.MainLoop()
When you run this and try to scroll down, you'll notice you cannot scroll with the mouse wheel.
So far, this all makes sense. Here's where it gets a little weird. If the selected object is a Button, using the mouse wheel engages the scroll bars. You can test this by pressing the button, then using the mouse wheel. Also, clicking on the panel, or even the scroll bar itself, doesn't allow the mouse wheel to work.
What I'm looking for is a way to make sure that if there are scroll bars, they can be used with the mouse wheel upon displaying the GUI (they are selected by default). I can accept that the mouse wheel will not function once a user clicks into the text control.
Additionally, if you have an explanation for why the mouse wheel works for buttons and not text controls, I'd love to hear it
EDIT: I know I can add a listener (thanks to Mr. Joran Beasley), but this means that the scroll bars within a multi-line text control can never be used with the mouse wheel. The ideal solution (which I'm not sure is possible), is to have clicking on anywhere outside the text control (panel or scroll bar) allows the mouse wheel to scroll the panel. Is this possible?
Additionally, I've switched over to using ScrolledWindow instead of ScrolledPanel
EDIT 2: The fix was to use the following:
self.panel.Bind(wx.EVT_MOTION, self.onMouseMove)
def onMouseMove(self, event):
self.panel.SetFocusIgnoringChildren()
EDIT 3: The actual fix was to do something a little tricky. Using the code below I bound only multiline text controls to EVT_ENTER_WINDOW and EVT_LEAVE_WINDOW as well as binding every item (and the panel itself) to EVT_MOUSEWHEEL. Then a logical self.inMLTxtCtrl tracks if the mouse if over any of the multiline text controls
self.panel.Bind(wx.EVT_MOUSEWHEEL, self.onWheel)
for sizerItem in self.panel.GetSizer().GetChildren():
try:
if sizerItem.GetWindow().IsMultiLine():
sizerItem.GetWindow().Bind(wx.EVT_ENTER_WINDOW, self.onMouseEnter)
sizerItem.GetWindow().Bind(wx.EVT_LEAVE_WINDOW, self.onMouseLeave)
sizerItem.GetWindow().Bind(wx.EVT_MOUSEWHEEL, self.onWheel)
except:
sizerItem.GetWindow().Bind(wx.EVT_MOUSEWHEEL, self.onWheel)
Then a logical self.inMLTxtCtrl tracks if the mouse if over any of the multiline text controls as shown below.
def onMouseEnter(self, event):
print "entering"
self.inMLTxtCtrl = True
def onMouseLeave(self, event):
print "leaving"
self.inMLTxtCtrl = False
Finally, the onWheel() function uses this logical flag to determine where to scroll. If the mouse is in a multiline text control when the scroll wheel is turned, it attempts to scroll in that text control. Othewise, the SetFocusIgnoringChildren() function is called and the panel is scrolled. Since the panel and text control use different scrolling methods, a try...except is needed.
def onWheel(self, event):
if self.inMLTxtCtrl:
print "in", event.GetWheelRotation()
else:
print "out", event.GetWheelRotation()
self.panel.SetFocusIgnoringChildren()
try:
currScroll = self.panel.FindFocus().GetViewStart()
newScroll = (currScroll[0],currScroll[1]- event.GetWheelRotation()/60)
self.panel.FindFocus().Scroll(*newScroll)
except:
self.panel.FindFocus().ScrollLines(event.GetWheelRotation()/-60)

class Tester(wx.Frame):
def OnTxtScroll(self,e):
currScroll = self.panel.GetViewStart()
newScroll = (currScroll[0],currScroll[1]- e.GetWheelRotation()/120)
self.panel.Scroll(*newScroll)
def __init__(self):
....
#your code
....
self.panel = panel
textBox.Bind(wx.EVT_MOUSEWHEEL,self.OnTxtScroll)
after your clarification ... I think that this would work (its a bit hacky and doesnt do exactly what you describe ... but it might work)
def OnTxtScroll(self,e):
print dir(e)
target = e.GetEventObject()
p1 = target.GetScrollPos(wx.VERTICAL)
e.Skip()
self.Update()
def updateScroll(p1,target,scroll_amt):
p2 = target.GetScrollPos(wx.VERTICAL)
if p1 ==p2:#scroll did not effect target object so lets scroll our main panel
currScroll = self.panel.GetViewStart()
newScroll = (currScroll[0],currScroll[1]- scroll_amt)
self.panel.Scroll(*newScroll)
wx.CallAfter(updateScroll,p1,target,e.GetWheelRotation()/120)

Related

How to sound a bell when the user clicks outside a modal window?

The situation is simple. I have a main window with a Help - About menu.
When this menu item is clicked, a modal window is opened (let's say it's an About-window).
With self.grab_set() I disabled the main-window (although the modal window does flicker when you click the main title bar).
So far, so good.
Here is the question: I really like to sound a bell when the user clicks outside the modal window on the main window.
This is what I could find about grab_set(), really not that much:
[effbot] ...a method called grab_set, which makes sure that no mouse or keyboard
events are sent to the wrong window.
[effbot] Routes all events for this application to this widget.
[kite.com] A grab directs all events to this and descendant widgets in the application.
[google books] grab_set() ensures that all of the application's events are sent to w until a corresponding call is made to grab_release ([Me:] or till the window is destroyed?)
I'm not quite sure how to understand this: does it mean you can handle an event on the main window within the modal window (like sounding my bell)?
So I tried things like:
self.bind('<Button-1>', self.bell) Exception in Tkinter callback: _tkinter.TclError: bad window path name
parent.bind('<Button-1>', self.bell) Nothing happens
So, how to sound a bell like when clicked outside the modal window on the main window, like in so many other applications?
Derived questions:
Is it still possible to cature events from the main window after using
grab_set for the modal window?
Is there a way to prevent the flickering?
I really like to understand this mysterious grab_set() method.
Stripped code:
import tkinter as tk
class About(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.geometry('200x150')
#--- OK button
btn_ok = tk.Button(self, text='OK', command=self.destroy) # destroy with OK
btn_ok.pack(side=tk.TOP)
btn_ok.focus() # destroy with spacebar
#--- Make window modal
self.grab_set()
# self.wait_window() # is this necessary in this case?
# self.bind('<Button-1>', self.bell) ??? The question
class MenuBar(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self)
helpmenu = tk.Menu(self, tearoff=0)
helpmenu.add_command(label='About', command=lambda: About(parent))
self.add_cascade(label='Help', menu=helpmenu)
class MainApp():
def __init__(self, parent):
parent.configure(background='#000000')
parent.geometry('800x600')
menubar = MenuBar(parent)
parent.configure(menu=menubar)
if __name__ == '__main__':
root = tk.Tk()
MainApp(root)
root.mainloop()
When you set a grab, all button clicks will go to the window with the grab. You capture them the way you capture any other event. In the case of a button click you do that by binding a function to <1>.
It's important to know that a binding on a root window or a Toplevel window will apply to all widgets in that window. For example, binding to self in your code will fire even when you click on the "Ok" button. Therefore, the callback should probably only do work when the widget associated with the event is the same as the toplevel.
Example:
class About(tk.Toplevel):
def __init__(self, parent):
...
self.bind("<1>", self.capture_click)
...
def capture_click(self, event):
if event.widget == self:
<your logic here>
In the case of wanting to know if the user clicked outside the window, you can use the coordinates of the event object to compare against the window to see if the click is inside or outside.
def on_click(self, event):
if event.widget == self:
if (event.x < 0 or event.x > self.winfo_width() or
event.y < 0 or event.y > self.winfo_height()):
self.bell()
I found a second solution. Though my question was explicitly about using grab_set(), this method does the same for me: making the window as modal as possible and sound a bell.
Instead of using self.grab(), you can also disable the parent window:
parent.attributes('-disabled', True)
Of course it needs to be enabled again when the OK button is clicked (and when the windows is closed with the [X] close control. However, my original About-window has no window decoration). The command for the OK-button becomes:
btn_ok = tk.Button(self, text='OK', command=lambda: self.closeme(parent))
...which calls the closeme function:
def closeme(self, parent):
parent.attributes('-disabled', False)
self.destroy()
The bell sounds automatically when clicking a disabled window.
Method 1: Keeps you in full control of the main window but does not 'freeze' the main window: you can still move it around.
Method 2: Completely freezes the main window, but if it happens to be (partially) covered by another window (not of this application), then you can only bring back to top using Alt+Tab (windows).
I'm sure I will use both techniques in the future depending on my needs.

Why does the caret disappear from a TextCtrl widget after a focus event?

I'm writing a calculator application on MS Windows 8.1 using wxPython-Phoenix in which I'd like the calculation to be performed as soon as the user enters a value into one of the parameter fields. To achieve that I'm using wx.EVT_KILL_FOCUS generated by any parameter field to generate a command event which triggers the calculation method. Everything works fine but the appearance of the caret in the parameter fields (implemented by TextCtrl widgets).
After setting the focus either by the Tab key or the mouse on a certain field and moving it (again by the Tab key or the mouse) to a different field - the caret is gone from the TextCtrl widget never to return! when you call the GetCaret() method on that TextCtrl widget the return value is None.
See attached example. The widget still accepts input and displays it but without a caret.
How can I restore the caret in the right position? or not lose it in the first place?
I've tried setting a new caret in the TextCtrl but it does not follow the text input. Since the application is ment to be intensely interactive, I want the triggering event to be the lost focus and not a button (to minimize the number of clicks)
import wx
class MyFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super(MyFrame, self).__init__(*args, **kwargs)
self.InitUI()
def InitUI(self):
# setting up the Input panel
panel = wx.Panel(self)
self.lbl1 = wx.StaticText(panel,wx.ID_ANY,'Some Text:')
self.txt1 = wx.TextCtrl(panel,wx.ID_ANY,style=wx.TE_RIGHT)
self.lbl2 = wx.StaticText(panel,wx.ID_ANY,'Some Other Text:')
self.txt2 = wx.TextCtrl(panel,wx.ID_ANY,style=wx.TE_RIGHT)
infgsz = wx.FlexGridSizer(2,2,15,15)
infgsz.AddMany([(self.lbl1,0,wx.ALIGN_LEFT),\
(self.txt1,0,wx.ALIGN_LEFT),\
(self.lbl2,0,wx.ALIGN_LEFT),\
(self.txt2,0,wx.ALIGN_LEFT)])
self.txt1.Bind(wx.EVT_KILL_FOCUS,self.OnInput)
self.txt2.Bind(wx.EVT_KILL_FOCUS,self.OnInput)
box = wx.BoxSizer(wx.HORIZONTAL)
box.Add(infgsz, flag= wx.EXPAND | wx.ALL, border=15)
panel.SetSizer(box)
self.SetSize((280, 140))
self.SetTitle('TextCtrl Demo')
self.Centre()
def OnInput(self, e):
if e.GetId() == self.txt1.GetId():
self.lbl2.SetForegroundColour(wx.ColourDatabase().Find('RED'))
self.lbl1.SetForegroundColour(wx.ColourDatabase().Find('BLACK'))
else:
self.lbl1.SetForegroundColour(wx.ColourDatabase().Find('BLUE'))
self.lbl2.SetForegroundColour(wx.ColourDatabase().Find('BLACK'))
self.Refresh()
def main():
app = wx.App()
frame = MyFrame(None)
frame.Show()
app.MainLoop()
if __name__ == '__main__':
main()
The above application displays 2 lines of text. In each line there is a label (wx.StaticText widget) on the left and a TextCtrl widget on the right. when you enter text into the TextCtrl widget of either line and move the focus to the other TextCtrl widget the corresponding label foreground color (the text color) changes to RED or BLUE and the other label changes to BLACK. However, without any seen reason, the caret disappears from the top TextCtrl widget and never returns to it! (On MS Windows 8.1 at least).
The documentation explicitly tells you
The focus event handlers should almost invariably call wxEvent::Skip() on their event argument to allow the default handling to take place.
and goes on to explain that not doing it may result in various problems -- such as the one you're observing.
Add e.Skip() to your event handler to fix it.

WxPython - How to hide the X and expand button on window

Im making a python program and in some functions it needs to hide the X and expand window buttons, how would i do it? Im using WxPython, how would I put this in?
The widgets in the window frame are defined as part of the window's style: CLOSE_BOX, MINIMIZE_BOX, and MAXIMIZE_BOX.
So, when you create the window, just leave those styles out.
If you're using a wx.Frame subclass, note that DEFAULT_FRAME_STYLE includes these values, so you will have to mask them out:
style = wx.DEFAULT_FRAME_STYLE & (~wx.CLOSE_BOX) & (~wx.MAXIMIZE_BOX)
super().__init__(whatever, args, you, use, style=style)
If you want to change them after creation, you use SetWindowStyle:
style = self.GetWindowStyle()
self.SetWindowStyle(style & (~wx.CLOSE_BOX) & (~wx.MAXIMIZE_BOX))
self.Refresh()
However, notice that the documentation of that function says:
Please note that some styles cannot be changed after the window creation and that Refresh() might need to be called after changing the others for the change to take place immediately.
And, from what I can tell, on Windows, if you create a window with a close box and then remove it later in this way, it doesn't actually go away. It does disable, which may be good enough. But if not, there's probably no way to do what you want without either reaching underneath wx to the native Windows API (which gets very tricky), or drawing the widgets on the frame manually (which gets even more tricky, especially if you care about looking right on different versions of Windows—not to mention porting to other platforms).
I wrote about Frame styles a while ago on my blog. To remove all the buttons, you could do this:
import wx
########################################################################
class NoSystemMenuFrame(wx.Frame):
"""
There is no system menu, which means the title bar is there, but
no buttons and no menu when clicking the top left hand corner
of the frame
"""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
no_sys_menu = wx.CAPTION
wx.Frame.__init__(self, None, title="No System Menu", style=no_sys_menu)
panel = wx.Panel(self)
self.Show()
#----------------------------------------------------------------------
if __name__ == "__main__":
app = wx.App(False)
frame = NoSystemMenuFrame()
app.MainLoop()
I tried setting the style to wx.DEFAULT_FRAME_STYLE & (~wx.CLOSE_BOX) & (~wx.MAXIMIZE_BOX) and to wx.DEFAULT_FRAME_STYLE^(wx.CLOSE_BOX|wx.MAXIMIZE_BOX), but both of those seem to only remove the Close box. For some reason, the Maximize button is still there on my Xubuntu machine.

TextCtrl scrollbar unusable until window is resized

Solved:
Thanks to Aya's answer below I now know that the issue was caused by self.panel = wx.Panel(self, -1) on line 18. I created a panel and didn't attach anything to it. The original issue description is still below for reference.
My Google-fu has failed me. I'm building the text editor that you can find here, written in Python with wxPython:
https://github.com/joshsaintjacque/py-ed/blob/master/pyed.py
The issue that I'm running into is this: when I open a text file (the only functionality built in at this point) that's larger than the viewable area in the TextCtrl the scroll bar remains disabled until the window is re-sized, then it works fine.
I know that the act of re-sizing the window is running some command that I'm neglecting to include in my OpenFile function (or perhaps in init), but I can't figure out what.
Any thoughts anyone has that could lead me in the right direction would be greatly appreciated.
Thanks!
+1 for including a link to the full source code - makes it so much easier to test.
I couldn't reproduce the fault you describe on wxPython 2.8.12 on Win32, but upon running your code, I found a seemingly extraneous wx.Panel object being created on pyed.py line 18...
self.panel = wx.Panel(self, -1)
...which seems to be interfering with the correct operation of the program. After commenting out that line, it seems to work fine.
A couple of other things I noticed: line 56...
self.SetTitle("PyEd - Editing ... " + filename)
...should probably be put in the preceding if-block, otherwise you'll get an error if the user clicks "Cancel" on the wx.FileDialog, and on line 16...
wx.Frame.__init__(self, parent, id, 'PyEd', (-1, -1), wx.Size(640, 480))
...if you use keyword args rather than positional args...
wx.Frame.__init__(self, parent=parent, id=id, title='PyEd', size=wx.Size(640, 480))
...you needn't bother re-specifying the default value for the window position, which is also slightly safer, in case the wxPython developers decide to change the defaults in a future version.
You can also factor out constant values, and the optional creation of the wx.Size object to reduce that line to...
wx.Frame.__init__(self, parent=None, title='PyEd', size=(640, 480))
Finally, with regards to IDs: in most cases you'll probably find they're of little use. Where they come in handy is where you want many similar controls, and it makes more sense to have them handled by a single event handler function.
Consider this example...
def create_buttons(parent):
parent.button1 = wx.Button(label='Button 1')
parent.button2 = wx.Button(label='Button 2')
parent.button3 = wx.Button(label='Button 3')
parent.button1.Bind(wx.EVT_BUTTON, on_button_1)
parent.button2.Bind(wx.EVT_BUTTON, on_button_2)
parent.button3.Bind(wx.EVT_BUTTON, on_button_3)
def on_button_1(event):
print 'You clicked button 1'
def on_button_2(event):
print 'You clicked button 2'
def on_button_3(event):
print 'You clicked button 3'
...which is fine, but if you need, say, 100 buttons, you may prefer to implement it like this...
def create_buttons(parent):
parent.buttons = [wx.Button(id=i, label='Button %d' % i) for i in range(100)]
parent.Bind(wx.EVT_BUTTON, on_button)
def on_button(event):
button_id = event.GetId()
print 'You clicked button %d' % button_id
Oh, and be careful using id as a variable name, because it's also a Python built-in function name.
It looks as if you're not setting the min or max size hints for the window, nor are you calling Self.Fit() to fit the box sizer to the window size (or is it the other way round? I'm rusty on my wxPython...)
Right where you call self.SetSizer(sizer), you should be able to fix this by adding:
self.Fit()
self.SetSizeHintSz(minSize=wx.Size(640, 480))
You may be able to get around the separate call to self.Fit() by using self.SetSizerAndFit()
(edited for spelling.)

wxPython: how to make taskbar icon respond to left-click

Using wxPython, I created a taskbar icon and menu.
Everything works fine (in Windows at least) upon right-click of the icon: i.e., the menu is displayed, and automatically hidden when you click somewhere else, like on Windows' taskbar.
Now I do want to have the menu appear when the icon is left-clicked as well.
So I inserted a Bind() to a left-click in the Frame class wrapper, calling the CreatePopupMenu() of the taskbar icon:
import wx
class BibTaskBarIcon(wx.TaskBarIcon):
def __init__(self, frame):
wx.TaskBarIcon.__init__(self)
self.frame = frame
icon = wx.Icon('test_icon.ico', wx.BITMAP_TYPE_ICO)
self.SetIcon(icon, "title")
def CreatePopupMenu(self):
self.menu = wx.Menu()
self.menu.Append(wx.NewId(), "dummy menu ")
self.menu.Append(wx.NewId(), "dummy menu 2")
return self.menu
class TaskBarFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, style=wx.FRAME_NO_TASKBAR)
...
self.tbicon = BibTaskBarIcon(self)
wx.EVT_TASKBAR_LEFT_UP(self.tbicon, self.OnTaskBarLeftClick)
...
def OnTaskBarLeftClick(self, evt):
self.PopupMenu(self.tbicon.CreatePopupMenu())
...
def main(argv=None):
app = wx.App(False)
TaskBarFrame(None, "testing frame")
app.MainLoop()
This works fine, except that the menu does not disappear automatically when you click somewhere else on your screen. In fact, left-clicking multiple times on the icon creates multiple menus. The only way to hide the menu(s) is to click on one of its items (which you don't always want). I've looked at the available methods of TaskbarIcon, but I failed to be clear about which one to use to hide the menu (.Destroy() didn't work). Moreover, I don't know which event to bind it to (there is a EVT_SET_FOCUS, but I couldn't find any EVT_LOOSE_FOCUS or similar).
So, how to hide the menu upon losing focus?
EDIT: I've inserted a bit more code, to make it more clear
Ah, I've discovered what went wrong. In the statement
self.PopupMenu(self.tbicon.CreatePopupMenu())
I had bound the popup menu to the frame, instead of to the taskbar icon.
By changing it to:
self.tbicon.PopupMenu(self.tbicon.CreatePopupMenu())
all is working well now.
Thanks for all remarks
I think the problem here is that the PopupMenu is usually used in a program's context, not a little icon in the system tray. What that means is that in a normal frame, the popup menu would detect the click the you clicked off of it. Here, you are clicking outside of the wxPython program. Also, the PopupMenu is usually used with EVT_CONTEXT_MENU, not this taskbar event.
You can try wx.EVT_KILL_FOCUS and see if that works since it should theoretically fire when you click off the menu. Or you could ask on the official wxPython forum here: http://groups.google.com/group/wxpython-users/topics
Mike Driscoll
Blog: http://blog.pythonlibrary.org

Categories