Create padding when fixed aspect ratio QWidget cannot fill entire window - python

A custom widget (class name MyLabel, inherits QLabel) has a fixed aspect ratio 16:9.
When I resize my window, the label is top-left aligned unless the window happens to be 16:9, in which case it fills the window perfectly.
How do I get the label to be centered? I have looked at size policies, alignments, using spaceitems and stretch, but I cannot seem to get it working as desired.
Here is a minimal reproducible example:
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import QSize, Qt
from PyQt5.Qt import QVBoxLayout, QWidget
class MyLabel(QLabel):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen") # Just for visibility
def resizeEvent(self, event):
# Size of 16:9 and scale it to the new size maintaining aspect ratio.
new_size = QSize(16, 9)
new_size.scale(event.size(), Qt.KeepAspectRatio)
self.resize(new_size)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__(None)
# Main widget and layout, and set as centralWidget
self.main_layout = QVBoxLayout()
self.main_widget = QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
# Add button to main_layout
label = MyLabel("Hello World")
self.main_layout.addWidget(label)
self.show()
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
Examples of desired outcome:
Examples of actual outcome:

Qt unfortunately doesn't provide a straight forward solution for widgets that require a fixed aspect ratio.
There are some traces in old documentation, but the main problem is that:
all functions related to aspect ratio (hasHeightForWidth() etc) for widgets, layouts and size policies are only considered for the size hint, so no constraint is available if the widget is manually resized by the layout;
as the documentation reports changing the geometry of a widget within the moveEvent() or resizeEvent() might lead to infinite recursion;
it's not possible to (correctly) control the size growth or shrinking while keeping aspect ratio;
For the sake of completeness, here's a partial solution to this issue, but be aware that QLabel is a very peculiar widget that has some constraints related to its text representation (most importantly, with rich text and/or word wrap).
class MyLabel(QLabel):
lastRect = None
isResizing = False
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen")
self.setScaledContents(True)
def restoreRatio(self, lastRect=None):
if self.isResizing:
return
rect = QRect(QPoint(),
QSize(16, 9).scaled(self.size(), Qt.KeepAspectRatio))
if not lastRect:
lastRect = self.geometry()
rect.moveCenter(lastRect.center())
if rect != lastRect:
self.isResizing = True
self.setGeometry(rect)
self.isResizing = False
self.lastRect = None
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
if self.pixmap():
return width * self.pixmap().height() / self.pixmap().width()
return width * 9 / 16
def sizeHint(self):
if self.pixmap():
return self.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.lastRect = self.geometry()
def resizeEvent(self, event):
self.restoreRatio(self.lastRect)
Since the purpose is to display an image, another possibility is to manually paint everything on your own, for which you don't need a QLabel at all, and you can just override the paintEvent of a QWidget, but for performance purposes it could be slightly better to use a container widget with a child QLabel: this would theoretically make things a bit faster, as all the computation is completely done in Qt:
class ParentedLabel(QWidget):
def __init__(self, pixmap=None):
super().__init__()
self.child = QLabel(self, scaledContents=True)
if pixmap:
self.child.setPixmap(pixmap)
def setPixmap(self, pixmap):
self.child.setPixmap(pixmap)
self.updateGeometry()
def updateChild(self):
if self.child.pixmap():
r = self.child.pixmap().rect()
size = self.child.pixmap().size().scaled(
self.size(), Qt.KeepAspectRatio)
r = QRect(QPoint(), size)
r.moveCenter(self.rect().center())
self.child.setGeometry(r)
def hasHeightForWidth(self):
return bool(self.child.pixmap())
def heightForWidth(self, width):
return width * self.child.pixmap().height() / self.child.pixmap().width()
def sizeHint(self):
if self.child.pixmap():
return self.child.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.updateChild()
def resizeEvent(self, event):
self.updateChild()
Finally, another possibility is to use a QGraphicsView, which is probably the faster approach of all, with a small drawback: the image shown based on the given size hint will probably be slightly smaller (a couple of pixels) than the original, with the result that it will seem a bit "out of focus" due to the resizing.
class ViewLabel(QGraphicsView):
def __init__(self, pixmap=None):
super().__init__()
self.setStyleSheet('ViewLabel { border: 0px solid none; }')
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scene = QGraphicsScene()
self.setScene(scene)
self.pixmapItem = QGraphicsPixmapItem(pixmap)
self.pixmapItem.setTransformationMode(Qt.SmoothTransformation)
scene.addItem(self.pixmapItem)
def setPixmap(self, pixmap):
self.pixmapItem.setPixmap(pixmap)
self.updateGeometry()
self.updateScene()
def updateScene(self):
self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)
def hasHeightForWidth(self):
return not bool(self.pixmapItem.pixmap().isNull())
def heightForWidth(self, width):
return width * self.pixmapItem.pixmap().height() / self.pixmapItem.pixmap().width()
def sizeHint(self):
if not self.pixmapItem.pixmap().isNull():
return self.pixmapItem.pixmap().size()
return QSize(160, 90)
def resizeEvent(self, event):
self.updateScene()

