I am struggling to add a side menu to my application.
I have a QMainWindow instance to which I was hoping to add a QDrawer object and achieve an effect similar to this sample.
Unfortunately, it seems that PySide2 only provides QMenu, QTooltip and QDialog widgets which inherit from the Popup class, and QDrawer is nowhere to be found. However, using a Drawer tag in a QML file works just fine. Shouldn't it be possible to also create an instance of QDrawer programmatically?
As another try, I tried to load a Drawer instance from a QML file and attach it to my QMainWindow. Unfortunately I can't quite understand what should I specify as parent, what should I wrap it in, what parameters should I use etc. - any advice would be appreciated (although I would much rather create and configure it programatically).
My goal is to create a QMainWindow with a toolbar, central widget and a QDrawer instance as a side navigation menu (such as in this sample). Can you please share some examples or explain what to do?
One possible solution is to implement a Drawer using Qt Widgets, the main feature is to animate the change of width for example using a QXAnimation, the other task is to set the anchors so that it occupies the necessary height. A simple example is the one shown in the following code:
import os
from PySide2 import QtCore, QtGui, QtWidgets
class Drawer(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedWidth(0)
self.setContentsMargins(0, 0, 0, 0)
# self.setFixedWidth(0)
self._maximum_width = 0
self._animation = QtCore.QPropertyAnimation(self, b"width")
self._animation.setStartValue(0)
self._animation.setDuration(1000)
self._animation.valueChanged.connect(self.setFixedWidth)
self.hide()
#property
def maximum_width(self):
return self._maximum_width
#maximum_width.setter
def maximum_width(self, w):
self._maximum_width = w
self._animation.setEndValue(self.maximum_width)
def open(self):
self._animation.setDirection(QtCore.QAbstractAnimation.Forward)
self._animation.start()
self.show()
def close(self):
self._animation.setDirection(QtCore.QAbstractAnimation.Backward)
self._animation.start()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
self.tool_button = QtWidgets.QToolButton(
checkable=True, iconSize=QtCore.QSize(36, 36)
)
content_widget = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
content_widget.setText("Content")
content_widget.setStyleSheet("background-color: green")
lay = QtWidgets.QVBoxLayout(central_widget)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.tool_button)
lay.addWidget(content_widget)
self.resize(640, 480)
self.drawer = Drawer(self)
self.drawer.move(0, self.tool_button.sizeHint().height())
self.drawer.maximum_width = 200
self.drawer.raise_()
content_lay = QtWidgets.QVBoxLayout()
content_lay.setContentsMargins(0, 0, 0, 0)
label = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
label.setText("Content\nDrawer")
label.setStyleSheet("background-color: red")
content_lay.addWidget(label)
self.drawer.setLayout(content_lay)
self.tool_button.toggled.connect(self.onToggled)
self.onToggled(self.tool_button.isChecked())
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.onCustomContextMenuRequested)
#QtCore.Slot()
def onCustomContextMenuRequested(self):
menu = QtWidgets.QMenu()
quit_action = menu.addAction(self.tr("Close"))
action = menu.exec_(QtGui.QCursor.pos())
if action == quit_action:
self.close()
#QtCore.Slot(bool)
def onToggled(self, checked):
if checked:
self.tool_button.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_MediaStop)
)
self.drawer.open()
else:
self.tool_button.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay)
)
self.drawer.close()
def resizeEvent(self, event):
self.drawer.setFixedHeight(self.height() - self.drawer.pos().y())
super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Related
I am new to pyqt, and I tried to make an application window that contains a list of buttons that are able to toggle a different window. Since I want the number of these buttons to be of a varying quantity, I created a list of QPushButton elements for iterating over them, creating as many as defined by the length of the list, nevertheless I noticed a very weird behavior :
The following code ...
import sys
from random import randint
from PyQt5 import QtWidgets
class AnotherWindow(QtWidgets.QWidget):
"""
This "window" is a QWidget. If it has no parent,
it will appear as a free-floating window.
"""
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout()
self.label = QtWidgets.QLabel("Another Window % d" % randint(0, 100))
layout.addWidget(self.label)
self.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self,windows):
super().__init__()
self.windows=[]
self.buttons=[]
l=QtWidgets.QVBoxLayout()
for i in range(len(windows)):
window=AnotherWindow()
self.windows.append(window)
button=QtWidgets.QPushButton(f'window {windows[i]}')
print(i," ",button)
self.buttons.append(button)
self.buttons[i].clicked.connect(self.toggle_window,i)
l.addWidget(self.buttons[i])
w = QtWidgets.QWidget()
w.setLayout(l)
self.setCentralWidget(w)
print(len(self.windows))
def toggle_window(self,i):
if self.windows[i].isVisible():
self.windows[i].hide()
else:
self.windows[i].show()
if __name__=="__main__":
app = QtWidgets.QApplication(sys.argv)
windows=[0,1,2,3]
windows=[str(i) for i in windows]
print(windows)
w = MainWindow(windows)
w.show()
app.exec()
produced the following error but only when the 4rth button (window 3) is pressed.
Qt: Dead lock detected while activating a BlockingQueuedConnection: Sender is QPushButton( ... ), receiver is PyQtSlotProxy( ... )
In effort to validate the code, I tried to narrow the list into a linear declaration of a static number of QPushButton instances, indicating that the issue occurs, only when I try to put the buttons on a list. For instance, the following script does not present any similar unpredictable behavior:
import sys
from random import randint
from PyQt5 import QtWidgets
class AnotherWindow(QtWidgets.QWidget):
"""
This "window" is a QWidget. If it has no parent,
it will appear as a free-floating window.
"""
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout()
self.label = QtWidgets.QLabel("Another Window % d" % randint(0, 100))
layout.addWidget(self.label)
self.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.window0 = AnotherWindow()
self.window1 = AnotherWindow()
self.window2 = AnotherWindow()
self.window3 = AnotherWindow()
l = QtWidgets.QVBoxLayout()
button0 = QtWidgets.QPushButton("window 0")
button0.clicked.connect(self.toggle_window0)
l.addWidget(button0)
button1 = QtWidgets.QPushButton("window 1")
button1.clicked.connect(self.toggle_window1)
l.addWidget(button1)
button2 = QtWidgets.QPushButton("window 2")
button2.clicked.connect(self.toggle_window2)
l.addWidget(button2)
button3 = QtWidgets.QPushButton("window 3")
button3.clicked.connect(self.toggle_window3)
l.addWidget(button3)
w = QtWidgets.QWidget()
w.setLayout(l)
self.setCentralWidget(w)
def toggle_window0(self, checked):
if self.window0.isVisible():
self.window0.hide()
else:
self.window0.show()
def toggle_window1(self):
if self.window1.isVisible():
self.window1.hide()
else:
self.window1.show()
def toggle_window2(self):
if self.window2.isVisible():
self.window2.hide()
else:
self.window2.show()
def toggle_window3(self, checked):
if self.window3.isVisible():
self.window3.hide()
else:
self.window3.show()
if __name__=="__main__":
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
To test it further, I extended the list to a list of random lengths (more than 10), where I reassured that the issue persist for specific indexes each time. For example if I create 20 buttons using the first approach, the same bug appears for - the 4rth, the 12fth and the last index exclusively - but not for the rest of them. I even tested it on a different machine. Having also searched in forums, I could not find a solution.
Do I do anything completely wrong here? Does anyone understands better to indicate why is this happening?
I kindly thank you in advance!
Environment: Ubuntu 22.04
Pyqt version : 1.9 (under conda)
Your problem is the following:
self.buttons[i].clicked.connect(self.toggle_window,i)
You are passing i as second argument to connect and expect the toggle_window function to be called with this argument. This is not happening. In toggle_window, i will always be False. See musicamente's comment regarding what this second argument to connect does.
What you should do instead is connect the button click to a function of your window object. From there, you can of course do a callback to a function of your main window as illustrated below:
import sys
from random import randint
from PyQt5 import QtWidgets
class AnotherWindow(QtWidgets.QWidget):
def __init__(self, parent, i):
super().__init__()
self.parent = parent
self.i = i
layout = QtWidgets.QVBoxLayout()
self.label = QtWidgets.QLabel("Another Window {}".format(i))
layout.addWidget(self.label)
self.setLayout(layout)
def toggle(self):
print("Toggling windows {}".format(self.i))
if self.isVisible():
self.hide()
self.parent.window_toggled(self.i, False)
else:
self.show()
self.parent.window_toggled(self.i, True)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, windows):
super().__init__()
self.windows=[]
self.buttons=[]
l=QtWidgets.QVBoxLayout()
for i,title in enumerate(windows):
window=AnotherWindow(self, i)
self.windows.append(window)
button=QtWidgets.QPushButton(title)
button.clicked.connect(window.toggle)
l.addWidget(button)
self.buttons.append(button)
w = QtWidgets.QWidget()
w.setLayout(l)
self.setCentralWidget(w)
def window_toggled(self, i, visible):
print("Window {} is now {}".format(i, "visible" if visible else "hidden"))
if __name__=="__main__":
app = QtWidgets.QApplication(sys.argv)
windows = ["window {}".format(i) for i in range(12)]
w = MainWindow(windows)
w.show()
app.exec()
There have been similar questions asked about overriding the QCompleter popup position but i'll still not found a working solution. I simply want to move the popup down around 5px (I have some specific styling requirements)
I've tried subclassing a QListView and using that as my popup using setPopup(). I then override the showEvent and move the popup down in Y. I also do this on the resizeEvent since I believe this is triggered when items are filtered and the popup resizes. However this doesn't work.. I then used a singleshot timer to trigger the move after 1ms. This does kind of work but it seems quite inconsistent - the first time it shows is different to subsequent times or resizing.
Below is my latest attempt (trying to hack it by counting the number of popups..), hopefully someone can show me what i'm doing wrong or a better solution
import sys
import os
from PySide2 import QtCore, QtWidgets, QtGui
class QPopup(QtWidgets.QListView):
def __init__(self, parent=None):
super(QPopup, self).__init__(parent)
self.popups = 0
def offset(self):
y = 3 if self.popups < 2 else 7
print('y: {}'.format(y))
self.move(self.pos().x(), self.pos().y() + y)
self.popups += 1
def showEvent(self, event):
print('show')
# self.offset()
QtCore.QTimer.singleShot(1, self.offset)
def resizeEvent(self, event):
print('resize')
# self.offset()
QtCore.QTimer.singleShot(1, self.offset)
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MyDialog, self).__init__(parent)
self.create_widgets()
self.create_layout()
self.create_connections()
def create_widgets(self):
self.le = QtWidgets.QLineEdit('')
self.completer = QtWidgets.QCompleter(self)
self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
self.completer.setMaxVisibleItems(10)
self.completer.setFilterMode(QtCore.Qt.MatchContains)
self.completer.setPopup(QPopup())
popup = QPopup(self)
self.completer.setPopup(popup)
self.model = QtCore.QStringListModel()
self.completer.setModel(self.model)
self.le.setCompleter(self.completer)
self.completer.model().setStringList(['one','two','three'])
def create_layout(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.le)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show() # Show the UI
sys.exit(app.exec_())
One solution could be to make a subclass of QLineEdit and override keyPressEvent to display the popup with an offset:
PySide2.QtWidgets.QCompleter.complete([rect=QRect()])
For PopupCompletion and QCompletion::UnfilteredPopupCompletion modes, calling this function displays the popup displaying the current completions. By default, if rect is not specified, the popup is displayed on the bottom of the widget() . If rect is specified the popup is displayed on the left edge of the rectangle.
see doc.qt.io -> QCompleter.complete.
Complete, self-contained example
The rect is calculated based on the y-position of the cursor rect. The height of the popup window is not changed. The width is adjusted to the width of the ZLineEdit widget.
rect = QtCore.QRect(0,
self.cursorRect().y() + 4,
self.width(),
self.completer().widget().height())
Your code, slightly modified using the points mentioned above, could look like this:
import sys
from PySide2 import QtCore, QtWidgets
from PySide2.QtWidgets import QLineEdit, QDialog, QCompleter
class ZLineEdit(QLineEdit):
def __init__(self, string, parent=None):
super().__init__(string, parent)
def keyPressEvent(self, event):
super().keyPressEvent(event)
if len(self.text()) > 0:
rect = QtCore.QRect(0,
self.cursorRect().y() + 4,
self.width(),
self.completer().widget().height())
self.completer().complete(rect)
class MyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.le = ZLineEdit('')
autoList = ['one', 'two', 'three']
self.completer = QCompleter(autoList, self)
self.setup_widgets()
self.create_layout()
self.create_connections()
def setup_widgets(self):
self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
self.completer.setMaxVisibleItems(10)
self.completer.setFilterMode(QtCore.Qt.MatchContains)
self.le.setCompleter(self.completer)
def create_layout(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.le)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show()
sys.exit(app.exec_())
Test
On the left side you see the default behavior. On the right side the popup is moved down 4px:
I'm working on an opensource markdown supported minimal note taking application for Windows/Linux. I'm trying to remove the title bar and add my own buttons. I want something like, a title bar with only two custom buttons as shown in the figure
Currently I have this:
I've tried modifying the window flags:
With not window flags, the window is both re-sizable and movable. But no custom buttons.
Using self.setWindowFlags(QtCore.Qt.FramelessWindowHint), the window has no borders, but cant move or resize the window
Using self.setWindowFlags(QtCore.Qt.CustomizeWindowHint), the window is resizable but cannot move and also cant get rid of the white part at the top of the window.
Any help appreciated. You can find the project on GitHub here.
Thanks..
This is my python code:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
The UI file is created in the following hierarchy
Here are the steps you just gotta follow:
Have your MainWindow, be it a QMainWindow, or QWidget, or whatever [widget] you want to inherit.
Set its flag, self.setWindowFlags(Qt.FramelessWindowHint)
Implement your own moving around.
Implement your own buttons (close, max, min)
Implement your own resize.
Here is a small example with move around, and buttons implemented. You should still have to implement the resize using the same logic.
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Here are some tips:
Option 1:
Have a QGridLayout with widget in each corner and side(e.g. left, top-left, menubar, top-right, right, bottom-right, bottom and bottom left)
With the approach (1) you would know when you are clicking in each border, you just got to define each one size and add each one on their place.
When you click on each one treat them in their respective ways, for example, if you click in the left one and drag to the left, you gotta resize it larger and at the same time move it to the left so it will appear to be stopped at the right place and grow width.
Apply this reasoning to each edge, each one behaving in the way it has to.
Option 2:
Instead of having a QGridLayout you can detect in which place you are clicking by the click pos.
Verify if the x of the click is smaller than the x of the moving pos to know if it's moving left or right and where it's being clicked.
The calculation is made in the same way of the Option1
Option 3:
Probably there are other ways, but those are the ones I just thought of. For example using the CustomizeWindowHint you said you are able to resize, so you just would have to implement what I gave you as example. BEAUTIFUL!
Tips:
Be careful with the localPos(inside own widget), globalPos(related to your screen). For example: If you click in the very left of your left widget its 'x' will be zero, if you click in the very left of the middle(content)it will be also zero, although if you mapToGlobal you will having different values according to the pos of the screen.
Pay attention when resizing, or moving, when you have to add width or subtract, or just move, or both, I'd recommend you to draw on a paper and figure out how the logic of resizing works before implementing it out of blue.
GOOD LUCK :D
While the accepted answer can be considered valid, it has some issues.
using setGeometry() is not appropriate (and the reason for using it was wrong) since it doesn't consider possible frame margins set by the style;
the position computation is unnecessarily complex;
resizing the title bar to the total width is wrong, since it doesn't consider the buttons and can also cause recursion problems in certain situations (like not setting the minimum size of the main window); also, if the title is too big, it makes impossible to resize the main window;
buttons should not accept focus;
setting a layout creates a restraint for the "main widget" or layout, so the title should not be added, but the contents margins of the widget should be used instead;
I revised the code to provide a better base for the main window, simplify the moving code, and add other features like the Qt windowTitle() property support, standard QStyle icons for buttons (instead of text), and proper maximize/normal button icons. Note that the title label is not added to the layout.
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
This is for the people who are going to implement custom title bar in PyQt6 or PySide6
The below changes should be done in the answer given by #musicamante
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# self.clickPos = event.windowPos().toPoint()
self.clickPos = event.scenePosition().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
# self.window().move(event.globalPos() - self.clickPos)
self.window().move(event.globalPosition().toPoint() - self.clickPos)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
# sys.exit(app.exec_())
sys.exit(app.exec())
References:
QMouseEvent.globalPosition(),
QMouseEvent.scenePosition()
This method of moving Windows with Custom Widget doesn't work with WAYLAND. If anybody has a solution for that please post it here for future reference
Working functions for WAYLAND and PyQT6/PySide6 :
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._move()
return super().mousePressEvent(event)
def _move(self):
window = self.window().windowHandle()
window.startSystemMove()
Please check.
I am trying to make a GUI that will display (and eventually let the user build) circuits. Below is a rough sketch of what the application is supposed to look like.
The bottom panel (currently a simple QToolBar) should be of constant height but span the width of the application and the side panels (IOPanels in the below code) should have a constant width and span the height of the application.
The main part of the application (Canvas, which is currently a QWidget with an overriden paintEvent method, but might eventually become a QGraphicsScene with a QGraphicsView or at least something scrollable) should then fill the remaining space.
This is my current code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt, QSize
class MainWindow(QMainWindow):
def __init__(self, *args):
super().__init__(*args)
self._wire_ys = None
self._init_ui()
self.update_wire_ys()
def update_wire_ys(self):
self._wire_ys = [(i + 0.5) * self.panel.height() / 4 for i in range(4)]
self.input.update_field_positions()
self.output.update_field_positions()
def wire_ys(self):
return self._wire_ys
def _init_ui(self):
self.panel = QWidget(self)
self.canvas = Canvas(self, self.panel)
self.input = IOPanel(self, self.panel)
self.output = IOPanel(self, self.panel)
hbox = QHBoxLayout(self.panel)
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
hbox.addWidget(self.input, 0, Qt.AlignLeft)
hbox.addWidget(self.output, 0, Qt.AlignRight)
self.setCentralWidget(self.panel)
self.addToolBar(Qt.BottomToolBarArea, self._create_run_panel())
self.reset_placement()
def _create_run_panel(self):
# some other code to create the toolbar
return QToolBar(self)
def reset_placement(self):
g = QDesktopWidget().availableGeometry()
self.resize(0.4 * g.width(), 0.4 * g.height())
self.move(g.center().x() - self.width() / 2, g.center().y() - self.height() / 2)
def resizeEvent(self, *args, **kwargs):
super().resizeEvent(*args, **kwargs)
self.update_wire_ys()
class IOPanel(QWidget):
def __init__(self, main_window, *args):
super().__init__(*args)
self.main = main_window
self.io = [Field(self) for _ in range(4)]
def update_field_positions(self):
wire_ys = self.main.wire_ys()
for i in range(len(wire_ys)):
field = self.io[i]
field.move(self.width() - field.width() - 10, wire_ys[i] - field.height() / 2)
def sizeHint(self):
return QSize(40, self.main.height())
class Field(QLabel):
def __init__(self, *args):
super().__init__(*args)
self.setAlignment(Qt.AlignCenter)
self.setText(str(0))
self.resize(20, 20)
# This class is actually defined in another module and imported
class Canvas(QWidget):
def __init__(self, main_window, *args):
super().__init__(*args)
self.main = main_window
def paintEvent(self, e):
print("ASFD")
qp = QPainter()
qp.begin(self)
self._draw(qp)
qp.end()
def _draw(self, qp):
# Draw stuff
qp.drawLine(0, 0, 1, 1)
# __main__.py
def main():
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Running that code gives me the following:
Here I have coloured the components to better see them using code like this in their construction:
p = self.palette()
p.setColor(self.backgroundRole(), Qt.blue)
self.setPalette(p)
self.setAutoFillBackground(True)
Green is the central panel (MainWindow.panel), blue are the IOPanels, the Fields are supposed to be red, and the Canvas is supposed to be white.
Ignore the bottom toolbar, it's some extra code I didn't include above (to keep it as minimal and relevant as possible), but it does no resizing of anything and no layout management except for its own child QWidget. In fact, including the painting code in my above minimal example gave a similar result with thinner bottom toolbar without the Run button. I'm just including the toolbar here to show its expected behaviour (as the toolbar is working correctly) in the general layout.
This result has several problems.
Problem 1
The Fields do not show up, initially. However, they do show up (and are appropriately placed within their respective panels) once I resize the main window. Why is this? The only thing the main window's resizeEvent does is update_wire_ys and update_field_positions, and those are performed by the main window's __init__ as well.
Problem 2
The IOPanels are not properly aligned. The first one should be on the left side of the central panel. Changing the order of adding them fixes this, as so:
hbox.addWidget(self.input, 0, Qt.AlignLeft)
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
hbox.addWidget(self.output, 0, Qt.AlignRight)
However, shouldn't the Qt.AlignX already do this, regardless of the order they're added in? What if I later on wanted to add another panel to the left side, would I have to remove all the components, add the new panel and then re-add them?
Problem 3
The IOPanels are not properly sized. They need to span the entire height of the central panel and touch the left/right edge of the central panel. I'm not sure if this is an issue with the layout or my colouring of the panels. What am I doing wrong?
Problem 4
The Canvas does not show up at all and in fact its paintEvent is never called ("ASFD" never gets printed to the console). I have not overridden its sizeHint, because I want the central panel's layout to appropriately size the Canvas by itself. I was hoping the stretch factor of 1 when adding the component would accomplish that.
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
How do I get the canvas to actually show up and fill all the remaining space on the central panel?
This is the typical spaghetti code, where many elements are tangled, which is usually difficult to test, I have found many problems such as sizeEvent is only called when the layout containing the widget is called, another example is when you use the Function update_field_positions and update_wire_ys that handle each other object.
In this answer I will propose a simpler implementation:
IOPanel clas must contain a QVBoxLayout that handles the changes of image size.
In the MainWindow class we will use the layouts with the alignments but you must add them in order.
lay.addWidget(self.input, 0, Qt.AlignLeft)
lay.addWidget(self.canvas, 0, Qt.AlignCenter)
lay.addWidget(self.output, 0, Qt.AlignRight)
To place a minimum width for IOPanel we use QSizePolicy() and setMinimumSize()
Complete code:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Field(QLabel):
def __init__(self, text="0", parent=None):
super(Field, self).__init__(parent=parent)
self.setAlignment(Qt.AlignCenter)
self.setText(text)
class IOPanel(QWidget):
numbers_of_fields = 4
def __init__(self, parent=None):
super(IOPanel, self).__init__(parent=None)
lay = QVBoxLayout(self)
for _ in range(self.numbers_of_fields):
w = Field()
lay.addWidget(w)
self.setMinimumSize(QSize(40, 0))
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.setSizePolicy(sizePolicy)
class Panel(QWidget):
def __init__(self, parent=None):
super(Panel, self).__init__(parent=None)
lay = QHBoxLayout(self)
self.input = IOPanel()
self.output = IOPanel()
self.canvas = QWidget()
lay.addWidget(self.input, 0, Qt.AlignLeft)
lay.addWidget(self.canvas, 0, Qt.AlignCenter)
lay.addWidget(self.output, 0, Qt.AlignRight)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=parent)
self.initUi()
self.reset_placement()
def initUi(self):
panel = Panel(self)
self.setCentralWidget(panel)
self.addToolBar(Qt.BottomToolBarArea, QToolBar(self))
def reset_placement(self):
g = QDesktopWidget().availableGeometry()
self.resize(0.4 * g.width(), 0.4 * g.height())
self.move(g.center().x() - self.width() / 2, g.center().y() - self.height() / 2)
def main():
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Screenshot:
Why does my application crash when i run the function setup_controls() twice.
Am I missing a 'parent/self' somewhere that is critical in the design?
import sys
from PySide import QtGui, QtCore
class QCategoryButton(QtGui.QPushButton):
def __init__(self, Text, treeitem, parent=None):
super(QCategoryButton, self).__init__(Text, parent)
self.treeitem = treeitem
def mousePressEvent(self, event):
mouse_button = event.button()
if mouse_button == QtCore.Qt.LeftButton:
self.treeitem.setExpanded(not self.treeitem.isExpanded())
class Example(QtGui.QWidget):
def __init__(self,):
super(Example, self).__init__()
self.initUI()
def initUI(self):
# formatting
self.resize(300, 300)
self.setWindowTitle("Example")
# widgets
self.ui_treeWidget = QtGui.QTreeWidget()
self.ui_treeWidget.setRootIsDecorated(False)
self.ui_treeWidget.setHeaderHidden(True)
self.ui_treeWidget.setIndentation(0)
self.setup_controls()
# self.setup_controls()
# layout
self.mainLayout = QtGui.QGridLayout(self)
self.mainLayout.addWidget(self.ui_treeWidget)
self.show()
def setup_controls(self):
# Add Category
pCategory = QtGui.QTreeWidgetItem()
self.ui_treeWidget.addTopLevelItem(pCategory)
self.ui_toggler = QCategoryButton('Settings', pCategory)
self.ui_treeWidget.setItemWidget(pCategory, 0, self.ui_toggler)
pFrame = QtGui.QFrame(self.ui_treeWidget)
pLayout = QtGui.QVBoxLayout(pFrame)
self.ui_ctrl = QtGui.QPushButton('Great')
self.ui_ctrlb = QtGui.QPushButton('Cool')
pLayout.addWidget(self.ui_ctrl)
pLayout.addWidget(self.ui_ctrlb)
pContainer = QtGui.QTreeWidgetItem()
pContainer.setDisabled(False)
pCategory.addChild(pContainer)
self.ui_treeWidget.setItemWidget(pContainer, 0, pFrame)
# Main
# ------------------------------------------------------------------------------
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
The setItemWidget method takes ownership of the widget that is passed to it. If you don't keep a reference it, it could get garbage-collected by Python. But of course Qt knows nothing about Python, so when it subsequently tries to access the widget that is no longer there ... boom!
This is the problematic line:
self.ui_toggler = QCategoryButton('Settings', pCategory)
On the second call, the previous widget stored in self.ui_toggler will get deleted, because there is no other reference held for it (on the Python side). So instead you should do this:
ui_toggler = QCategoryButton('Settings', pCategory, self)
self.ui_treeWidget.setItemWidget(pCategory, 0, ui_toggler)