Qt Dialog in Main Window, make the background dimmed - python

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()

Related

Adding the same object to a QTabWidget

I have a few toggleable QPushButtons that I need to add to all tabs of a QTabWidget. Each tab should track the current state of each toggleable QPushButton which is why I am adding the same object instead of creating new buttons. The position of the widgets doesn't really matter, as long as I have the same tbutton object on all tabs. (i.e. when tbutton is enabled on Tab 1, it should be enabled on Tabs 2 and 3).
Below is the code I'm using. Note that in this example I'm only showing one toggleable QPushButton.
import sys
from PyQt5.QtWidgets import QTabWidget, QPushButton, QApplication, QWidget, QHBoxLayout
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = QTabWidget()
tbutton = QPushButton("foo")
tbutton.setCheckable(True)
w1 = QWidget()
layout1 = QHBoxLayout(w1)
layout1.addWidget(QPushButton("bar"))
layout1.addWidget(tbutton)
w2 = QWidget()
layout2 = QHBoxLayout(w2)
layout2.addWidget(QPushButton("baz"))
layout2.addWidget(tbutton)
tabW.addTab(w1, "Tab 1")
tabW.addTab(w2, "Tab 2")
tabW.addTab(tbutton, "Tab 3")
tabW.show()
sys.exit(app.exec_())
When the app is run, only Tab 3 contains tbutton. While it is possible to always show a single button using QTabWidget's setCornerWidget method, it is more difficult to modify the layout when the corner widget added is more complicated (i.e. more toggleable buttons).
Widgets cannot be "shared" between parents and/or shown multiple times. Every time you add a widget to a new layout, then it will be removed from the previous.
While you could reparent the widget by adding it to the layout everytime the tab is changed, it's really not a practical solution, especially if complex layout systems are involved, and reparenting is normally used only in very specific cases where it's really important to use the same instance elsewhere due to its complex state (for instance, a toolbar). Since you're just using a button, there's little point in reusing the same instance.
A more logical and safe solution is to create a button for every tab and link their state using signals, then if you also need to connect the toggled signal to another function, do it to just to one of them (not all):
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = QTabWidget()
w1 = QWidget()
layout1 = QHBoxLayout(w1)
layout1.addWidget(QPushButton("bar"))
tbutton1 = QPushButton("foo", checkable=True)
layout1.addWidget(tbutton1)
w2 = QWidget()
layout2 = QHBoxLayout(w2)
layout2.addWidget(QPushButton("baz"))
tbutton2 = QPushButton("foo", checkable=True)
layout2.addWidget(tbutton2)
tbutton3 = QPushButton("foo", checkable=True)
tabW.addTab(w1, "Tab 1")
tabW.addTab(w2, "Tab 2")
tabW.addTab(tbutton3, "Tab 3")
buttons = tbutton1, tbutton2, tbutton3
def toggleButtons(state):
for button in buttons:
button.setChecked(state)
for button in buttons:
button.toggled.connect(toggleButtons)
tabW.show()
sys.exit(app.exec_())
The above indication is because the code as it's written triggers some level of recursion, as the signal will be emitted for all setChecked calls on buttons that have a different state. Since Qt is well written, all state changes emit signals only when the new state is actually different, but if you want to do things more correctly, just block the signals temporarily:
def toggleButtons(state):
for button in buttons:
blocked = button.blockSignals(True)
button.setChecked(state)
button.blockSignals(blocked)
But, the best solution is to use QSignalBlocker which does almost the same thing, but in a safer fashion:
def toggleButtons(state):
for button in buttons:
with QtCore.QSignalBlocker(button):
button.setChecked(state)
This has a catch: since the signal will then be only emitted once, for the button that has actually triggered, if you need to do something else when the state changes, you either do it in toggleButtons or you connect the function to all buttons.
If you want to separate the logic of "group toggling" from the actual function that reacts to the state change, a better solution is to use a custom signal, but you must use a class.
class TabWidget(QTabWidget):
stateChanged = QtCore.pyqtSignal(bool)
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = TabWidget()
# ...
buttons = tbutton1, tbutton2, tbutton3
def toggleButtons(state):
for button in buttons:
with QtCore.QSignalBlocker(button):
button.setChecked(state)
tabW.stateChanged.emit(state)
for button in buttons:
button.toggled.connect(toggleButtons)
def doSomething(state):
print('state changed', state)
tabW.stateChanged.connect(doSomething)
tabW.show()
sys.exit(app.exec_())
Please consider that this "procedural" flow is fine for educational purposes, but using a class and implement both UI and logic in it is usually a better practice, and the above functions should then become methods in that class so that they can act in the context of the instace.

