Disclaimer: Perhaps I've gone about this incorrectly, but Im only about 24 hours into GUI development with wxpython. So general advice is appreciated.
Goal: I'm trying to familiarize myself with wxpython by making a simple screen saver.
Problem: Im trying to bind both mouse movement, and keyboard movement (aka trying to capture ANY user input) so it Destroys() the app on event (typical screensaver behavior). So far, my mouse events are being captured and it destroys properly, but my keyboard events are not (pound on my keyboard as I might!). I've tried EVT_CHAR and CHAR_HOOK as other SO posts have recommended. I've even go as far to see where my focus is - as I think that this is my problem (notice the line has self.SetFocus() - if you remove this, and move the mouse, the returned "self.FindFocus()" thats triggered by mouse movement events is returning None... with the SetFocus() it is now returning my class of SpaceFrame).
Question: Why can't I capture key strokes and activate my keyboardMovement() method? Interestingly enough, the example from here works just fine for the keyboard events down/up. So I'm 100% it's user error.
import wx
import random
MAX_INVADERS = 10
INVADERS_COLORS = ["yellow_invader",
"green_invader",
"blue_invader",
"red_invader"]
class SpaceFrame(wx.Frame):
def __init__(self):
"""
The generic subclassed Frame/"space" window. All of the invaders fall
into this frame. All animation happens here as the parent window
as well.
"""
wx.Frame.__init__(self, None, wx.ID_ANY, "Space Invaders", pos=(0, 0))
self.SetFocus()
self.Bind(wx.EVT_MOTION, self.mouseMovement)
self.Bind(wx.EVT_CHAR_HOOK, self.keyboardMovement)
self.panel = wx.Panel(self)
self.panel.SetBackgroundColour('black')
self.SetBackgroundColour('black')
self.monitorSize = wx.GetDisplaySize()
for invader in range(0, MAX_INVADERS, 1):
randX = random.randint(0, self.monitorSize[0])
self.showInvader(coords=(randX, 0),
invader=random.choice(INVADERS_COLORS),
scale=(random.randint(2, 10)/100.0))
def mouseMovement(self, event, *args):
print self.FindFocus()
def keyboardMovement(self, event, *args):
self.Destroy()
def showInvader(self, coords=(0, 0), invader="green_invader", scale=.05):
"""
Displays an invader on the screen
"""
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.dropInvader, self.timer)
self.timer.Start(1000)
self.invader = wx.Bitmap("{0}.png".format(invader))
self.invader = wx.ImageFromBitmap(self.invader)
self.invader = self.invader.Scale((self.invader.GetWidth()*scale),
(self.invader.GetHeight()*scale),
wx.IMAGE_QUALITY_HIGH)
self.result = wx.BitmapFromImage(self.invader)
self.control = wx.StaticBitmap(self, -1, self.result)
self.control.SetPosition(coords)
self.panel.Show(True)
def moveInvader(self, coords):
self.control.SetPosition(coords)
def dropInvader(self, *args):
# print "this hit"
self.control.SetPosition((100, 600))
if __name__ == "__main__":
application = wx.PySimpleApp()
window = SpaceFrame()
window.ShowFullScreen(True, style=wx.FULLSCREEN_ALL)
application.MainLoop()
Research Done So Far: Maybe I missed something, but nothing stood out to me here.
Source1 - Mouse Vs Python examples
Source2 - SO similar problem (but not exactly my solution)
Source3 - WXpython forum/mailing list
Source4 - WXpython forum/mailing list
Related
I have a wx frame that uses a SplitterWindow (the frame has also main menu, toolbar, status bar – but this is not relevant here). Everything works as expected, except for the mouse right click popup menu over buttons, in that the popup menu shows up at apparently random positions over the screen – and at "negative" random positions when moving the frame to the second screen (monitor). By "apparently" I mean that the popup menu position seems somewhat related to the actual button (or frame) position, but multiplied with some factor – positive on main screen and negative on the second.
I ran the code only on Windows 10 64bit / Python 3.9.0 64bit / wx '4.1.1 msw (phoenix) wxWidgets 3.1.5'. The first lines of the frame code were generated via wxGlade, so perhaps this could be related in the particular way the frame code was initially generated.
I created a stripped down test code, shown below, that mimics the exact situation of the real code in terms of mouse right click popup menu. In this test code I placed the button in the second pane, but it behaves the same on whatever pane.
I tried the same popup menu code on other simple wx example codes but without using SplitterWindow and there the popup behavior was ok. What should I change or improve in the test code below ?
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import wx
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
self.SetSize((400, 300))
self.SetTitle("test frame")
sizer_0 = wx.BoxSizer(wx.HORIZONTAL)
self.window_1 = wx.SplitterWindow(self, wx.ID_ANY)
self.window_1.SetMinimumPaneSize(20)
sizer_0.Add(self.window_1, 1, wx.EXPAND, 0)
self.pane_1 = wx.Panel(self.window_1, wx.ID_ANY)
sizer_1 = wx.BoxSizer(wx.HORIZONTAL)
self.pane_2 = wx.Panel(self.window_1, wx.ID_ANY)
sizer_2 = wx.BoxSizer(wx.HORIZONTAL)
self.button = wx.Button(self.pane_2, wx.ID_ANY, "test button")
sizer_2.Add(self.button, 0, 0, 0)
self.pane_1.SetSizer(sizer_1)
self.pane_2.SetSizer(sizer_2)
self.window_1.SplitVertically(self.pane_1, self.pane_2)
self.SetSizer(sizer_0)
self.Layout()
self.Bind(wx.EVT_CONTEXT_MENU, self.OnButtonContextMenu, self.button)
def OnButtonContextMenu(self, event):
self.PopupMenu(ButtonContext(self), event.GetPosition())
##
class ButtonContext(wx.Menu):
def __init__(self, parent):
super(ButtonContext, self).__init__()
self.parent = parent
button_popup = wx.MenuItem(self, wx.ID_ANY, 'test popup')
self.Append(button_popup)
self.Bind(wx.EVT_MENU, self.button_action, button_popup)
def button_action(self, event):
event.Skip()
##
class MyApp(wx.App):
def OnInit(self):
self.frame = MyFrame(None, wx.ID_ANY, "")
self.SetTopWindow(self.frame)
self.frame.Show()
return True
##
if __name__ == "__main__":
app = MyApp(0)
app.MainLoop()
It's slightly convoluted but I think you are overriding the InvokingWindow's position for wx.Menu by passing event.GetPosition() to class ButtonContext.
In short if you drop that parameter, event.GetPosition() and just invoke it with self.PopupMenu(ButtonContext(self)), it will default to the parent window, the button itself.
The result being that it will always focus on the button that you just right clicked.
I'm trying to change the cursor shape on key event:
When i press 'C', i want to display a LineCursor,
when i press 'S', i want to display a CrossCursor, and
when i press 'N', i want to display the standard ArrowCursor.
The cursor change only if it leave the canvas and return to it,
but not if the cursor stay in the canvas.
self.update() on the canvas don't work
Here the code to reproduce the problem :
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setObjectName("MainWindow")
self.resize(942, 935)
self.centralwidget = QWidget(self)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.MainView = QGraphicsView(self.centralwidget)
self.MainView.setObjectName("MainView")
self.horizontalLayout.addWidget(self.MainView)
self.setCentralWidget(self.centralwidget)
self.setWindowTitle("MainWindow")
self.scene = QGraphicsScene( 0.,0., 1240., 1780. )
self.canvas = Canvas()
self.widget = QWidget()
box_layout = QVBoxLayout()
self.widget.setLayout(box_layout)
box_layout.addWidget(self.canvas)
self.scene.addWidget(self.widget)
self.MainView.setScene(self.scene)
self.MainView.setRenderHints(QPainter.Antialiasing)
self.MainView.fitInView(0, 0, 45, 55, Qt.KeepAspectRatio)
self.show()
empty = QPixmap(1240, 1748)
empty.fill(QColor(Qt.white))
self.canvas.newPixmap(empty)
def keyPressEvent(self, e):
key = e.key()
if key == Qt.Key_C:
self.canvas.setCutCursor()
elif key == Qt.Key_N:
self.canvas.setNormalCursor()
elif key == Qt.Key_S:
self.canvas.setSelectionCursor()
class Canvas(QLabel):
def __init__(self):
super().__init__()
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setSizePolicy(sizePolicy)
self.setAlignment(Qt.AlignLeft)
self.setAlignment(Qt.AlignTop)
def newPixmap(self, pixmap):
self.setPixmap(pixmap)
def setCutCursor(self):
newCursor = QPixmap(500,3)
newCursor.fill(QColor("#000000"))
self.setCursor(QCursor(newCursor))
def setSelectionCursor(self):
self.setCursor(Qt.CrossCursor)
def setNormalCursor(self):
self.setCursor(QCursor(Qt.ArrowCursor))
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWindow = MainWindow()
sys.exit(app.exec_())
It seems to be an old bug that was never resolved: setCursor on QGraphicsView don't work when add QWidget on the QGraphicsScene
There is a possible workaround, but it's far from perfect.
First of all, you have to consider that while dealing with a QGraphicsScene and its view[s] is not easy when dealing with mouse events and widget proxies, mostly because of the multiple nested levels of events and interaction between the actual view (and its parent, up to the top level window) and the proxy itself, which is an abstraction of the widget you added to the scene. While Qt devs did a huge amount of work to make it as transparent as possible, at some point you will probably face some unexpected or undesired behavior that is usually hard to fix or work around, and that's also because a graphics scene might be visualized in more than a single view.
Besides the aforementioned bug, you have to consider that a graphics view uses QWidget.setCursor internally whenever any of its items call setCursor on themselves, and since the view is a very complex widget, at some point it might even try to "restore" the cursor if it thinks it should (even if it shouldn't).
Finally, some events which also have something to do with focus might become in the way of all that.
The first workaround is to set the cursor to the view itself (or, better, the view's viewport, which is the actual widget that shows the scene contents). To ensure that, we obviously need to check if the cursor is inside the canvas.
Unfortunately, because of the event handling written above, this could become a bit messy, as some events are even delayed by at least a cycle within the main Qt event loop; the result is that while setting a cursor the first time might work, setting it again might not, and even if it would, it's possible that the cursor will not be applied until the mouse is moved at least by one pixel.
As a second workaround, we need an event filter to bypass all that and check the cursor whenever the mouse is moved within the viewport margins.
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# ...
self.show()
empty = QPixmap(1240, 1748)
empty.fill(QColor(Qt.darkGray))
self.canvas.newPixmap(empty)
# install an event filter on the view's viewport;
# this is very, *VERY* important: on the *VIEWPORT*!
# if you install it on the view, it will *not* work
self.MainView.viewport().installEventFilter(self)
def insideCanvasRect(self, pos):
canvasRect = self.canvas.rect()
# translate the canvas rect to its top level window to get the actual
# geometry according to the scene; we can't use canvas.geometry(), as
# geometry() is based on the widget's parent coordinates, and that
# parent could also have any number of parents in turn;
canvasRect.translate(self.canvas.mapTo(self.canvas.window(), QPoint(0, 0)))
# map the geometry to the view's transformation, which probably uses
# some scaling, but also translation *and* shearing; the result is a
# polygon, as with shearing you could transform a rectangle to an
# irregular quadrilateral
polygon = self.MainView.mapFromScene(QRectF(canvasRect))
# tell if the point is within the resulting polygon
return polygon.containsPoint(pos, Qt.WindingFill)
def eventFilter(self, source, event):
if source == self.MainView.viewport() and (
(event.type() == QEvent.MouseMove and not event.buttons()) or
(event.type() == QEvent.MouseButtonRelease)
):
# process the event
super(MainWindow, self).eventFilter(source, event)
if self.insideCanvasRect(event.pos()):
source.setCursor(self.canvas.cursor())
else:
source.unsetCursor()
# usually a mouse move event within the view's viewport returns False,
# but in that case the event would be propagated to the parents, up
# to the top level window, which might reset the *previous* cursor
# at some point, no matter if we try to avoid that; to prevent that
# we return True to avoid propagation.
# Note that this will prevent any upper-level filtering and *could*
# also create some issues for the drag and drop framework
if event.type() == QEvent.MouseMove:
return True
return super(MainWindow, self).eventFilter(source, event)
def keyPressEvent(self, e):
# send the canvas a fake leave event
QApplication.sendEvent(self.canvas, QEvent(QEvent.Leave))
key = e.key()
if key == Qt.Key_C:
self.canvas.setCutCursor()
elif key == Qt.Key_N:
self.canvas.setNormalCursor()
elif key == Qt.Key_S:
self.canvas.setSelectionCursor()
pos = self.canvas.rect().center()
event = QEnterEvent(pos, self.canvas.mapTo(self.canvas.window(), pos), self.canvas.mapToGlobal(pos))
# send a fake enter event (mapped to the center of the widget, just to be sure)
QApplication.sendEvent(self.canvas, event)
# if we're inside the widget, set the view's cursor, otherwise it will not
# be set until the mouse is moved
if self.insideCanvasRect(self.MainView.viewport().mapFromGlobal(QCursor.pos())):
self.MainView.viewport().setCursor(self.canvas.cursor())
I have made a PyQt app with event filter that makes a widget follow mouse movement, It follows the mouse movement and sometimes goes back to position 0, 0...
import sys
from PyQt4 import QtGui, QtCore
import resources
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
widget = QtGui.QWidget(self) # Central Widget
self.edit = QtGui.QLineEdit(self) # Field that show's mouse position
self.somewidget = QtGui.QPushButton(self) # Widget that will follow mouse movement
self.setCentralWidget(widget)
self.edit.move(0, 0)
self.somewidget.show()
self.timer = QtCore.QTimer(self) # Timer that will be used to set a little interval for mouse movement, to make it smoother.
self.timer.timeout.connect(self.movefunc)
def eventFilter(self, source, event): # Defining event type to follow mouse movement without button.
if event.type() == QtCore.QEvent.MouseMove:
if event.buttons() == QtCore.Qt.NoButton:
self.pos = event.pos() # Define full position.
self.edit.setText('x: %d, y: %d' % (self.pos.x(), self.pos.y())) # show's mouse x and y position in field.
self.timer.start(5) # starts timer for 5 milliseconds.
else:
pass
return QtGui.QMainWindow.eventFilter(self, source, event)
def movefunc(self):
self.somewidget.move(self.pos.x(), self.pos.y()) # moves widget to mouse position in every 5 milliseconds.
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
win = Window()
win.show()
app.installEventFilter(win)
sys.exit(app.exec_())
so as you can see widget moves every 5 milliseconds, so it can be more smoother, but it does get back to position 0, 0 sometimes (every 1-2 second), what may the problem be? How can i limit it to certain positions, with it being smooth?
A few things
You shouldn't have to call timer.start() more than once. Once the timer begins, it's going to continue triggering every 5 milliseconds until you call timer.stop().
The coordinate system for the Window and the button aren't the same. Coordinates are always in the geometry of their parent. For the Window, that means global coordinates, because it's a top level window. For the button, the coordinates are in the geometry of the window, because the window is its parent. So giving the button coordinates from the window doesn't really make much sense.
I am sure this is a terribly stupid question. But I've used python for a year, i went through "Learn Python the hard way", I went through the Zetcode WxPython Tutorial, I looked through the WXPython TextCtrl Demo, and I've spent 2 days google searching for the answer to this seemingly simple question and I'm not figuring it out.
All I am trying to do is make a very simple test game where I have an input box, where the user types something like "get pineapple", then presses [ENTER], and send this phrase to a function I will make that processes the request, then sends the output to an output box that says something like "you cannot get the pineapple." Since I have coded lots of things at the office in python, and have coded full-featured financial models and games with VBA, I was sure this would be easy but it seems impossible. And WXPython is the obstacle.
If i put a string variable under "frame" or "panel" or under the class objects for TC1 or TC2, then they're treated as utterly alien and empty in the MainLoop, no matter where i put the variable it's not recognized. It seems impossible to use the same variable when making a function under the TextCtrl1 and TextCtrl2, or vice versa. I think I have to do something wwith "event.skip", or maybe make layers of passing things deep into the layers of WXPython and then pulling them all the way back out again, but I am not sure where to put it or how to do this.
Above all please tell me how I can figure out the answers to questions like this myself! I feel humiliated just asking it since it seems like it must be so easy, because if it were hard it would exist and be answered on this Q&A website.
I have all the GUI skeleton looking good, I have the "Enter" keypress event working, i have my multimedia set up and working fine, I know how i will be setting up my objects, I just need to be pointed in the right direction here. Even if you could just show me how to take input from one textctrl, and pass it unchanged to the output read-only textctrl, that would be perfect and i could figure out the rest. I could post the rest of my code here but I read somewhere that was bad manners.
EDIT: I am pasting the code here at the request of a would-be answerer.
Thank you in advance.
import wx
from random import randint
from pygame import mixer # Load the required library
mixer.init()
mixer.music.load('sog.ogg')
mixer.music.set_volume(1.0)
mixer.music.play()
RandomSound1 = mixer.Sound('CritHit.wav')
RandomSound2 = mixer.Sound('swallow2.wav')
RandomSound3 = mixer.Sound('thunder2.wav')
#instantiate class
class MusTogButt(wx.Button):
def __init__(self, *args, **kw):
super(MusTogButt, self).__init__(*args, **kw)
self.Bind(wx.EVT_BUTTON, self.MusicToggleFunction)
def MusicToggleFunction(self, mtf):
if mixer.music.get_busy() == True:
print "test, is playing"
mixer.music.stop()
else:
mixer.music.play()
class SoundTestButt(wx.Button):
def __init__(self, *args, **kw):
super(SoundTestButt, self).__init__(*args, **kw)
self.Bind(wx.EVT_BUTTON, self.PlayRandSound)
def PlayRandSound(self, mtf):
randsoundnum = randint (0,100)
if randsoundnum < 34:
RandomSound1.play()
elif randsoundnum < 68:
RandomSound2.play()
else:
RandomSound3.play()
class CPFInputter(wx.TextCtrl):
def __init__(self, *args, **kw):
super(CPFInputter, self).__init__(*args, **kw)
self.Bind(wx.EVT_COMMAND_ENTER, self.TextEntryFunction)
def TextEntryFunction(self, mtf):
print "you pressed enter"
class Example(wx.Frame):
def __init__(self, parent, title):
super(Example, self).__init__(parent, title=title,
size=(800, 600))
self.InitUI()
self.Centre()
self.Show()
def InitUI(self):
panel = wx.Panel(self)
#--------------------------------------------------------------
#This is an event handler. It handles the pressing of Enter, only.
def UserInputtedCommand(self):
keycode = self.GetKeyCode()
if keycode == wx.WXK_RETURN or keycode == wx.WXK_NUMPAD_ENTER:
print self.GetValue()
hbox = wx.BoxSizer(wx.HORIZONTAL)
fgs = wx.FlexGridSizer(3, 2, 9, 25)
# 3 rows
# 2 columns
# 9 vert gap
# 25 horizonatl gap
OutBoxLabel = wx.StaticText(panel, label="Outbox") #notice that these do NOT have wx.expand and they do NOT expand when the window is sized.
InBoxLabel = wx.StaticText(panel, label="Input:") #notice that these do NOT have wx.expand and they do NOT expand when the window is sized.
#make a bunch of input text controls, under the main panel.
InTC = wx.TextCtrl(panel)
InTC.Bind(wx.EVT_KEY_DOWN, UserInputtedCommand)
OutTC = wx.TextCtrl(panel, style=wx.TE_MULTILINE)
MusicToggle = MusTogButt(panel, label="Toggle Music Playback")
SoundTester = SoundTestButt(panel, label="Play Random Sound")
#Use AddMany AFTER you've built all your widgets with their specifications and put them in objects.
fgs.AddMany([(OutBoxLabel), (OutTC, 1, wx.EXPAND),
(InBoxLabel), (InTC, 1, wx.EXPAND), (MusicToggle), (SoundTester)])
fgs.AddGrowableRow(0, 1)
fgs.AddGrowableCol(1, 1)
# So, in other words, the 1st and second textboxes can grow horizontally, and the 3rd and final textbox can grow horizontally and vertically.
#lastly, add the FGS to the main hbox.
hbox.Add(fgs, proportion=1, flag=wx.ALL|wx.EXPAND, border=15)
#...and set sizer.
panel.SetSizer(hbox)
if __name__ == '__main__':
app = wx.App()
Example(None, title='CPF_FunGame2_MediaTest')
print "cpf_Fungame2_mediatest_running"
app.MainLoop()
Below creates two text controls, when you type something in the first and press enter it shows up in the second control.
In your code you are missing the style=wx.TE_PROCESS_ENTER.
# -*- coding: utf-8 -*-
#!/usr/bin/env python
import wx
import wx.lib.sized_controls as sc
class AFrame(sc.SizedFrame):
def __init__(self, *args, **kwds):
super(AFrame, self).__init__(*args, **kwds)
pane = self.GetContentsPane()
self.tc1 = wx.TextCtrl(pane, style=wx.TE_PROCESS_ENTER)
self.tc2 = wx.TextCtrl(pane)
self.tc1.Bind(wx.EVT_TEXT_ENTER, self.onTc1Enter)
def onTc1Enter(self, Evt):
self.tc2.ChangeValue(self.tc1.GetValue())
if __name__ == "__main__":
import wx.lib.mixins.inspection as WIT
app = WIT.InspectableApp()
f = AFrame(None)
f.Show()
app.MainLoop()
I suspect that your issue has to do with the scope of your variables, but without more detail I can only speculate what the issue might be. You could have this handled one of a few ways:
Pass the object of the output box to this function, then you can use its methods to display the result.
Return the value to allow it to be used at a greater scope than it was originally at.
When you bind to the TextCtrl, try this:
self.InTC.Bind(wx.EVT_TEXT, UserInputtedCommand)
def UserInputtedCommand (self, event):
Line = self.InTC.GetValue()
How could I have a scrollbar inside a gtk.Layout.
For example, in my code I have:
import pygtk
pygtk.require('2.0')
import gtk
class ScrolledWindowExample:
def __init__(self):
self.window = gtk.Dialog()
self.window.connect("destroy", self.destroy)
self.window.set_size_request(300, 300)
self.scrolled_window = gtk.ScrolledWindow()
self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.window.vbox.pack_start(self.scrolled_window, True, True, 0)
self.layout = gtk.Layout()
self.scrolled_window.add(self.layout)
self.current_pos = 0
self.add_buttom()
self.window.show_all()
def add_buttom(self, widget = None):
title = str(self.current_pos)
button = gtk.ToggleButton(title)
button.connect_object("clicked", self.add_buttom, None)
self.layout.put(button, self.current_pos, self.current_pos)
button.show()
self.current_pos += 20
def destroy(self, widget):
gtk.main_quit()
if __name__ == "__main__":
ScrolledWindowExample()
gtk.main()
What I really want is to find some way to make the scroll dynamic. See the example that I put above, when you click any button, another button will be added. But the scrollbar doesn't work.
What can I do to get the scroll bars working?
Does it works if you either use gtk.Window() instead of gtk.Dialog(); or execute self.window.run() after self.window.show_all()?
The difference between Dialog and common Window is that Dialog has its own loop which processes events. As you do not run its run() command, this loop never gets the chance to catch the events, so ScrolledWindow does not receives them, and does not change its size.