his has plagued me for eons, mostly due to how many combinations of methodologies there are for moving widgets and whatnot. Essentially I have a simple widget that I'd like to be able to pop up in specific areas of my app. Problem is I can never seem to get it to pop up where I want it. Additionally, I'd like to set it up in a way where I can adjust the "pointer" side of it based on whether it's popping up to point at a widget in the top-left of the app versus, say, the bottom-right.
Ideally, I'd be able to place the popup nearly adjacent to the edges of the parent widget, and anchor it based on where it is. Here's what I've been trying.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class popup(QWidget):
def __init__(self, parent = None, widget=None):
QWidget.__init__(self, parent)
layout = QGridLayout(self)
button = QPushButton("Very Interesting Text Popup. Here's an arrow ^")
layout.addWidget(button)
self.move(widget.rect().bottomLeft())
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.button = QPushButton('Hit this button to show a popup', self)
self.button.clicked.connect(self.handleOpenDialog)
self.button.move(250, 50)
self.resize(600, 200)
def handleOpenDialog(self):
self.popup = popup(self, self.button)
self.popup.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
This code generates a button that's randomly in the middle of the widget. What I'm trying to get is, in this example, the popup to show under the button with its "pivot" in the top right such that the arrow in the popup button would be pointing to the bottom right corner of the widget. However it's popping up in the top left of the Window instead. In all of my messing around with .move, .setGeometry, and playing with QRect, I can't for the life of me figure this out. Huge kudos to whoever can lend a hand. Thanks!
I know this is old, but I was searching for this recently and this is the best answer; I have a useful addition (for anyone else searching for this recipe!)
I implemented it as a mixin, which I think gives more flexibility to your dialogs:
class PopupDialogMixin(object): # will not work (with PySide at least) unless implemented as 'new style' class. I.e inherit from object
def makePopup(callWidget):
"""
Turns the dialog into a popup dialog.
callWidget is the widget responsible for calling the dialog (e.g. a toolbar button)
"""
self.setContentsMargins(0,0,0,0)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup)
self.setObjectName('ImportDialog')
# Move the dialog to the widget that called it
point = callWidget.rect().bottomRight()
global_point = callWidget.mapToGlobal(point)
self.move(global_point - QtCore.QPoint(self.width(), 0))
Your custom dialog would then inherit from both QtCore.QDialog and PopupDialogMixin. This gives you the option to use your dialog in the 'normal' way or make it a popup dialog. e.g:
dlg = MyDialog(self)
dlg.makePopup(self.myButton)
I think implementing it as a mixin gives a number of benefits:
No need to write the 'popup' code for each custom dialog you want as a popup
'Default' behaviour of the dialog is preserved - e.g. if you want to reuse it somewhere else as a 'regular' dialog, you just use it like normal
No need to pass anything extra to __init__ other than parent.
Here you go - the comments kind of explain the logic behind it - since the question is an example and about the positioning, I kept the rest of the code the same except the popup class, but just to mention cause its a pet peeve - you shouldn't import * (ever) but especially with something as big as PyQt4.QtCore/QtGui...
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class popup(QWidget):
def __init__(self, parent = None, widget=None):
QWidget.__init__(self, parent)
layout = QGridLayout(self)
button = QPushButton("Very Interesting Text Popup. Here's an arrow ^")
layout.addWidget(button)
# adjust the margins or you will get an invisible, unintended border
layout.setContentsMargins(0, 0, 0, 0)
# need to set the layout
self.setLayout(layout)
self.adjustSize()
# tag this widget as a popup
self.setWindowFlags(Qt.Popup)
# 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 - QPoint(self.width(), 0))
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.button = QPushButton('Hit this button to show a popup', self)
self.button.clicked.connect(self.handleOpenDialog)
self.button.move(250, 50)
self.resize(600, 200)
def handleOpenDialog(self):
self.popup = popup(self, self.button)
self.popup.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
Related
I am trying to create a dynamic custom dialog class. That takes the central widget as a parameter and that centralwidget will be the main widget of this custom dialog. (for dynamic)
When It will show itself, it will make the background dark/dimmed.
Probably I should use exec_ function for not clickable out of dialog area.
The important thing is to add himself to the layout of the mainwindow, so it can automatically adjust itself when the mainwindow's size/position changes.
I can do this with hooking resizeEvent/moveEvent but I am looking better way to do this. If I add this custom dialog to mainwindow's layout, it is gonna be better.
Thanks.
Creating a child widget that is drawn over the current content of the window and resized in the resizeEvent() override is absolutely not a problem. In fact, it's practically what Qt does every time a widget using a layout manager is resized. The benefit of this approach is that you can completely "cover" all contents of the window, including the menu bar, the status bar and any dock/toolbar.
If you still want them to be usable and only want to cover the main widget, you can do the same by setting the "cover" as a child of the main widget itself, instead of using the window as a parent.
An alternative could be to use a QStackedWidget as a central widget, and set the layout (which is a QStackedLayout) to use the StackAll stackingMode, which will allow you to show all "pages" of the stacked layout superimposed.
Note that this approach has an important drawback: you must take care of the tab-focus. Since all widgets (including widgets that belong to another "page") are shown and enabled, changing focus through Tab will allow changing the focus to widgets that are not part of your "dialog".
I'll leave you with a basic example, the central widget is a QTableWidget, and it shows the "popup" whenever an item is doubleclicked.
Please carefully study it and try to understand what it does.
from PyQt5 import QtCore, QtWidgets
class Container(QtWidgets.QWidget):
def showEvent(self, event):
if not event.spontaneous():
self.setFocus()
# certain widgets might want to keep focus on tab
# so we delay the focusNextChild
QtCore.QTimer.singleShot(0, self.focusNextChild)
def focusNextPrevChild(self, isNext):
# keep tab focus on this widget
super().focusNextPrevChild(isNext)
return self.isAncestorOf(QtWidgets.QApplication.focusWidget())
def paintEvent(self, event):
# stylesheets set on QWidget subclasses need this
qp = QtWidgets.QStylePainter(self)
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
qp.drawPrimitive(QtWidgets.QStyle.PE_Widget, opt)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.menuBar().addMenu('Test').addAction('Action')
self.stack = QtWidgets.QStackedWidget(self)
self.setCentralWidget(self.stack)
self.stack.layout().setStackingMode(QtWidgets.QStackedLayout.StackAll)
table = QtWidgets.QTableWidget(20, 30)
self.stack.addWidget(table)
table.cellDoubleClicked.connect(self.showDialog)
self.resize(QtWidgets.QApplication.primaryScreen().size() * 2 / 3)
def showDialog(self, row, column):
background = QtWidgets.QWidget(objectName='background')
background.setStyleSheet('''
#background {
background: rgba(64, 64, 64, 64);
}
Container {
background: palette(window);
border: 1px outset palette(window);
border-radius: 5px;
}
''')
backLayout = QtWidgets.QVBoxLayout(background)
container = Container()
backLayout.addWidget(container, alignment=QtCore.Qt.AlignCenter)
container.setAutoFillBackground(True)
layout = QtWidgets.QVBoxLayout(container)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(20)
font = self.font()
font.setPointSize(font.pointSize() * 3)
layout.addWidget(QtWidgets.QLabel(
'Hello!', font=font, alignment=QtCore.Qt.AlignCenter))
layout.addWidget(QtWidgets.QLabel(
'You doubleclicked cell {}, {}'.format(row + 1, column + 1)))
button = QtWidgets.QPushButton('Close')
layout.addWidget(button)
self.centralWidget().addWidget(background)
self.centralWidget().setCurrentWidget(background)
# Important! you must always delete the widget when you don't need it
# anymore. Alternatively, hide it if you want to reuse it again later
button.clicked.connect(background.deleteLater)
app = QtWidgets.QApplication([])
win = MainWindow()
win.show()
app.exec()
So I'm relatively new to qt and fairly seasoned with python.
However, I have an instance where I want multiple widgets (in this case labels) to share the same custom context menu but I need to get access to the widget's information.
When I use setContextMenuPolicy and customContextMenuRequested.connect on each label I only get information for the first label despite accessing the context menu with the second label.
Below is a stripped down version of what I'm working with:
from PyQt5 import QtGui
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QMenu, QLabel
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "PyQt5 Context Menu"
self.top = 200
self.left = 500
self.width = 200
self.height = 100
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.firstLabel = QLabel(self)
self.firstLabel.setText("Meep!")
self.firstLabel.setObjectName("firstLabel")
self.firstLabel.setStyleSheet("background-color: rgb(252, 233, 79);")
self.firstLabel.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.firstLabel.customContextMenuRequested.connect(self.customMenuEvent)
self.firstLabel.setGeometry(QtCore.QRect(0,0,50,30))
self.secondLabel = QLabel(self)
self.secondLabel.setText("Peem!")
self.secondLabel.setObjectName("secondLabel")
self.secondLabel.setStyleSheet("background-color: rgb(79,233, 252);")
self.secondLabel.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.secondLabel.customContextMenuRequested.connect(self.customMenuEvent)
self.secondLabel.setGeometry(QtCore.QRect(80,40,50,30))
print("FIRST:", self.firstLabel)
print("SECOND:", self.secondLabel)
self.show()
def customMenuEvent(self, eventPosition):
child = self.childAt(eventPosition)
print(child)
contextMenu = QMenu(self)
getText = contextMenu.addAction("Text")
getName = contextMenu.addAction("Name")
quitAct = contextMenu.addAction("Quit")
action = contextMenu.exec_(self.mapToGlobal(eventPosition))
if action == getText:
print(child.text())
if action == getName:
print(child.objectName())
if action == quitAct:
self.close()
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
From the documentation about the customContextMenuRequested(pos) signal:
The position pos is the position of the context menu event that the widget receives
This means that you will always receive the mouse position relative to the widget that fires the signal.
You're using QWidget.childAt(), which is relative to the parent geometry, but since the provided position is relative to the child widget, you'll always end up with coordinates that are relative to the top left corner of the parent.
This becomes clear if you try to set the geometry of the first widget in a position that's not the top left corner: even if you right-click on the first widget, you'll see that the menu will not appear where you clicked. If you look closely, you'll also see that the menu appears exactly at the position you clicked, based on the coordinates of the parent top left corner.
For the sake of simplicity, an easy solution would be to map the coordinates from the "sender" (which is the object that fired the last signal the receiver has received) to its parent:
def customMenuEvent(self, eventPosition):
child = self.childAt(self.sender().mapTo(self, eventPosition))
contextMenu = QMenu(self)
getText = contextMenu.addAction("Text")
getName = contextMenu.addAction("Name")
quitAct = contextMenu.addAction("Quit")
# note that the mapToGlobal is referred to the child!
action = contextMenu.exec_(child.mapToGlobal(eventPosition))
# ...
But be aware that this could lead to some inconsistency, especially when dealing with multithreading (as, theoretically, another thread could have fired another signal between the right click event and the moment the receiver actually receives it).
There are various different approaches to avoid that, but IT mostly depends on how you are going to structure your program.
For example, you could use a lambda to add an argument that helps to identify the source from which the event has been sent:
self.firstLabel.customContextMenuRequested.connect(
lambda pos, child=self.firstLabel: self.customMenuEvent(pos, child))
# ...
def customMenuEvent(self, eventPosition, child):
# ...
I'm getting this weird result when using QMenuBar I've used this exact code before for the QMenuBar and it worked perfectly. But it doesn't show more than 1 QMenu
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from functools import partial
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# background = QWidget(self)
lay = QVBoxLayout(self)
lay.setContentsMargins(5, 35, 5, 5)
self.menu()
self.setWindowTitle('Control Panel')
self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton')))
self.grid = QGridLayout()
lay.addLayout(self.grid)
self.setLayout(lay)
self.setMinimumSize(400, 320)
def menu(self):
menubar = QMenuBar(self)
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('Dark mode', self, checkable=True)
viewStatAct.setStatusTip('enable/disable Dark mode')
viewMenu.addAction(viewStatAct)
settingsMenu = menubar.addMenu('Configuration')
email = QAction('Set Email', self)
settingsMenu.addAction(email)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
Result:
I am aware that I am using QWidget when I should be using QMainWindow But is there a workaround???
(I apologize in advance for the terrible quality of the image, there is no good way to take a picture of a QMenuBar)
The problem is that with a QWidget you are not using the "private" layout that a QMainWindow has, which automatically resizes specific children widgets (including the menubar, the statusbar, the dock widgets, the toolbars and, obviously, the "centralWidget").
Remember that a QMainWindow has its own layout (which can't and shouldn't be changed), because it needs that specific custom layout to lay out the aforementioned widgets. If you want to set a layout for the main window, you'll need to apply it to its centralWidget.
Read carefully how the Main Window Framework behaves; as the documentation reports:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
In order to work around that when using a basic QWidget, you'll have to manually resize the children widgets accordingly. In your case, you only need to resize the menubar, as long as you have a reference to it:
def menu(self):
self.menubar = QMenuBar(self)
# any other function has to be run against the *self.menubar* object
viewMenu = self.menubar.addMenu('View')
# etcetera...
def resizeEvent(self, event):
# calling the base class resizeEvent function is not usually
# required, but it is for certain widgets (especially item views
# or scroll areas), so just call it anyway, just to be sure, as
# it's a good habit to do that for most widget classes
super(MainMenu, self).resizeEvent(event)
# now that we have a direct reference to the menubar widget, we are
# also able to resize it, allowing all actions to be shown (as long
# as they are within the provided size
self.menubar.resize(self.width(), self.menubar.height())
Note: you can also "find" the menubar by means of self.findChild(QtWidgets.QMenuBar) or using the objectName, but using an instance attribute is usually an easier and better solution.
Set minimum width
self.setMinimumSize(320,240)
I am building a PyQt application which is supposed to receive mouse right-click drags on a QGraphicsView, draw a "lasso" (a line stretching from the drag origin to the mouse position, and a circle at the mouse position), and then, on mouse release, erase the lasso graphics and display an input dialog for the next part of the app.
For some reason, when I use the mouse to click "Ok" on the input dialog, a menu artifact appears on the QGraphicsView which contained the lasso. The menu artifact is a drop-down menu line that says "(check mark) Exit". Occasionally it may be the context menu for one of my custom QGraphicsObjects as well - but for whatever reason, calling the dialog and then clicking "Ok" results in an unintended right-click-like event on the QGraphicsView.
This only seems to happen when the last step before method return is the QInputDialog - replacing it with a pass or a call to some other method does not result in the artifact. I'd be very grateful to anyone with a clue to what is causing this problem!
Here's the minimal code:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QMainWindow):
# The app main window.
def __init__(self):
super(Window, self).__init__()
# Initialize window UI
self.initUI()
def initUI(self, labelText=None):
# Set user-interface attributes.
# Set up menu-, tool-, status-bars and add associated actions.
self.toolbar = self.addToolBar('Exit')
# Create a menu item to exit the app.
exitAction = QtGui.QAction(QtGui.QIcon('icons/exit.png'), '&Exit', self)
exitAction.triggered.connect(QtGui.qApp.quit)
self.toolbar.addAction(exitAction)
# Create the main view.
self.viewNetwork = NetworkPortal()
self.viewNetwork.setMinimumWidth(800)
self.viewNetwork.setMinimumHeight(800)
self.setCentralWidget(self.viewNetwork)
self.show()
class NetworkPortal(QtGui.QGraphicsView):
# A view which allows you to see and manipulate a network of nodes.
def __init__(self):
super(NetworkPortal, self).__init__(QtGui.QGraphicsScene())
# Add the CircleThing graphic to the scene.
circleThing = CircleThing()
self.scene().addItem(circleThing)
class CircleThing(QtGui.QGraphicsEllipseItem):
# Defines the graphical object.
def __init__(self):
super(CircleThing, self).__init__(-10, -10, 20, 20)
# Set flags for the graphical object.
self.setFlags(
QtGui.QGraphicsItem.ItemIsSelectable |
QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges
)
self.dragLine = None
self.dragCircle = None
def mouseMoveEvent(self, event):
# Reimplements mouseMoveEvent to drag out a line which can, on
# mouseReleaseEvent, form a new Relationship or create a new Thing.
# If just beginning a drag,
if self.dragLine == None:
# Create a new lasso line.
self.startPosX = event.scenePos().x()
self.startPosY = event.scenePos().y()
self.dragLine = self.scene().addLine(
self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y(),
QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine)
)
# Create a new lasso circle at the location of the drag position.
self.dragCircle = QtGui.QGraphicsEllipseItem(-5, -5, 10, 10)
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
self.scene().addItem(self.dragCircle)
# If a drag is already in progress,
else:
# Move the lasso line and circle to the drag position.
self.dragLine.setLine(QtCore.QLineF(self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y()))
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
def mouseReleaseEvent(self, event):
# If the line already exists,
if self.dragLine != None:
# If the released button was the right mouse button,
if event.button() == QtCore.Qt.RightButton:
# Clean up the link-drag graphics.
self.scene().removeItem(self.dragLine)
self.dragLine = None
self.scene().removeItem(self.dragCircle)
self.dragCircle = None
# Create the related Thing.
# Display input box querying for name value.
entry, ok = QtGui.QInputDialog.getText(None, 'Enter some info: ',
'Input:', QtGui.QLineEdit.Normal, '')
return
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
newWindow = Window()
sys.exit(app.exec_())
This is a guess on my part, but I've seen this kind of weird thing before.
Some Qt widgets have default behavior on certain types of events. I've never used QGraphicsView, but often the default for a right click is to open a context-sensitive pop-up menu (usually useless, in my experience). That may be happening in your case, which would explain why you only see this when there's a right click.
You can suppress the default Qt behavior by calling event.ignore() before returning from mouseReleaseEvent.
There was not a direct answer to the cause of this bug, but using a QTimer.singleShot() to call the dialog (in combination with a QSignalMapper in order to enter parameters) is a functional workaround that separates the dialog from the method in which the bug was occurring. Thanks to #Avaris for this one.
I have an extended main window with a QtGui.QTabWidget added to it. I am creating several widgets extended from QtGui.QWidget which I can add and remove to the tab widget.
What I would like to do is have a "pop-out" button that causes the child widget to be removed from the tab widget and come up as it's own independent window (and a "pop-in" button to put it back into the main window). The same sort of idea as Gtalk-in-Gmail has.
Note that if I close the main window, the other "tabs" or "windows" should also close, and I should be able to put all the windows side-by-side and have them all visible and updating at the same time. (I will be displaying near-realtime data).
I am new to Qt, but if I'm not mistaken, if a Widget has no parent it comes up independently. This works, but I then have no idea how I could "pop" the window back in.
class TCWindow(QtGui.QMainWindow):
.
.
.
def popOutWidget(self, child):
i = self.tabHolder.indexOf(child)
if not i == -1:
self.tabCloseRequested(i)
self.widgets[i].setParent(None)
self.widgets[i].show()
My gut says that there should still be a parent/child relationship between the two.
Is there a way to keep the parent but still have the window come up independently, or am I misunderstanding Qt's style?
Otherwise, would creating a variable in the child to hold a link to the main window (like self.parentalUnit = self.parent()) be a good idea or a hackish/kludgy idea?
Leave the parent as is. If you remove the parent, then closing main window won't close 'floating' tabs, since they are now top-level windows. windowFlags defines if a widget is window or a child widget. Basically, you need to alternate between QtCore.Qt.Window and QtCore.Qt.Widget
Below is a small but complete example:
#!/usr/bin/env python
# -.- coding: utf-8 -.-
import sys
from PySide import QtGui, QtCore
class Tab(QtGui.QWidget):
popOut = QtCore.Signal(QtGui.QWidget)
popIn = QtCore.Signal(QtGui.QWidget)
def __init__(self, parent=None):
super(Tab, self).__init__(parent)
popOutButton = QtGui.QPushButton('Pop Out')
popOutButton.clicked.connect(lambda: self.popOut.emit(self))
popInButton = QtGui.QPushButton('Pop In')
popInButton.clicked.connect(lambda: self.popIn.emit(self))
layout = QtGui.QHBoxLayout(self)
layout.addWidget(popOutButton)
layout.addWidget(popInButton)
class Window(QtGui.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__()
self.button = QtGui.QPushButton('Add Tab')
self.button.clicked.connect(self.createTab)
self._count = 0
self.tab = QtGui.QTabWidget()
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button)
layout.addWidget(self.tab)
def createTab(self):
tab = Tab()
tab.setWindowTitle('%d' % self._count)
tab.popIn.connect(self.addTab)
tab.popOut.connect(self.removeTab)
self.tab.addTab(tab, '%d' % self._count)
self._count += 1
def addTab(self, widget):
if self.tab.indexOf(widget) == -1:
widget.setWindowFlags(QtCore.Qt.Widget)
self.tab.addTab(widget, widget.windowTitle())
def removeTab(self, widget):
index = self.tab.indexOf(widget)
if index != -1:
self.tab.removeTab(index)
widget.setWindowFlags(QtCore.Qt.Window)
widget.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
In Qt, layout takes ownership over the widgets that are added to layout, so let it handle parentship.
You can create another widget (with no parent) which will be hidden until you press pop-out button and when it is pressed, you remove "pop-out widget" from its original layout and add it to layout of the hidden widget. And when pop-in button pressed - return widget to it's original layout.
To close this hidden window, when closing main window, you will need to redefine closeEvent(QCloseEvent* ev) to something like this (sorry for c++, but i bet, in python it's all the same):
void MainWindow::closeEvent(QCloseEvent* ev)
{
dw->setVisible(false); // independent of mainwindow widget
sw->setVisible(false); // independent of mainwindow widget
QWidget::closeEvent(ev); //invoking close event after all the other windows are hidden
}