I was having trouble formatting the title of this question, because I wasn't sure I'm going about this the right way, so let me explain.
I want to try and add a right-click context menu to an existing program for which I don't have the source code. wxPython is generally my framework of choice. I figured there was a couple ways of doing this:
1) Create a transparent wx.Frame which is tied to and sits on top of the existing program, intercepting mouse events. If I did this, I wasn't sure if the mouse events could then be passed to the underlying window. I like this option, because it would allow adding more useful information in the overlay.
2) Create a headless program which globally intercepts right-click events, and spawns the context menu at the pointer location when certain conditions are met. Based on the research I've done so far, this didn't seem possible without continuously polling for mouse position.
What am I missing? Is there a more elegant solution for this? Is this even possible using Python?
edit: I have a partial proof-of-concept working which looks like this:
import wx
import win32gui
import win32api
import win32con
class POC_Frame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title='POC', pos=(0,0), size=wx.Size(500, 500), style=wx.DEFAULT_FRAME_STYLE)
self.ToggleWindowStyle(wx.STAY_ON_TOP)
extendedStyleSettings = win32gui.GetWindowLong(self.GetHandle(), win32con.GWL_EXSTYLE)
win32gui.SetWindowLong(self.GetHandle(), win32con.GWL_EXSTYLE,
extendedStyleSettings | win32con.WS_EX_LAYERED | win32con.WS_EX_TRANSPARENT)
win32gui.SetLayeredWindowAttributes(self.GetHandle(), win32api.RGB(0,0,0), 100, win32con.LWA_ALPHA)
self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)
self.Bind(wx.EVT_RIGHT_UP, self.onRightUp)
self.CaptureMouse()
def onRightDown(self, event):
print(event)
def onRightUp(self, event):
print(event)
app = wx.App(False)
MainFrame = POC_Frame(None)
MainFrame.Show()
app.MainLoop()
This seems to work OK, as it passes the right click events to the underlying window, while still recognizing them, but it only does it exactly once. As soon as it loses focus, it stops working and nothing I've tried to return focus to it seems to work.
I've always had better luck hooking global mouse and keyboard events with pyHook rather than wx. Here is a simple example:
import pyHook
import pyHook.cpyHook # ensure its included by cx-freeze
class ClickCatcher:
def __init__(self):
self.hm = None
self._is_running = True
self._is_cleaned_up = False
self._is_quitting = False
self.set_hooks()
# this is only necessary when not using wx
# def send_quit_message(self):
# import ctypes
# win32con_WM_QUIT = 18
# ctypes.windll.user32.PostThreadMessageW(self.pump_message_thread.ident, win32con_WM_QUIT, 0, 0)
def __del__(self):
self.quit()
def quit(self):
if not self._is_running:
return
self._is_quitting = True
self._is_running = False
if self.hm:
# self.hm.UnhookKeyboard()
self.hm.UnhookMouse()
# self.send_quit_message()
self._is_cleaned_up = True
def set_hooks(self):
self._is_running = True
self._is_cleaned_up = False
self.hm = pyHook.HookManager()
self.hm.MouseRightUp = self.on_right_click
# self.hm.HookKeyboard()
self.hm.HookMouse()
def on_right_click(self):
# create your menu here
pass
If you weren't using wx, you'd have to use pythoncom.PumpMessages to push mouse and keyboard events to you program, but App.Mainloop() accomplishes the same thing (if you use PumpMessages and Mainloop together about half of the events won't be push to your program).
Creating a wx.Menu is easy enough. You can find the mouse coordinates using wx.GetMousePosition()
Related
OS: W10. This may be significant. If you have different results on a different platform, feedback would be helpful.
Here is an MRE. If you run it and go Ctrl+O, the menu labels become greyed. If you select a file in the QFileDialog by clicking the "Open" button or using its mnemonic (Alt+O), the open-file dialog is dismissed and the "Files" and "Help" menus become un-greyed.
However, if you go Ctrl+O again, and this time enter the name of a file in the "File name" box (QLineEdit), and then press Return, the dialog is dismissed (with a successful selection result) but the "Files" and "Help" menus remain greyed-out. It looks like this:
import sys, os
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Greying of menus MRE')
self.setGeometry(QtCore.QRect(100, 100, 400, 200))
menubar = QtWidgets.QMenuBar(self)
self.setMenuBar(menubar)
self.files_menu = QtWidgets.QMenu('&Files', self)
menubar.addMenu(self.files_menu)
self.help_menu = QtWidgets.QMenu('&Help', self)
menubar.addMenu(self.help_menu)
self.new_action = QtWidgets.QAction('&New', self)
self.files_menu.addAction(self.new_action)
self.open_action = QtWidgets.QAction('&Open', self)
self.files_menu.addAction(self.open_action)
self.open_action.setShortcut("Ctrl+O")
self.open_action.triggered.connect(self.open_file)
def focusInEvent(self, event ):
print('main_window focusInEvent')
super().focusInEvent(event)
def focusOutEvent(self, event ):
print('main_window focusOutEvent')
super().focusInEvent(event)
def activateWindow(self):
print('main_window activateWindow')
super().activateWindow()
def open_file(self):
print('open file')
main_window_self = self
# open_doc_dialog = QtWidgets.QFileDialog(self.get_main_window())
class OpenDocFileDialog(QtWidgets.QFileDialog):
def accepted(self):
print('accepted')
super().accepted()
def accept(self):
print('accept')
super().accept()
def close(self):
print('close')
super().close()
def done(self, r):
print(f'done r {r}')
# neither of these solves the problem:
# main_window_self.activateWindow()
# main_window_self.files_menu.activateWindow()
super().done(r)
def hide(self):
print(f'hide')
super().hide()
def focusInEvent(self, event ):
print('focusInEvent')
super().focusInEvent(event)
def focusOutEvent(self, event ):
print('focusOutEvent')
super().focusInEvent(event)
def activateWindow(self):
print('activateWindow')
super().activateWindow()
open_doc_dialog = OpenDocFileDialog(self)
open_doc_dialog.setWindowTitle('Choose file')
open_doc_dialog.setDirectory(os.getcwd())
# we cannot use the native dialog, because we need control over the UI
options = open_doc_dialog.Options(open_doc_dialog.DontUseNativeDialog)
open_doc_dialog.setOptions(options)
open_doc_button = open_doc_dialog.findChild(QtWidgets.QDialogButtonBox).button(QtWidgets.QDialogButtonBox.Open)
lineEdit = open_doc_dialog.findChild(QtWidgets.QLineEdit)
# this does not solve the problem
# lineEdit.returnPressed.disconnect()
# lineEdit.returnPressed.connect(open_doc_button.click)
print(f'open_doc_button {open_doc_button}, lineEdit {lineEdit}')
# show the dialog
dialog_code = open_doc_dialog.exec()
if dialog_code != QtWidgets.QDialog.Accepted: return
sel_files = open_doc_dialog.selectedFiles()
print(f'sel_files: {sel_files}')
app = QtWidgets.QApplication([])
main_window = MainWindow()
main_window.show()
sys.exit(app.exec())
This problem can be understood, if not solved, with reference to this answer.
Note that this greying-out is not disablement. As explained in the above link, this has to do with "active/inactive states" of the menus (or their labels). The menus remain enabled throughout, although in this case it's impossible to know that while the open-file dialog is showing because it is modal. Clicking on one menu after the dialog has gone, or just hovering over it, is enough to un-grey them both...
The explanation, as I understand it, is that the "File name" box QLineEdit has a signal, returnPressed, which appears to activate something subtley different to the slot which is invoked when you use the "Choose" button. You can see I have experimented with trying to re-wire that signal, to no avail.
The method done of the QFileDialog appears to be called however the dialog closes (unlike close!), so I tried "activating" the main window... and then the individual QMenus... Doesn't work.
I am not clear how to get a handle on this "active state" business or why the slot connected to returnPressed is (seemingly) unable to give the "active state" back to the menus when the other slot manages to do so.
Edit
Searching on Musicamante's "unpolishing" suggestion led me to this:
lineEdit.returnPressed.disconnect()
def return_pressed():
style = main_window_self.menubar.style()
style.unpolish(main_window_self.menubar)
open_doc_button.click()
lineEdit.returnPressed.connect(return_pressed)
... unfortunately this doesn't work.
This looks like a possible Windows-related bug, since I can't reproduce it on Linux. As a work-around, you could try forcing a repaint after the dialog closes:
# show the dialog
dialog_code = open_doc_dialog.exec()
self.menubar.repaint()
Finally got it, thanks to Musicamante's suggestion:
lineEdit.returnPressed.disconnect()
def return_pressed():
style = main_window_self.menubar.style()
style.unpolish(main_window_self.menubar)
open_doc_button.click()
main_window_self.menubar.repaint()
lineEdit.returnPressed.connect(return_pressed)
... I actually tried this several times, just to make sure it was doing what was intended. So in fact, fortunately, no single-shot timer was needed in this case.
I have a touchscreen laptop that folds back enough to become like a tablet. If I put it down on the table, I don't want to be hitting keys accidentally, so I'm working on a script to disable the keyboard when I hit Ctrl-F10 and then re-enable it when I do that again. I'm using xlib from PyPI, and I've gotten this so far:
from Xlib.display import Display
from Xlib.ext import xinput
class Handler:
def __init__(self, display):
self.enabled = True
self.display = display
def handle(self, event):
if event.data['detail'] == 76 and event.data['mods']['base_mods'] == 4:
if self.enabled:
self.display.grab_server()
else:
self.display.ungrab_server()
self.enabled = not self.enabled
try:
display = Display()
handler = Handler(display)
screen = display.screen()
screen.root.xinput_select_events([
(xinput.AllDevices, xinput.KeyPressMask),
])
while True:
event = display.next_event()
handler.handle(event)
finally:
display.close()
It does disable the keyboard on Ctrl-F10, but as soon as I re-enable, all the keys I pressed when it was disabled are activated all at once. Is there a way to clear the queue before re-enabling, or a better way to disable the keyboard?
Try XGrabKeyboard: https://tronche.com/gui/x/xlib/input/XGrabKeyboard.html
(But this requires you to create your own window for grabbing; you can e.g. create a window of size 1x1 at position -10x-10)
I think the values for things like owner_events and keyboard_mode do not matter much. The main effect should be that the input focus goes to your own window. time should be CurrentTime (which is 0) and pointer_mode should be GrabModeAsync, so that you do not interfere with the pointer.
I'm having a problem trying to handle focus events in wxPython 3, the phoenix fork.
I'm trying to clear the value in a TextCtrl when it's focused if it still has its default value, and then restore that default value on leaving focus if the user hasn't entered a different value.
This is the handler I have:
def on_enter(self, event):
control = wx.Window.FindFocus()
if control.Id in TEXT_MAPPING.keys():
if control.Value == TEXT_MAPPING[control.Id]:
control.SetValue('')
exit_control = event.GetWindow()
if exit_control.Id in (TEXT_MAPPING.keys()):
if not exit_control.GetValue():
exit_control.SetValue(TEXT_MAPPING[exit_control.Id])
event.Skip()
The handler itself works fine, though it's a little clunky. My issue is that I would like to bind this on a global level, such that whenever any wx.EVT_SET_FOCUS event is fired, I can have it be handled by this function.
I found this:
import wx
class MyApp(wx.App):
def FilterEvent(self, evt):
print evt
return -1
app = MyApp(redirect=False)
app.SetCallFilterEvent(True)
frm = wx.Frame(None, title='Hello')
frm.Show()
app.MainLoop()
but I'm confused on how I would pass the event down to the children of MyApp.
What you can do is bind focus kill and focus select to functions, like this:
self.user_text.Bind(wx.EVT_SET_FOCUS, self.on_focus_username)
self.user_text.Bind(wx.EVT_KILL_FOCUS, self.on_deselect_username)
where self is the panel where the TextCtrl "user_text" is located".
The functions might look like this:
def on_focus_username(self, event):
if self.user_text.GetForegroundColour() != wx.BLACK:
self.user_text.SetForegroundColour(wx.BLACK)
# Clear text when clicked/focused
self.user_text.Clear()
def on_deselect_username(self, event):
if self.user_text.GetLineLength(0) is 0:
self.user_text.SetForegroundColour(wx.Colour(192, 192, 192))
# Put a default message when unclicked/focused away
self.user_text.SetValue("Enter Username")
Hope this helped, if I didn't answer something clearly/correctly let me know.
My wxPython GUI either quits with a Segmentation Fault or fails to quit at all using the standard options. The only successful quit option (no errors) is wx.Exit, which I understand is not a great practice. I've traced the issues down to a few factors, but I'm scratching my head as to why they are having this effect.
Using the wxPython inspector (wx.lib.inspection.InspectionTool()), I've been able to determine that a FigureFrameWxAgg is being created when I run certain pylab functions (pylab.xticks() is the function that creates it here, but I haven't tracked down every single function that has this effect). I don't know what this window is for. It's invisible and doesn't appear to do anything. However, this window totally messes up the shutdown of my GUI. If I use self.Destroy, Python doesn't shut down fully. If I use sys.exit, I get a Segmentation fault. I need to catch the wx.EVT_CLOSE so that I can prompt the user to save his/her work.
Here is the code for a simplified version of the GUI:
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigCanvas
import wx
import wx.grid
import wx.lib.scrolledpanel
import wx.lib.inspection
import sys
import pylab
class my_frame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, 'Many Rows')
self.InitUI()
def InitUI(self):
self.panel = wx.Window(self, size=(200, 200))
hbox_all= wx.BoxSizer(wx.HORIZONTAL)
self.create_menubar()
self.fig1 = pylab.Figure((5, 5), dpi=100)
self.canvas1 = FigCanvas(self.panel, -1, self.fig1)
self.fig1.text(0.01,0.98,"Arai plot",{'family':'Arial', 'fontsize':10, 'style':'normal','va':'center', 'ha':'left' })
self.araiplot = self.fig1.add_axes([0.1,0.1,0.8,0.8])
self.araiplot.clear()
self.araiplot.plot(range(5),range(5),lw=0.75,clip_on=False)
xt = pylab.xticks()
grid = wx.grid.Grid(self.panel)
grid.ClearGrid()
grid.CreateGrid(100, 100)
grid.AutoSize()
hbox_all.AddSpacer(self.canvas1)
hbox_all.AddSpacer(20)
hbox_all.AddSpacer(grid)
hbox_all.AddSpacer(20)
self.panel.SetSizer(hbox_all)
hbox_all.Fit(self)
self.Centre()
self.Show()
def create_menubar(self):
"""
Create menu bar
"""
self.menubar = wx.MenuBar()
menu_file = wx.Menu()
menu_file.AppendSeparator()
m_exit = menu_file.Append(wx.ID_EXIT, "Quit", "Quit application")
self.Bind(wx.EVT_CLOSE, self.on_menu_exit)
self.menubar.Append(menu_file, "&File")
self.SetMenuBar(self.menubar)
def on_menu_exit(self, event):
self.Destroy() # this doesn't quit Python fully, unless I comment out 'matplotlib.use('WXAgg')'
#for w in wx.GetTopLevelWindows():
# if w.Title == 'Figure 1':
# w.Destroy() # if I pre-destroy the FigureFrameWxAgg window, I get a PyDeadObjectError when I run self.Destroy
# self.Destroy() #
# wx.Exit() # forces the program to exit, with no clean up. works, but not an ideal solution
#sys.exit() # program closes, but with segmentation error
#self.Close() # creates infinite recursion error, because we have a binding to wx.EVT_CLOSE
if __name__ == '__main__':
app = wx.PySimpleApp(redirect=False)
app.frame = my_frame()
if '-i' in sys.argv:
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()
To add one more level of complexity, the Segmentation Fault with sys.exit() only happens with my brew installed Python. Sys.exit() works fine with Canopy Python.
My questions are: how can I fix this error? And, is using wx.Exit() really so bad?
There are several issues with your example:
Do not use pylab in GUI applications, because is brings its own mainloop (which will not quit when the wxPython mainloop quits). You had to kill pylab.
# both not required
# matplotlib.use('WXAgg')
# import pylab
# use instead
from matplotlib.figure import Figure
...
def __init__(# ...
...
self.fig1 = Figure((5, 5), dpi=100)
Your menu item "Close" does not work (at least not on Windows). wx.ID_EXIT is meant for buttons in dialogs. Do not ask me which predefined IDs are meant for menus.
ID_QUIT = wx.NewId()
menu_file.Append(ID_QUIT , "Quit", "Quit application")
#
self.Bind(wx.EVT_MENU, self.on_quit, id=ID_QUIT)
def on_quit(self, evt):
self.Close()
In this case it is not necessary to bind to wx.EVT_CLOSE. If you want to do something on the close event, you have to skip it. When you skip it, wxPython will deal with it on its own.
self.Bind(wx.EVT_CLOSE, self.on_close)
...
def on_close(self, evt):
# you can veto the close here or perform cleanup
evt.Skip()
If you change your code accordingly, wxPython will close everything properly.
I agree with the answer from #nepix32 on not using pylab but use the OOP approach of Matplotlib. However, in my case, I still need the matplotlib.use('WX') for the application to show.
I have the same problem of wxpython GUI application not terminated properly if a Matplotlib figure is displayed even if I'm not using pylab or pyplot.
My workaround is to pass the app object to the wx.Frame class and call app.Exit at EVT_CLOSE.
class my_frame(wx.Frame):
def __init__(self, app=None):
...
self.InitUI()
self.Bind(wx.EVT_CLOSE, lambda evt: app.Exit())
...
if __name__ == '__main__':
...
app.frame = my_frame(app)
...
I am up for anyone who can suggest cleaner solution. To be honest, I'm still new to wxpython.
I am new to python. I am trying to gather links from youtube based on search string. Assumption is the link I want will be the first result on my search results.
Problem I am facing is mouse clicks are not recognised on the gui. It just moves to the x,y cordinates but I cannot see that it is getting clicked. Please help me to understand why this clicks are not recognized.
Below is the code: I created a GUI in wxpython, and I am trying to click on search bar of youtube, then paste the search string, then click on search take the link address from the first result. This below code is sample code. In my real code search string will be passed from excel sheet.
import win32api, win32con
import win32com.client as win32
from win32com.client import Dispatch, Constants
import os
import time
import win32clipboard
import wx
import wx.html2
import sys
x_pad = 0
y_pad = 0
def leftClick():
wx.EVT_LEFT_DOWN
time.sleep(.1)
wx.EVT_LEFT_UP
print "Click." #completely optional. But nice for debugging purposes.
def rightClick():
wx.EVT_RIGHT_DOWN
time.sleep(.1)
wx.EVT_RIGHT_UP
print "Right Click." #completely optional. But nice for debugging purposes.
def mousePos(cord):
win32api.SetCursorPos((x_pad + cord[0], y_pad + cord[1]))
class URL(wx.Frame):
def __init__(self, *args, **kw):
super(URL, self).__init__(*args, **kw)
self.InitUI()
def InitUI(self):
pnl = wx.Panel(self)
sbtn = wx.Button(pnl, label='Start', pos=(850, 560))
cbtn = wx.Button(pnl, label='Close', pos=(850, 610))
stdot = wx.TextCtrl(pnl, pos =(400,580),size=(400,80), style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL)
sys.stdout = stdot
sys.stderr = stdot
sbtn.Bind(wx.EVT_BUTTON, self.OnStart)
cbtn.Bind(wx.EVT_BUTTON, self.OnClose)
brwser = wx.html2.WebView.New(pnl, size = (1280,550), pos = (0,0), url =("http://www.youtube.com/results?search_query=sarabhai+vs+sarabhai+episode+25&sm=3"))
self.SetSize((1280, 720))
self.SetTitle('YouTube URL Grab')
self.SetPosition((0,0))
self.Show(True)
def OnStart(self,e):
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardText("Sarabhai vs Sarabhai episode 31") #Sample string for search
win32clipboard.CloseClipboard()
mousePos((252, 53))
leftClick()
leftClick()
leftClick()
rightClick()
time.sleep(5)
mousePos((280, 144))
leftClick()
time.sleep(5)
mousePos((755, 53))
leftClick()
time.sleep(7)
mousePos((467, 140))
rightClick()
time.sleep(5)
mousePos((514, 321))
leftClick()
def OnClose(self, e):
self.Close(True)
def main():
ex = wx.App()
URL(None)
ex.MainLoop()
if __name__ == '__main__':
main()
The names wx.EVT_LEFT_DOWN and wx.EVT_LEFT_UP are actually just numbers for the event identifies that you need to bind to they do not cause the events to happen! Look at the examples in the docs and demos package.
The basic process is:
During frame intitialisation bind handlers to the events you need to handle, these should be of type fn(self, evt) and by convention are named onNameOfEvent e.g. def onLeftBtnDown(self, evt) these contain the events that you need to react to and the evt parameter gives you an event object the members of which depend on the event type. The event binding uses the format self.Bind(wx.EVT_NAME_OF_EVENT, onNameOfEvent) and is normally in the windows __init__ method, (this is what the vast majority of GUI code does, including non wx GUIs).
If, (much more rarely), you need to generate events from your code rather than just reacting to the events you still need to do the above then where you need to create the event by declaring the appropriate type of event object, e.g. wx.CommandEvent, (note that you can create your own event types), populating with the required values and then use 'wx.PostEvent' to add it to the event queue for the window or control that it applies to. Sometime later, when it gets to the top of the queue, your event handler will be called with this event.