I have a frame with one radio box to toggle full screen. The frame is to go full screen when the user clicks the Maximize button. However, if I use the maximize button, the radio box would then fail to restore the window. If I use the radio box to go full screen, it will be able to restore the window.
import wx
class FSWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.SetSize((800, 600))
self.RadioFullScreen = wx.RadioBox(self, -1, "Display", choices=["Windowed","Full Screen"])
self.RadioFullScreen.Bind(wx.EVT_RADIOBOX, self.FS)
self.Bind(wx.EVT_MAXIMIZE, self.OnMaximize)
self.Sizer = None
self.Show()
def FS(self, Event):
if self.RadioFullScreen.GetSelection():
self.ShowFullScreen(True)
else:
self.ShowFullScreen(False)
def OnMaximize(self, Event):
self.ShowFullScreen(True) # <-- Add self.Restore() or self.Maximize(False) here
self.RadioFullScreen.SetSelection(1)
App = wx.App()
frame =FSWindow(None, -1, "MainWindow")
App.MainLoop()
However, if I add self.Restore() or self.Maximize(False) before the self.ShowFullScreen(True) like I commented on the source code above, the radio buttons will work. Problem is, the window will be restored first before going full screen which is ugly. Any solution for this? Also please explain why this happened, if possible.
Running Python 2.7.9, WxPython 3.0.2 on Window 7 Professional 32-bit
It seems that ShowFullScreen is not setting some flag, so things get out of sync.
If I just use Maximize/Restore things work fine for me, i.e. following changes to your code.
def FS(self, Event):
if self.RadioFullScreen.GetSelection():
self.Maximize()
#self.ShowFullScreen(True, style=wx.FULLSCREEN_ALL)
print('done fs true')
else:
#self.ShowFullScreen(False, style=wx.FULLSCREEN_ALL)
self.Restore()
print('done fs false')
def OnMaximize(self, Event):
Event.Skip()
self.RadioFullScreen.SetSelection(1)
print('done max')
If you don't want the menu bar etc when the screen is maximized then uncomment the ShowFullScreen lines.
You are handling an event "Maximize", most of the time you want default behaviour also to happen, that is why I added Event.Skip to the OnMaximize handler - in this case it doesn't make a difference as it looks like the event is only fired after maximisation is already done.
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'm building a custom combobox, from a (subclassed) QLineEdit and QListWidget for the dropdown menu
I'm setting the window flags to QTool so that its a floating window but doesnt steal focus from the lineedit (since the user needs to be able to input text to filter the list). This works fine but the list is now completely detached from the parent widget, so I can drag the top menu bar and move it away from the list which I don't want.
Is there a way to use QTool or QTooltip but keep it parented to a widget?
One other method would be setting the window flags to QPopup, in which case the popup closes when the top menu bar is clicked so cannot be dragged away. However with QPopup it steals focus from the line edit
Below is a simple example illustrating the issue:
from PySide2 import QtCore, QtWidgets, QtGui
import sys
class LineEditClickable(QtWidgets.QLineEdit):
"""Custom QLineEdit to detect clicked, focus and key events
Signals: clicked, focusOut, arrowUp, arrowDown
"""
clicked = QtCore.Signal(QtGui.QMouseEvent)
def __init__(self, value=''):
super(LineEditClickable, self).__init__(value)
# remove border on Mac
self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 0)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
def mousePressEvent(self, event):
"""Emit clicked signal"""
self.clicked.emit(event)
super(LineEditClickable, self).mousePressEvent(event)
class popup(QtWidgets.QWidget):
def __init__(self, parent = None, widget=None):
QtWidgets.QWidget.__init__(self, parent)
layout = QtWidgets.QVBoxLayout(self)
self.list = QtWidgets.QListWidget()
layout.addWidget(self.list)
# adjust the margins or you will get an invisible, unintended border
layout.setContentsMargins(0, 0, 0, 0)
self.adjustSize()
# tag this widget as a popup
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool)
# self.setWindowFlags(QtCore.Qt.Popup)
def update(self, widget):
# calculate the botoom right point from the parents rectangle
point = widget.rect().bottomRight()
# map that point as a global position
global_point = widget.mapToGlobal(point)
# by default, a widget will be placed from its top-left corner, so
# we need to move it to the left based on the widgets width
self.move(global_point - QtCore.QPoint(self.width(), 0))
def show_popup(self, widget):
self.update(widget)
self.show()
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.le = LineEditClickable(self)
self.le.clicked.connect(self.handleOpenDialog)
self.le.move(250, 50)
self.resize(600, 200)
self.popup = popup(self, self.le)
self.popup.list.addItems(['one','two','three'])
def handleOpenDialog(self):
self.popup.show_popup(self.le)
self.popup.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())```
The basic answer to your question is to use the correct flags and focus options.
If you look at how QCompleter implements setPopup(), you'll see the following:
popup->setWindowFlag(Qt::Popup);
popup->setFocusPolicy(Qt::NoFocus);
[...]
popup->setFocusProxy(d->widget);
As you've already experienced, Tool is not a good option: while avoids stealing focus from the line edit, it also has issues with any mouse click that happens outside the UI.
If you still want to use Tool, you could update the widget position, by installing an event filter on the top level window of the line edit and intercept its move events, but it's not guaranteed that it works and totally depends on the platform you're using it. For example, on certain Linux window managers you only receive it when the mouse is released after dragging the window.
class popup(QtWidgets.QWidget):
_widget = None
_window = None
# ...
def show_popup(self, widget):
if self._window:
self._window.removeEventFilter(self)
self.update(widget)
self.show()
self._widget = widget
self._window = widget.window()
self._window.installEventFilter(self)
def hideEvent(self, event):
if self._window:
self._window.removeEventFilter(self)
def closeEvent(self, event):
if self._window:
self._window.removeEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Move:
self.update(self._widget)
return super().eventFilter(source, event)
Frankly, I'd suggest you to use what Qt already provides you without trying to reinvent the wheel. In your case, use a QCompleter and reimplement what you need for it's popup.
Note that if you want to show all items when the line edit gets focus and there's no text yet, you could change the completion mode.
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
# ...
self.textChanged.connect(self.showCompleter)
def showCompleter(self):
completer = self.completer()
if not completer:
return
if not self.text():
completer.setCompletionMode(completer.UnfilteredPopupCompletion)
else:
completer.setCompletionMode(completer.PopupCompletion)
completer.complete()
You might want to do the same also in the keyPressEvent override, after calling the base class implementation and ensuring that the popup is not yet visible.
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()
I am making a Tkinter application. In the application, I want to pop up a context menu, which I do using Tk.Menu.post().
I don't know how to make this menu disappear when the application loses focus. I need to do this because the menu stays on top, even when switching to another window, leaving a menu 'artifact'.
I put a <FocusOut> event on the menu, which is triggered if the menu has focus and the user moves focus to another application. This works well.
What do I do if the main application window has focus? I could put a <FocusOut> event on the application window, which closes the menu; however, this ends up being called when I give focus to the menu, which closes the menu. The menu is created with parent as the main application, so I am not sure why <FocusOut> on the main app is even triggered when the menu gets focus.
How do I distinguish between the main application window losing focus to a different application vs losing focus to my menu?
I don't want to use tk_popup() because I want the user to continue to provide input to the main window. (use of the menu is optional).
Thanks to #Brad Lanam I came up with a SOLUTION, which I have included:
from Tkinter import *
class App(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
self.entry = Entry(self)
self.entry.grid(padx=30, pady=30)
self.entry.focus_set()
self.entry.bind("<Tab>", self.put_menu)
self.entry.bind("<Down>", self.set_focus_on_menu)
self.menu = Menu(self, tearoff=False)
self.menu.add_command(label="test")
self.menu.bind("<FocusOut>", self.destroy_menu)
self.bind("<FocusIn>", self.app_got_focus)
self.bind("<FocusOut>", self.app_lost_focus)
self.bind("<3>", self.put_menu)
def app_got_focus(self, event):
self.config(background="red")
def app_lost_focus(self, event):
self.config(background="grey")
######## SOLUTION BEGIN #########
if self.focus_get() != self.menu:
self.destroy_menu(event)
######## SOLUTION END ###########
def set_focus_on_menu(self, event):
self.menu.focus_set()
def menu_got_focus(self, event):
self.menu.activate(0)
def put_menu(self, event):
self.menu.post(self.winfo_x() + self.entry.winfo_x(), self.winfo_y() + self.entry.winfo_y()+20)
def destroy_menu(self, event):
self.menu.destroy()
app = App()
app.mainloop()
self.focus_get() will return the object that has focus, which can be used to distinguish between the menu receiving focus, vs some other application.
For example, to remove the menu when the focus moves to another application:
def app_lost_focus(self, event):
if self.focus_get() != self.menu:
self.destroy_menu(event)
The simple curve in this application only appears when it's dragged off the screen, or the window is resized. When the application just starts up it doesn't appear, and when the window is maximized or minimized it also disappears. However, all of these times, "Path Drawn" is printed, so all of the painting functions are called. Is there something I'm doing wrong with regards to creating and drawing on the graphicscontext? If not, how can I make the window totally refresh in these special cases?
import wx
class Path(object):
def paint(self,gc):
print "Path Drawn"
gc.SetPen(wx.Pen("#000000",1))
path=gc.CreatePath()
path.MoveToPoint(wx.Point2D(10,10))
path.AddCurveToPoint(wx.Point2D(10,50),
wx.Point2D(10,150),
wx.Point2D(100,100))
gc.DrawPath(path)
class TestPane(wx.Panel):
def __init__(self,parent=None,id=-1):
wx.Panel.__init__(self,parent,id,style=wx.TAB_TRAVERSAL)
self.SetBackgroundColour("#FFFFFF")
self.Bind(wx.EVT_PAINT,self.onPaint)
self.SetDoubleBuffered(True)
self.path=Path()
def onPaint(self, event):
event.Skip()
dc=wx.PaintDC(self)
dc.BeginDrawing()
gc = wx.GraphicsContext.Create(dc)
gc.PushState()
self.path.paint(gc)
gc.PopState()
dc.EndDrawing()
def drawTestRects(self,dc):
dc.SetBrush(wx.Brush("#000000",style=wx.SOLID))
dc.DrawRectangle(50,50,50,50)
dc.DrawRectangle(100,100,100,100)
class TestFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(640,480))
self.mainPanel=TestPane(self,-1)
self.Show(True)
app = wx.App(False)
frame = TestFrame(None,"Test App")
app.MainLoop()
Comment out the self.SetDoubleBuffered(True) part and it will work, because due to bug http://trac.wxwidgets.org/ticket/11138 window isn't refreshed correctly if SetDoubleBuffered and GraphicsContext are used together.
If you MUST need double buffering implement it yourselves e.g. first draw to a MeomryDC and then blit or paint bitmap to paint dc.