Python PyQt5 how to show the full QMenuBar with a QWidget

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)

PyQt: How can i set stylesheet contents on scaled mode?

I had an application that contained a lot of widgets with stylesheet on them, However, I did not add any layout to interface, It neither had central widget included, But the application was running without any problems.
However, whenever i tried to resize the application (scaling it down) the widgets would not scale, of course.
I had an little research (Because i could not find anything else related to my problem) and i found this on Qt Documentation, stylesheet reference:
"The actual image that is drawn is determined using the same algorithm as QIcon (i.e) the image is never scaled up but always scaled down if necessary."
How can i make stylesheet scale down with window? (If stylesheet has background image on)
For example i have button with stylesheet:
btn = QtGui.QPushButton(self)
btn.move(0, 0)
btn.setObjectName('btn)
btn.setStyleSheet("#btn {background-image: url(':/images/somepicture.png'); border: none; }")
How can i make this button scale down with window, Can i achieve this without layouts? If not how can i do it with layouts? (without it limiting too much)
If you add the button as the central widget to a QMainWindow it should automatically adjust it's size to fit the available space. However, to get the button image to scale, you need to set the image as a border-image stylesheet property (a little strange). A working example for PyQt4:
from PyQt4 import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
btn = QtGui.QPushButton(self)
btn.setStyleSheet("border-image: url('somepicture.png');") # Scaled
#btn.setStyleSheet("background-image: url('somepicture.png');") # Not scaled
self.setCentralWidget(btn)
self.show()
app = QtGui.QApplication([])
window = MainWindow()
app.exec_()
Note that you don't need to set an id (objectName) to assign the CSS to a specific widget, you can simply pass in the CSS rule via .setStyleSheet().
You cannot set a layout on QMainWindow as it already has a complex layout system to accommodate docking widgets and toolbars. Therefore, if you want to use a layout to add more than one widget to the window, you need to use a container widget to hold it. The following working example demonstrates this:
from PyQt4 import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
w = QtGui.QWidget() # container widget
l = QtGui.QVBoxLayout() # your layout
w.setLayout(l) # set the layout on your container widget
btn = QtGui.QPushButton(self)
btn.setStyleSheet("border-image: url('somepicture.png');")
label = QtGui.QLabel('Hello!')
l.addWidget(btn) # add your widget to the layout
l.addWidget(label) # add the label to the layout
self.setCentralWidget(w) # add the container widget to the QMainWindow
self.show()
app = QtGui.QApplication([])
window = MainWindow()
app.exec_()
If you want to be able to position widgets absolutely, rather than adding them to a layout (which will control their size/position) you can pass the parent element (relative to which x,y coords are taken) when creating it:
from PyQt4 import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
w = QtGui.QWidget() # container widget
btn = QtGui.QPushButton(w)
btn.move(100,100)
btn.setStyleSheet("border-image: url('somepicture.png');")
self.setCentralWidget(w) # add the container widget to the QMainWindow
self.show()
app = QtGui.QApplication([])
window = MainWindow()
app.exec_()
But positioning a widget absolutely like this loses you the ability to auto-scale it to fit the parent widget. If you just want some padding/spacing around the element in the window, take a look at .setContentsMargins on the QLayouts, e.g. l.setContentsMargins(50,50,50,50) will put a 50px margin around the button.

How to pop out a separate window from a tabWidget in PySide Qt

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
}

Properly Positioning Popup Widgets in PyQt

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_())

Categories

Resources