Related

QLabel setMinimumHeight After Custom WordWrap Qt.TextWrapAnywhere PyQt5 ( Full responsive With/Without Emoji )

I want Qt.TextWrapAnywhere for my QLabel in a Layout.
I followed This instruction.My code is also same to give a minimal code
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle
class SuperQLabel(QLabel):
def __init__(self, *args, **kwargs):
super(SuperQLabel, self).__init__(*args, **kwargs)
self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
self.isTextLabel = True
self.align = None
def paintEvent(self, event):
opt = QStyleOption()
opt.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
self.style().drawItemText(painter, self.rect(),
self.textalignment, self.palette(), True, self.text())
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.resize(100, 200)
self.label = QLabel()
self.label.setWordWrap(True)
self.label.setText("11111111111111111111\n2222222211111111")
self.slabel = SuperQLabel()
self.slabel.setMinimumWidth(10)
self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
self.centralwidget = QWidget()
self.setCentralWidget(self.centralwidget)
self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.label)
self.mainlayout.addWidget(self.slabel)
self.centralwidget.setLayout(self.mainlayout)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I changed little bit that code self.slabel.setMinimumWidth(10) otherwise resizing Label according to width wont work.
It is perfectly wrapping the text according to width.But the Problem is when height is considered self.label = QLabel() Normal QLabel auto adjust height according to content with layout.
For example if i add one \n with text that means Qlabel must show 2 lines.
But with this new Custom Label e.g.self.slabel = SuperQLabel() wrapping is good as long as there is space for height in layout.
I think i have to use setminimumHeight() but dont know how to get proper height after custom wrapping.
As long as the label is shown in a scroll area (which will not create issues with the top level layout), a better solution is to use a QTextEdit subclass, with the following configuration:
readOnly must be True;
scroll bars are disabled;
the vertical size policy must be Preferred (and not Expanding);
both minimumSizeHint() and sizeHint() should use the internal QTextDocument to return a proper height, with a minimum default width;
any change in size or contents must trigger updateGeometry() so that the parent layout will know that the hint has changed and geometries could be computed again;
the hint must include possible decorations of the scroll area (which is a QFrame);
This allows avoiding the paintEvent() override, and provide a better and easier implementation of the size mechanism due to the features provided by QTextDocument, while minimizing the possibility of recursion to an acceptable level.
class WrapLabel(QtWidgets.QTextEdit):
def __init__(self, text=''):
super().__init__(text)
self.setStyleSheet('''
WrapLabel {
border: 1px outset palette(dark);
border-radius: 8px;
background: palette(light);
}
''')
self.setReadOnly(True)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Maximum)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.textChanged.connect(self.updateGeometry)
def minimumSizeHint(self):
doc = self.document().clone()
doc.setTextWidth(self.viewport().width())
height = doc.size().height()
height += self.frameWidth() * 2
return QtCore.QSize(50, height)
def sizeHint(self):
return self.minimumSizeHint()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateGeometry()
class ChatTest(QtWidgets.QScrollArea):
def __init__(self):
super().__init__()
self.messages = []
container = QtWidgets.QWidget()
self.setWidget(container)
self.setWidgetResizable(True)
layout = QtWidgets.QVBoxLayout(container)
layout.addStretch()
self.resize(480, 360)
for i in range(1, 11):
QtCore.QTimer.singleShot(1000 * i, lambda:
self.addMessage('1' * randrange(100, 250)))
def addMessage(self, text):
self.widget().layout().addWidget(WrapLabel(text))
QtCore.QTimer.singleShot(0, self.scrollToBottom)
def scrollToBottom(self):
QtWidgets.QApplication.processEvents()
self.verticalScrollBar().setValue(
self.verticalScrollBar().maximum())
Update: HTML and QTextDocument
When using setHtml() and setDocument(), the source could have pre formatted text that doesn't allow wrapping. To avoid that, it's necessary to iterate through all QTextBlocks of the document, get their QTextBlockFormat, check the nonBreakableLines() property and eventually set it to False and set the format back with a QTextCursor.
class WrapLabel(QtWidgets.QTextEdit):
def __init__(self, text=None):
super().__init__()
if isinstance(text, str):
if Qt.mightBeRichText(text):
self.setHtml(text)
else:
self.setPlainText(text)
elif isinstance(text, QtGui.QTextDocument):
self.setDocument(text)
# ...
def setHtml(self, html):
doc = QtGui.QTextDocument()
doc.setHtml(html)
self.setDocument(doc)
def setDocument(self, doc):
doc = doc.clone()
tb = doc.begin() # start a QTextBlock iterator
while tb.isValid():
fmt = tb.blockFormat()
if fmt.nonBreakableLines():
fmt.setNonBreakableLines(False)
# create a QTextCursor for the current text block,
# then set the updated format to override the wrap
tc = QtGui.QTextCursor(tb)
tc.setBlockFormat(fmt)
tb = tb.next()
super().setDocument(doc)
Be aware, though, that this could not be enough whenever objects with predefined or minimum width are used: images and tables. The result will be that if the object is larger than the available space, it will be cropped on its right (or left for RightToLeft text layouts).
After Some Research,I successfully fixed it.
There is a trick
This is Full Responsive With/Without Emoji😅
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter,QFontMetrics,QFont
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle
import math
class SuperQLabel(QLabel):
def __init__(self, *args, **kwargs):
super(SuperQLabel, self).__init__(*args, **kwargs)
self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
self.isTextLabel = True
self.align = None
def paintEvent(self, event):
opt = QStyleOption()
opt.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
self.style().drawItemText(painter, self.rect(),
self.textalignment, self.palette(), True, self.text())
fm=QFontMetrics(self.font())
#To get unicode in Text if using Emoji(Optional)
string_unicode = self.text().encode("unicode_escape").decode()
##To remove emoji/unicode from text while calculating
string_encode = self.text().encode("ascii", "ignore")
string_decode = string_encode.decode()
#If Unicode/Emoji is Used
if string_unicode.count("\\U0001") > 0:
height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()+1
# +1 is varrying according to Different font .SO set different value and test.
else:
height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()
row=math.ceil(fm.horizontalAdvance(self.text())/self.width())
self.setMinimumHeight(row*height)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.resize(100, 200)
self.label = QLabel()
self.label.setWordWrap(True)
self.label.setStyleSheet("background:red;")
self.label.setText("11111111111111111111\n2222222211111111")
self.emoji_font = QFont("Segoe UI Emoji",15,0,False)
self.emoji_font.setBold(True)
self.slabel = SuperQLabel()
self.slabel.setMinimumWidth(10)
self.slabel.setStyleSheet("background:green;")
self.slabel.setFont(self.emoji_font)
########### Plain Text ######################
# self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
#################### Or Using Emoji ############
self.slabel.setText("111111111111111111😉111ABCDDWAEQQ111111111111😅1111111111😉111111wqewqgdfgdfhyhtyhy1111111😅111111111111😉1111111111111111111")
self.centralwidget = QWidget()
self.setCentralWidget(self.centralwidget)
self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.label)
self.mainlayout.addWidget(self.slabel)
self.centralwidget.setLayout(self.mainlayout)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

