Custom title bar with dockable toolbar - qt

I want to create a custom title bar for my PyQt application, and my application uses dockable toolbars. To be dockable, the toolbars should be added to the MainWindow. However, with my custom toolbar being a Widget added to a frameless window, the toolbars dock themselves around the title bar. They can be docked above the title bar, and whenever docked on the sides, they push the title bar, which is not the expected behavior. I understand that this is due to the fact the the toolbar areas are always around the central widget of the window, and my custom title bar is inside the central widget. However, I don't see how I can make this work the way I want. Here is a MWE (I'm using PyQt=5.12.3) :
import sys
from typing import Optional
from PyQt5.QtCore import Qt, QSize, QPoint
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QMainWindow, QApplication, QVBoxLayout, QPushButton, QWidget, QToolBar, QHBoxLayout, QLabel, \
QToolButton
class CustomTitleBar(QWidget):
def __init__(self, title: str, parent: Optional[QWidget] = None):
super().__init__(parent=parent)
self.window_parent = parent
layout = QHBoxLayout()
self.setObjectName("CustomTitleBar")
self.setLayout(layout)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.setFixedHeight(40)
self.title = title
self.title_label = QLabel(self.title)
self.title_label.setObjectName("TitleBarLabel")
layout.addWidget(self.title_label)
layout.addStretch(1)
but_minimize = QToolButton()
but_minimize.setText("🗕")
but_minimize.setObjectName("MinimizeButton")
layout.addWidget(but_minimize)
but_minimize.clicked.connect(self.window().showMinimized)
self.but_resize = QToolButton()
if self.window().isMaximized():
self.but_resize.setText("🗗")
else:
self.but_resize.setText("🗖")
layout.addWidget(self.but_resize)
self.but_resize.clicked.connect(self.toggle_maximized)
self.but_resize.setObjectName("ResizeButton")
but_close = QToolButton()
but_close.setText("🗙")
layout.addWidget(but_close)
but_close.clicked.connect(self.window().close)
but_close.setObjectName("CloseButton")
self.m_pCursor = QPoint(0, 0)
self.moving = False
def toggle_maximized(self):
if self.window().isMaximized():
self.but_resize.setText("🗖")
self.window().showNormal()
else:
self.but_resize.setText("🗗")
self.window().showMaximized()
def mousePressEvent(self, event: QMouseEvent) -> None:
pass
def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
pass
def mouseMoveEvent(self, event: QMouseEvent) -> None:
pass
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
pass
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint)
self.resize(QSize(800, 600))
main_widget = QWidget(self)
self.setCentralWidget(main_widget)
layout = QVBoxLayout(self)
main_widget.setLayout(layout)
titlebar = CustomTitleBar("Custom TitleBar Test Window", self)
layout.addWidget(titlebar)
layout.addWidget(QPushButton("Hello world"))
layout.addStretch(1)
my_toolbar = QToolBar(self)
self.addToolBar(Qt.RightToolBarArea, my_toolbar)
my_toolbar.addWidget(QPushButton("A"))
my_toolbar.addWidget(QPushButton("B"))
my_toolbar.addWidget(QPushButton("C"))
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
The resulting window is as follow:
How can I get the dockable toolbars to behave around my custom title bar the way they should behave around a standard title bar ?

Since the title bar should be put outside the standard contents, you cannot put it inside the central widget.
The solution is to setContentsMargins() using the height of the title bar for the top margin. Then, since the title bar is not managed by any layout, you need to resize it by overriding the resizeEvent():
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.titlebar = CustomTitleBar("Custom TitleBar Test Window", self)
self.setContentsMargins(0, self.titlebar.sizeHint().height(), 0, 0)
def resizeEvent(self, event):
super().resizeEvent(event)
self.titlebar.resize(self.width(), self.titlebar.sizeHint().height())