Overriding QCompleter popup position

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:

How to resize square children widgets after parent resize in Qt5?

I want to do board with square widgets. When I run code it creates nice board but after resize it become looks ugly. I am trying resize it with resize Event but it exists (probably some errors). I have no idea how to resize children after resize of parent.
Children widgets must be squares so it is also problem since I can not use auto expand. Maybe it is simple problem but I can not find solution. I spend hours testing different ideas but it now works as it should.
This what I want resize (click maximize):
After maximize it looks ugly (I should change children widget but on what event (I think on resizeEvent but it is not works) and how (set from parent or children cause program exit).
This is my minimize code:
import logging
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QFont, QPaintEvent, QPainter
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout
class Application(QApplication):
pass
class Board(QWidget):
def square_size(self):
size = self.size()
min_size = min(size.height(), size.width())
min_size_1_8 = min_size // 8
square_size = QSize(min_size_1_8, min_size_1_8)
logging.debug(square_size)
return square_size
def __init__(self, parent=None):
super().__init__(parent=parent)
square_size = self.square_size()
grid = QGridLayout()
grid.setSpacing(0)
squares = []
for x in range(8):
for y in range(8):
square = Square(self, (x + y - 1) % 2)
squares.append(squares)
square.setFixedSize(square_size)
grid.addWidget(square, x, y)
self.squares = squares
self.setLayout(grid)
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
# how to resize children?
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
class Square(QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter()
painter.begin(self)
painter.fillRect(self.rect(), self.color)
painter.end()
def main():
logging.basicConfig(level=logging.DEBUG)
app = Application(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
default_font = QFont()
default_font.setPointSize(12)
app.setFont(default_font)
board = Board()
board.setWindowTitle('Board')
# ugly look
# chessboard.showMaximized()
# looks nize but resize not works
board.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
How should I do resize of square children to avoid holes?
2nd try - improved code but still I have not idea how to resize children
Some new idea with centering it works better (no gaps now) but still I do not know how to resize children (without crash).
After show():
Too wide (it keeps proportions):
Too tall (it keeps proportions):
Larger (it keeps proportions but children is not scaled to free space - I do not know how to resize children still?):
Improved code:
import logging
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QFont, QPaintEvent, QPainter
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout
class Application(QApplication):
pass
class Board(QWidget):
def square_size(self):
size = self.size()
min_size = min(size.height(), size.width())
min_size_1_8 = min_size // 8
square_size = QSize(min_size_1_8, min_size_1_8)
logging.debug(square_size)
return square_size
def __init__(self, parent=None):
super().__init__(parent=parent)
square_size = self.square_size()
vertical = QVBoxLayout()
horizontal = QHBoxLayout()
grid = QGridLayout()
grid.setSpacing(0)
squares = []
for x in range(8):
for y in range(8):
square = Square(self, (x + y - 1) % 2)
squares.append(squares)
square.setFixedSize(square_size)
grid.addWidget(square, x, y)
self.squares = squares
horizontal.addStretch()
horizontal.addLayout(grid)
horizontal.addStretch()
vertical.addStretch()
vertical.addLayout(horizontal)
vertical.addStretch()
self.setLayout(vertical)
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
# how to resize children?
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
class Square(QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter()
painter.begin(self)
painter.fillRect(self.rect(), self.color)
painter.end()
def main():
logging.basicConfig(level=logging.DEBUG)
app = Application(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
default_font = QFont()
default_font.setPointSize(12)
app.setFont(default_font)
board = Board()
board.setWindowTitle('Board')
# ugly look
# chessboard.showMaximized()
# looks nice but resize not works
board.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
How should I resize square children without crash?
There are two possible solution.
You can use the Graphics View framework, which is intended exactly for this kind of applications where custom/specific graphics and positioning have to be taken into account, otherwise create a layout subclass.
While reimplementing a layout is slightly simple in this case, you might face some issues as soon as the application becomes more complex. On the other hand, the Graphics View framework has a steep learning curve, as you'll need to understand how it works and how object interaction behaves.
Subclass the layout
Assuming that the square count is always the same, you can reimplement your own layout that will set the correct geometry based on its contents.
In this example I also created a "container" with other widgets to show the resizing in action.
When the window width is very high, it will use the height as a reference and center it horizontally:
On the contrary, when the height is bigger, it will be centered vertically:
Keep in mind that you should not add other widgets to the board, otherwise you'll get into serious issues.
This would not be impossible, but its implementation might be much more complex, as the layout would need to take into account the other widgets positions, size hints and possible expanding directions in order to correctly compute the new geometry.
from PyQt5 import QtCore, QtGui, QtWidgets
class Square(QtWidgets.QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
self.setMinimumSize(50, 50)
def paintEvent(self, event: QtGui.QPaintEvent) -> None:
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), self.color)
class EvenLayout(QtWidgets.QGridLayout):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSpacing(0)
def setGeometry(self, oldRect):
# assuming that the minimum size is 50 pixel, find the minimum possible
# "extent" based on the geometry provided
minSize = max(50 * 8, min(oldRect.width(), oldRect.height()))
# create a new squared rectangle based on that size
newRect = QtCore.QRect(0, 0, minSize, minSize)
# move it to the center of the old one
newRect.moveCenter(oldRect.center())
super().setGeometry(newRect)
class Board(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
layout = EvenLayout(self)
self.squares = []
for row in range(8):
for column in range(8):
square = Square(self, not (row + column) & 1)
self.squares.append(square)
layout.addWidget(square, row, column)
class Chess(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
header = QtWidgets.QLabel('Some {}long label'.format('very ' * 20))
layout.addWidget(header, 0, 0, 1, 3, QtCore.Qt.AlignCenter)
self.board = Board()
layout.addWidget(self.board, 1, 1)
leftLayout = QtWidgets.QVBoxLayout()
layout.addLayout(leftLayout, 1, 0)
rightLayout = QtWidgets.QVBoxLayout()
layout.addLayout(rightLayout, 1, 2)
for b in range(1, 9):
leftLayout.addWidget(QtWidgets.QPushButton('Left Btn {}'.format(b)))
rightLayout.addWidget(QtWidgets.QPushButton('Right Btn {}'.format(b)))
footer = QtWidgets.QLabel('Another {}long label'.format('very ' * 18))
layout.addWidget(footer, 2, 0, 1, 3, QtCore.Qt.AlignCenter)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Chess()
w.show()
sys.exit(app.exec_())
Using the Graphics View
The result will be visually identical to the previous one, but while the overall positioning, drawing and interaction would be conceptually a bit easier, understanding how Graphics Views, Scenes and objects work might require you some time to get the hang of it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Square(QtWidgets.QGraphicsWidget):
def __init__(self, color):
super().__init__()
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def paint(self, qp, option, widget):
qp.fillRect(option.rect, self.color)
class Scene(QtWidgets.QGraphicsScene):
def __init__(self):
super().__init__()
self.container = QtWidgets.QGraphicsWidget()
layout = QtWidgets.QGraphicsGridLayout(self.container)
layout.setSpacing(0)
self.container.setContentsMargins(0, 0, 0, 0)
layout.setContentsMargins(0, 0, 0, 0)
self.addItem(self.container)
for row in range(8):
for column in range(8):
square = Square(not (row + column) & 1)
layout.addItem(square, row, column, 1, 1)
class Board(QtWidgets.QGraphicsView):
def __init__(self):
super().__init__()
scene = Scene()
self.setScene(scene)
self.setAlignment(QtCore.Qt.AlignCenter)
# by default a graphics view has a border frame, disable it
self.setFrameShape(0)
# make it transparent
self.setStyleSheet('QGraphicsView {background: transparent;}')
def resizeEvent(self, event):
super().resizeEvent(event)
# zoom the contents keeping the ratio
self.fitInView(self.scene().container, QtCore.Qt.KeepAspectRatio)
class Chess(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
header = QtWidgets.QLabel('Some {}long label'.format('very ' * 20))
layout.addWidget(header, 0, 0, 1, 3, QtCore.Qt.AlignCenter)
self.board = Board()
layout.addWidget(self.board, 1, 1)
leftLayout = QtWidgets.QVBoxLayout()
layout.addLayout(leftLayout, 1, 0)
rightLayout = QtWidgets.QVBoxLayout()
layout.addLayout(rightLayout, 1, 2)
for b in range(1, 9):
leftLayout.addWidget(QtWidgets.QPushButton('Left Btn {}'.format(b)))
rightLayout.addWidget(QtWidgets.QPushButton('Right Btn {}'.format(b)))
footer = QtWidgets.QLabel('Another {}long label'.format('very ' * 18))
layout.addWidget(footer, 2, 0, 1, 3, QtCore.Qt.AlignCenter)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Chess()
w.show()
sys.exit(app.exec_())

Custom Titlebar with frame in PyQt5

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.

PyQT custom widget fixed as square

I'm developing a custom widget (inheriting from QWidget) to use as a control. How can I fix the aspect-ratio of the widget to be square, but still allow it to be resized by the layout manager when both vertical and horizontal space allows?
I know that I can set the viewport of the QPainter so that it only draws in a central square area, but that still allows the user to click either side of the drawn area.
It seems like there is no universal way to keep a widget square under all circumstances.
You must choose one:
Make its height depend on its width:
class MyWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
policy.setHeightForWidth(True)
self.setSizePolicy(policy)
...
def heightForWidth(self, width):
return width
...
Make its minimal width depend on its height:
class MyWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
...
def resizeEvent(self, e):
setMinimumWidth(height())
...
Such a widget will be kept square as long as there is such a possibility.
For other cases you should indeed consider changing the viewport, as you mentioned. Mouse events shouldn't be that much of a problem, just find the center of the widget (divide dimensions by 2), find min(width, height) and go from there. You should be able to validate the mouse events by coordinate. It is nice to call QMouseEvent.accept, only if the event passed the validation and you used the event.
I'd go with BlaXpirit's method, but here's an alternative that I've used before.
If you subclass the custom widget's resiseEvent() you can adjust the requested size to make it a square and then set the widget's size manually.
import sys
from PyQt4 import QtCore, QtGui
class CustomWidget(QtGui.QFrame):
def __init__(self, parent=None):
QtGui.QFrame.__init__(self, parent)
# Give the frame a border so that we can see it.
self.setFrameStyle(1)
layout = QtGui.QVBoxLayout()
self.label = QtGui.QLabel('Test')
layout.addWidget(self.label)
self.setLayout(layout)
def resizeEvent(self, event):
# Create a square base size of 10x10 and scale it to the new size
# maintaining aspect ratio.
new_size = QtCore.QSize(10, 10)
new_size.scale(event.size(), QtCore.Qt.KeepAspectRatio)
self.resize(new_size)
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
layout = QtGui.QVBoxLayout()
self.custom_widget = CustomWidget()
layout.addWidget(self.custom_widget)
self.setLayout(layout)
app = QtGui.QApplication(sys.argv)
window = MainWidget()
window.show()
sys.exit(app.exec_())

Categories