As I guess, firstly you need to look QDockWidget Because you put your CustomTitleBar into centeralWidget in MainWindow and it states in Docking area, this is expected behaviour.
You need to create a Vertical Layout which would your , and put CustomTitleBar into it, you don't need MainWindow in your code. In main you can try something like that:
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QWidget()
layout = QHBoxLayout()
titlebar = CustomTitleBar("Custom TitleBar Test Window", self)
layout.addWidget(titlebar)
window.setLayout(layout)
main_widget = QWidget(self)
layout.addWidget(main_widget)
main_widget_layout = QVBoxLayout()
main_widget.setLayout(main_widget_layout)
my_toolbar = QToolBar(self)
main_widget.addToolBar(Qt.RightToolBarArea, my_toolbar)
my_toolbar.addWidget(QPushButton("A"))
my_toolbar.addWidget(QPushButton("B"))
my_toolbar.addWidget(QPushButton("C"))
main_widget.addWidget(QPushButton("Hello world"))
main_widget_layout.addStretch(1)
layout.addStretch(1)
window.show()
app.exec_()

Related

How to update QScrollArea size when contents change size?

My QScrollArea does not update its size dinamically when I add a new QPushButton inside it.
I want to add/remove some QPushButton inside a QScrollArea dinamically, but my QScrollArea does not update
its size.
I want my QScrollArea has always a minimum possible size.
With this code:
import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QMainWindow,
QPushButton, QScrollArea, QVBoxLayout)
class MyWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
# Contents
w = QWidget()
lay = QVBoxLayout(w)
lay.addWidget(QPushButton('Button'))
scroll.setWidget(w)
# Controls
button_add = QPushButton('Add')
button_add.clicked.connect(lambda: lay.addWidget(QPushButton('Button')))
button_del = QPushButton('Del')
button_del.clicked.connect(lambda: lay.takeAt(0).widget().deleteLater() if lay.count()>0 else None)
# Main Layout
vlay = QVBoxLayout()
vlay.addWidget(scroll)
vlay.addStretch()
vlay.addWidget(button_add)
vlay.addWidget(button_del)
w = QWidget(self)
w.setLayout(vlay)
self.setCentralWidget(w)
self.resize(200, 300)
if __name__ == '__main__':
app = QApplication([])
mainWin = MyWindow()
mainWin.show()
sys.exit(app.exec_())
I got this view (left) when started and (right) when I add some QPushButtons:
So I have two questions:
How I start my application with QScrollArea with a minimum size?
How QScrollAre can update its size dinamically?
My desirable view is:
And of course, when I add a lot of QPushButtons, a Scrollbar appears.
You can set maximum height for scrollarea equals to widget contents (layout size + margins). This should be done after layout completes it's calculations (for example asyncronously with zero-timer).
import sys
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QScrollArea, QVBoxLayout
class MyWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
# Contents
w = QWidget()
lay = QVBoxLayout(w)
#lay.addWidget(QPushButton('Button'))
scroll.setWidget(w)
def updateSize():
left, top, right, bottom = lay.getContentsMargins()
hint = lay.sizeHint()
scroll.setMaximumHeight(hint.height() + top + bottom + 1)
def addButton():
lay.addWidget(QPushButton('Button'))
QTimer.singleShot(0, updateSize)
def removeButton():
if lay.count() > 0:
lay.takeAt(0).widget().deleteLater()
QTimer.singleShot(0, updateSize)
addButton()
# Controls
button_add = QPushButton('Add')
button_add.clicked.connect(addButton)
button_del = QPushButton('Del')
button_del.clicked.connect(removeButton)
# Main Layout
vlay = QVBoxLayout()
vlay.addWidget(scroll)
vlay.addStretch(1)
vlay.addWidget(button_add)
vlay.addWidget(button_del)
vlay.setStretch(0,1000)
w = QWidget(self)
w.setLayout(vlay)
self.setCentralWidget(w)
self.resize(200, 300)
if __name__ == '__main__':
app = QApplication([])
mainWin = MyWindow()
mainWin.show()
app.exec()

How to keep the shortcuts of a hidden widget in PyQt5?

I have a menu bar with a shortcut associated to it. I want to hide the menu bar but in that case the associated shortcut will be disabled. Here is an example:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QAction
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.InitWindow()
def InitWindow(self):
mainMenu = self.menuBar()
fileMenu = mainMenu.addMenu("&File")
mainMenu.hide() # comment it and the shortcut 'q' will work
quitItem = QAction("Quit", self)
quitItem.setShortcut("Q")
quitItem.triggered.connect(self.close)
fileMenu.addAction(quitItem)
if __name__ == "__main__":
App = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(App.exec())
If you put the line mainMenu.hide() in a comment, i.e. if the menu bar is shown, then the app. will quit with the shortcut 'q'. How could I keep the shortcuts of a hidden widget?
In the app. I want to add full-screen support, and in that case I want to hide the menu bar, but also, I want to keep the shortcuts in full-screen mode.
I found a working solution. The idea is the following: the main window has a shortcut ('q' in the example), and the menu bar also has this shortcut. To avoid conflict, disable the window's shortcut if the menu bar is present. If the menu bar is hidden, enable the window's shortcut.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QShortcut
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.shortcutQuit = QShortcut(QKeySequence("q"), self)
self.shortcutQuit.activated.connect(self.close)
self.shortcutQuit.setEnabled(False) # disable it if the menu bar is visible
self.InitWindow()
def InitWindow(self):
self.mainMenu = self.menuBar()
fileMenu = self.mainMenu.addMenu("&File")
hideItem = QAction("Hide Menu Bar", self)
hideItem.setShortcut("h")
hideItem.triggered.connect(self.my_hide)
quitItem = QAction("Quit", self)
quitItem.setShortcut("Q")
quitItem.triggered.connect(self.close)
fileMenu.addAction(hideItem)
fileMenu.addAction(quitItem)
def my_hide(self):
self.mainMenu.hide()
self.shortcutQuit.setEnabled(True)
if __name__ == "__main__":
App = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(App.exec())

How can I specify seperators using QDockWidget?

I have the following example code:
from PyQt5 import QtWidgets, QtCore, QtGui
import sys
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent=parent)
self.bgcolor = self.palette().color(self.backgroundRole()).name()
self.central = QtWidgets.QTextEdit(self)
self.central.setText('this is the central widget')
self.setCentralWidget(self.central)
self.setDockOptions(self.AnimatedDocks) #prevent tabbing
self.rightDock = QtWidgets.QDockWidget('right dock', self)
self.rightDock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
self.rightDock.setStyleSheet('QDockWidget::title{text-align:left;background:'+self.bgcolor+';}')
self.everywhereDock = QtWidgets.QDockWidget('everywhere dock',self)
self.everywhereDock.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea | QtCore.Qt.TopDockWidgetArea | QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea)
self.everywhereDock.setFeatures(QtWidgets.QDockWidget.DockWidgetFloatable | QtWidgets.QDockWidget.DockWidgetMovable)
self.everywhereDock.setStyleSheet('QDockWidget::title{text-align:left;background:'+self.bgcolor+';}')
self.dockable = QtWidgets.QTextEdit(self.rightDock)
self.dockable.setText('this is dockable only on the right')
self.dockable2 = QtWidgets.QTextEdit(self.everywhereDock)
self.dockable2.setText('this is dockable everywhere, also its not closable')
self.rightDock.setWidget(self.dockable)
self.everywhereDock.setWidget(self.dockable2)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.rightDock)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.everywhereDock)
self.setTabPosition(QtCore.Qt.AllDockWidgetAreas, QtWidgets.QTabWidget.North)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
m = MainWindow()
m.show()
sys.exit(app.exec_())
The question is how to insert icons, where the borders between DockWidgetAreas are draggable, so that the user has a hint, that there is this functionality.
To clarify:
I want an icon between the black arrows:
The QDockWidget supports a "title widget" which is not a separator, but you can add it into every QDockWidget using QDockWidget::setTitleBarWidget(QWidget *widget).
So you can create a generic QWidget to hold this icon using a QHorizontalLayout or something, and put it into the title bar. The default mouse events handled by Qt (such as the drag events) should continue to works normally and you have a customizable title bar.
self.rightDock = QtWidgets.QDockWidget('right dock', self)
self.rightDock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
// add custom title widget
self.rightDock.setTitleBarWidget(self.titleWidget)
// add widget to dock widget
self.rightDock.setWidget(self.dockable)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.rightDock)

PyQt: Adding widgets to scrollarea during the runtime

I'm trying to add new widgets (in the example below I use labels) during the runtime by pressing on a button. Here the example:
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Widget(QWidget):
def __init__(self, parent= None):
super(Widget, self).__init__()
btn_new = QPushButton("Append new label")
self.connect(btn_new, SIGNAL('clicked()'), self.add_new_label)
#Container Widget
self.widget = QWidget()
#Layout of Container Widget
layout = QVBoxLayout(self)
for _ in range(20):
label = QLabel("test")
layout.addWidget(label)
self.widget.setLayout(layout)
#Scroll Area Properties
scroll = QScrollArea()
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setWidgetResizable(False)
scroll.setWidget(self.widget)
#Scroll Area Layer add
vLayout = QVBoxLayout(self)
vLayout.addWidget(btn_new)
vLayout.addWidget(scroll)
self.setLayout(vLayout)
def add_new_label(self):
label = QLabel("new")
self.widget.layout().addWidget(label)
if __name__ == '__main__':
app = QApplication(sys.argv)
dialog = Widget()
dialog.show()
app.exec_()
When I start the application everything looks ok, the list of labels is correctly shown and their size is also correct. But, when I press several times on the button to add new labels, the new ones are added to the list but their size change. All labels of the list go smaller.
How do I fix this error?
The problem is the line:
scroll.setWidgetResizable(False)
which obviously stops the widget resizing when you add more child widgets to it (and so they all get squashed together in the same space).
So reset it to True and add a stretchable space to the bottom of the widget's layout:
layout.addStretch()
self.widget.setLayout(layout)
...
scroll.setWidgetResizable(True)
scroll.setWidget(self.widget)
then insert the new labels before the spacer:
def add_new_label(self):
label = QLabel("new")
layout = self.widget.layout()
layout.insertWidget(layout.count() - 1, label)

Pyside Changing Layouts

I have a Interface that has 4 buttons across the top of the screen, and below that I have QFrame. I want to change the layout in the QFrame based on what button is pressed. For instance, if Button_1 is pressed show a TextEdit Widget, if Button_2 is pressed show a ListViewWidget.
Does anyone have any idea on how this can be done?
Thank You Very Much!
Use a QStackedLayout to store your widgets. Then you can change to the one with setCurrentIndex. Alternatively, you can use a QStackedWidget in place of your QFrame.
A simple example:
import sys
from PySide import QtGui
class Window(QtGui.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.textEdit = QtGui.QTextEdit('Text Edit')
self.listWidget = QtGui.QListWidget()
self.listWidget.addItem('List Widget')
self.label = QtGui.QLabel('Label')
self.stackedLayout = QtGui.QStackedLayout()
self.stackedLayout.addWidget(self.textEdit)
self.stackedLayout.addWidget(self.listWidget)
self.stackedLayout.addWidget(self.label)
self.frame = QtGui.QFrame()
self.frame.setLayout(self.stackedLayout)
self.button1 = QtGui.QPushButton('Text Edit')
self.button1.clicked.connect(lambda: self.stackedLayout.setCurrentIndex(0))
self.button2 = QtGui.QPushButton('List Widget')
self.button2.clicked.connect(lambda: self.stackedLayout.setCurrentIndex(1))
self.button3 = QtGui.QPushButton('Label')
self.button3.clicked.connect(lambda: self.stackedLayout.setCurrentIndex(2))
buttonLayout = QtGui.QHBoxLayout()
buttonLayout.addWidget(self.button1)
buttonLayout.addWidget(self.button2)
buttonLayout.addWidget(self.button3)
layout = QtGui.QVBoxLayout(self)
layout.addLayout(buttonLayout)
layout.addWidget(self.frame)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Something like...
myframe.layout().removeWidget(mywidget)
mywidget = QTextEdit()
myframe.layout().addWidget(myWidget)

Resources