How to display icon and text together in PYQT5 menubar - python

So, i want to display both the text and the ICON as a menubar item.
I have used the below statement as:
self.helpMenu = menubar1.addMenu(QtGui.QIcon("home.png"),"&TEXT")
But this displays only the icon and not the text.
So need help to fix it

Premise
It seems that, despite Qt provides an addMenu() function to create a menu that has both an icon and text, it is not fully supported.
There is a related and very old bug report on the matter, which has been flagged as closed and "Out of scope". I cannot test it right now, but I'm going to suppose that it's due to the native menubar support, which is mostly intended for macOS and Linux distros that support that feature.
That said, a workaround is possible, and that's done through a QProxyStyle.
It's a bit complex, but it works seamlessly given that:
it's enabled only when the native menubar feature is not used (whether it's available or just disabled);
it uses the 'fusion' style or the default style on Windows;
The trick is to ensure that the proxy returns a correct size for sizeFromContents() that includes both the text and the icon if both exist, and to use the default implementations as much as possible in drawControl() and drawItemText() (which is called from more standard styles).
class MenuProxy(QtWidgets.QProxyStyle):
menuHack = False
alertShown = False
def useMenuHack(self, element, opt, widget):
if (element in (self.CT_MenuBarItem, self.CE_MenuBarItem) and
isinstance(widget, QtWidgets.QMenuBar) and
opt.icon and not opt.icon.isNull() and opt.text):
if not self.alertShown:
if widget.isNativeMenuBar():
# this will probably not be shown...
print('WARNING: menubar items with icons and text not supported for native menu bars')
styleName = self.baseStyle().objectName()
if not 'windows' in styleName and styleName != 'fusion':
print('WARNING: menubar items with icons and text not supported for "{}" style'.format(
styleName))
self.alertShown = True
return True
return False
def sizeFromContents(self, content, opt, size, widget=None):
if self.useMenuHack(content, opt, widget):
# return a valid size that includes both the icon and the text
alignment = (QtCore.Qt.AlignCenter | QtCore.Qt.TextShowMnemonic |
QtCore.Qt.TextDontClip | QtCore.Qt.TextSingleLine)
if not self.proxy().styleHint(self.SH_UnderlineShortcut, opt, widget):
alignment |= QtCore.Qt.TextHideMnemonic
width = (opt.fontMetrics.size(alignment, opt.text).width() +
self.pixelMetric(self.PM_SmallIconSize) +
self.pixelMetric(self.PM_LayoutLeftMargin) * 2)
textOpt = QtWidgets.QStyleOptionMenuItem(opt)
textOpt.icon = QtGui.QIcon()
height = super().sizeFromContents(content, textOpt, size, widget).height()
return QtCore.QSize(width, height)
return super().sizeFromContents(content, opt, size, widget)
def drawControl(self, ctl, opt, qp, widget=None):
if self.useMenuHack(ctl, opt, widget):
# create a new option with no icon to draw a menubar item; setting
# the menuHack allows us to ensure that the icon size is taken into
# account from the drawItemText function
textOpt = QtWidgets.QStyleOptionMenuItem(opt)
textOpt.icon = QtGui.QIcon()
self.menuHack = True
self.drawControl(ctl, textOpt, qp, widget)
self.menuHack = False
# compute the rectangle for the icon and call the default
# implementation to draw it
iconExtent = self.pixelMetric(self.PM_SmallIconSize)
margin = self.pixelMetric(self.PM_LayoutLeftMargin) / 2
top = opt.rect.y() + (opt.rect.height() - iconExtent) / 2
iconRect = QtCore.QRect(opt.rect.x() + margin, top, iconExtent, iconExtent)
pm = opt.icon.pixmap(widget.window().windowHandle(),
QtCore.QSize(iconExtent, iconExtent),
QtGui.QIcon.Normal if opt.state & self.State_Enabled else QtGui.QIcon.Disabled)
self.drawItemPixmap(qp, iconRect, QtCore.Qt.AlignCenter, pm)
return
super().drawControl(ctl, opt, qp, widget)
def drawItemText(self, qp, rect, alignment, palette, enabled, text, role=QtGui.QPalette.NoRole):
if self.menuHack:
margin = (self.pixelMetric(self.PM_SmallIconSize) +
self.pixelMetric(self.PM_LayoutLeftMargin))
rect = rect.adjusted(margin, 0, 0, 0)
super().drawItemText(qp, rect, alignment, palette, enabled, text, role)
class Test(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
menu = self.menuBar().addMenu(QtGui.QIcon.fromTheme('document-new'), 'File')
menu.addAction(QtGui.QIcon.fromTheme('application-exit'), 'Quit')
self.menuBar().addMenu(QtGui.QIcon.fromTheme('edit-cut'), 'Edit')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle(MenuProxy(QtWidgets.QStyleFactory.create('fusion')))
# or, for windows systems:
# app.setStyle(MenuProxy())
test = Test()
test.show()
sys.exit(app.exec_())

I have the same story with Windows 7 and PyQt 5.12.2 and tried to solve it like this:
import sys
from PyQt5.Qt import *
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.centralWidget = QLabel("Hello, World")
self.centralWidget.setAlignment(Qt.AlignCenter)
self.setCentralWidget(self.centralWidget)
menuBar = QMenuBar(self)
self.setMenuBar(menuBar)
self.helpContentAction = QAction(QIcon("img/readMe.png"), "&Help Content", self)
self.aboutAction = QAction("&About", self)
# helpMenu = menuBar.addMenu(QIcon("img/qtlogo.png"), "&Help")
helpMenu = menuBar.addMenu(" &Help") # +++
# ^^^^^^^^^^^^
helpMenu.addAction(self.helpContentAction)
helpMenu.addAction(self.aboutAction)
qss = """
QMenuBar {
background-color: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 lightgray, stop:1 darkgray
);
}
QMenuBar::item {
background-color: darkgray;
padding: 1px 5px 1px -25px; /* +++ */
background: transparent;
image: url(img/qtlogo.png); /* +++ * /
}
QMenuBar::item:selected {
background-color: lightgray;
}
QMenuBar::item:pressed {
background: lightgray;
}
"""
if __name__ == "__main__":
app = QApplication(sys.argv)
# app.setStyle('Fusion')
app.setStyleSheet(qss) # +++
app.setFont(QFont("Times", 10, QFont.Bold))
win = Window()
win.setWindowTitle("Python Menus")
win.resize(600, 350)
win.show()
sys.exit(app.exec_())

Related

QStyleItemDelegate checkbox doesn't match stylesheet

I've made a custom StyleItemDelegate and for some reason when it paints the checkbox indicator, it doesn't match what is defined in my Stylesheet. How can I fix this? You can see on the right side of the application that the StyleSheet properly affects how the checkbox is displayed in the default listview paint event.
Update #1
I've made a custom style item delegate to support rich text html rendering which all works great. I need to re-implement the checkbox since i've overwritten the paint event and ensure the checkbox is still available. However my text is overlapping the checkbox making it un useable. As a result when trying to paint the checkbox indicator, the highlighting of the ListItem is broken and only shows a slim blue strip on the left side.
Screenshot
Code
################################################################################
# imports
################################################################################
import os
import sys
from PySide2 import QtGui, QtWidgets, QtCore
################################################################################
# QStyledItemDelegate
################################################################################
class MyDelegate(QtWidgets.QStyledItemDelegate):
MARGINS = 10
def __init__(self, parent=None, *args):
QtWidgets.QStyledItemDelegate.__init__(self, parent, *args)
# overrides
def sizeHint(self, option, index):
'''
Description:
Since labels are stacked we will take whichever is the widest
'''
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
# draw rich text
doc = QtGui.QTextDocument()
doc.setHtml(index.data(QtCore.Qt.DisplayRole))
doc.setDocumentMargin(self.MARGINS)
doc.setDefaultFont(options.font)
doc.setTextWidth(option.rect.width())
return QtCore.QSize(doc.idealWidth(), doc.size().height())
# methods
def paint(self, painter, option, index):
painter.save()
painter.setClipping(True)
painter.setClipRect(option.rect)
opts = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(opts, index)
style = QtGui.QApplication.style() if opts.widget is None else opts.widget.style()
# Draw background
if option.state & QtWidgets.QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight().color())
else:
painter.fillRect(option.rect, QtGui.QBrush(QtCore.Qt.NoBrush))
# Draw checkbox
if (index.flags() & QtCore.Qt.ItemIsUserCheckable):
cbStyleOption = QtWidgets.QStyleOptionButton()
if index.data(QtCore.Qt.CheckStateRole):
cbStyleOption.state |= QtWidgets.QStyle.State_On
else:
cbStyleOption.state |= QtWidgets.QStyle.State_Off
cbStyleOption.state |= QtWidgets.QStyle.State_Enabled
cbStyleOption.rect = option.rect.translated(self.MARGINS, 0)
style.drawControl(QtWidgets.QStyle.CE_CheckBox, cbStyleOption, painter, option.widget)
# Draw Title
doc = QtGui.QTextDocument()
doc.setHtml(index.data(QtCore.Qt.DisplayRole))
doc.setTextWidth(option.rect.width())
doc.setDocumentMargin(self.MARGINS)
doc.setDefaultFont(opts.font)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# highlight text
if option.state & QtWidgets.QStyle.State_Selected:
ctx.palette.setColor(option.palette.Text, option.palette.color(option.palette.Active, option.palette.HighlightedText))
else:
ctx.palette.setColor(option.palette.Text, option.palette.color(option.palette.Active, option.palette.Text))
textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, option)
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
# end
painter.restore()
################################################################################
# Widgets
################################################################################
class ListViewExample(QtWidgets.QWidget):
'''
Description:
Extension of listview which supports searching
'''
def __init__(self, parent=None):
super(ListViewExample, self).__init__(parent)
self.setStyleSheet('''
QListView {
color: rgb(255,255,255);
background-color: rgb(60,60,60);
}
QCheckBox, QCheckBox:disabled {
background: transparent;
}
QWidget::indicator {
width: 12px;
height: 12px;
border: 2px solid rgb(90,90,90);
border-radius: 3px;
background: rgb(30,30,30);
}
QWidget::indicator:checked {
border: 2px solid rgb(76,175,80);
background: rgb(0,255,40);
}
''')
self.itemModel = QtGui.QStandardItemModel()
self.checkbox = QtWidgets.QCheckBox('Sample')
self.listView = QtWidgets.QListView()
self.listView.setIconSize(QtCore.QSize(128,128))
self.listView.setModel(self.itemModel)
self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.checkboxA = QtWidgets.QCheckBox('Sample')
self.listViewA = QtWidgets.QListView()
self.listViewA.setIconSize(QtCore.QSize(128,128))
self.listViewA.setModel(self.itemModel)
self.listViewA.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
# layout
self.mainLayout = QtWidgets.QGridLayout()
self.mainLayout.addWidget(self.checkbox,0,0)
self.mainLayout.addWidget(self.listView,1,0)
self.mainLayout.addWidget(self.checkboxA,0,1)
self.mainLayout.addWidget(self.listViewA,1,1)
self.setLayout(self.mainLayout)
################################################################################
# Widgets
################################################################################
def main():
app = QtWidgets.QApplication(sys.argv)
window = ListViewExample()
window.resize(600,400)
window.listView.setItemDelegate(MyDelegate())
window.itemModel.clear()
for i in range(10):
html = '''
<span style="font-size:12px;">
<b> Player <span>•</span> #{}</b>
</span>
<br>
<span style="font-size:11px;">
<b>Status:</b> <span style='color:rgb(255,0,0);'>⬤</span> Active
<br>
<b>Position:</b> WR
<br>
<b>Team:</b> <span style='color:rgb(0,128,255);'>█</span> Wings
</span>
'''.format(i)
item = QtGui.QStandardItem()
item.setData(html, QtCore.Qt.DisplayRole)
item.setCheckable(True)
window.itemModel.appendRow(item)
window.show()
app.exec_()
if __name__ == '__main__':
pass
main()
QStyle painting functions assume that you're using the default behavior of that style for the control/primitive. This means that, in most cases, the last widget argument can be ignored, since no overriding is being considered.
When using stylesheets, instead, things change. If the widget or any of its parents (including the QApplication) has a style sheet, the widget will use an internal QStyleSheetStyle, inheriting the behavior of the QApplication style, and overridden by any style sheet set for the parents.
In this case, that argument becomes mandatory, as the underlying QStyleSheetStyle will need to check if the widget has (or inherits) a stylesheet and eventually "climb" the widget tree in order to know if and how any custom styling has been set for it.
You just need to add that to the arguments of the function:
style.drawControl(QtWidgets.QStyle.CE_CheckBox, cbStyleOption, painter,
option.widget)
^^^^^^^^^^^^^
The above solves the issue of the styling, but not of the drawing.
While the appearance of a check box in an item view is usually the same of a QCheckBox (and the ::indicator pseudo selector can be used for both), they are actually painted with different functions by the style.
The problem of your implementation is that you're drawing with drawControl and CE_CheckBox, but for item views you must use drawPrimitive and PE_IndicatorItemViewItemCheck. Also, since you're just translating the original option rect, the result is that drawControl will paint over a rectangle that is big as the whole item rectangle (thus painting over the selection).
The proper solution is to create a new QStyleOptionViewItem based on the existing one, and use the rectangle returned by subElementRect with SE_ItemViewItemCheckIndicator.
def paint(self, painter, option, index):
if (index.flags() & QtCore.Qt.ItemIsUserCheckable):
cbStyleOption = QtWidgets.QStyleOptionViewItem(opts)
if index.data(QtCore.Qt.CheckStateRole):
cbStyleOption.state |= QtWidgets.QStyle.State_On
else:
cbStyleOption.state |= QtWidgets.QStyle.State_Off
cbStyleOption.state |= QtWidgets.QStyle.State_Enabled
cbStyleOption.rect = style.subElementRect(
style.SE_ItemViewItemCheckIndicator, opts, opts.widget)
style.drawPrimitive(style.PE_IndicatorItemViewItemCheck,
cbStyleOption, painter, opts.widget)
Note that you should not use translation in the painting, as that would make it inconsistent with the mouse interaction.
To translate the element, use the top and left properties in the stylesheet instead:
QWidget::indicator {
left: 10px;
...
}
Also note that you're getting the text rectangle using the original option, which is not initialized, so that could return an invalid rectangle; you should then use the actually initialized option and also use the widget argument as explained above:
textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText,
opts, opts.widget)

how to automatically change the color of selected item in QListWidget

I want to change the color of selected item in QListItem, and I find qss may be a solution. And the code is:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
with open('mainStyle.qss', 'r', encoding='utf-8') as file:
self.setStyleSheet(file.read())
# self.setStyleSheet('*{font-size: 15px;background-color: rgb(150, 150, 150);}'
# 'QListWidget::item:selected{background: rgb(128,128,255);}')
self.setStyleSheet('QListWidget::item:selected{background: rgb(128,128,255);}')
layout = QVBoxLayout()
self.setLayout(layout)
listWidget = QListWidget()
layout.addWidget(listWidget)
w1 = QWidget()
w1Item = QListWidgetItem()
w1Item.setSizeHint(QSize(150, 150))
listWidget.insertItem(0, w1Item)
listWidget.setItemWidget(w1Item, w1)
w2 = QWidget()
w2Item = QListWidgetItem()
w2Item.setSizeHint(QSize(150, 150))
listWidget.insertItem(1, w2Item)
listWidget.setItemWidget(w2Item, w2)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
app.exec_()
We can see that the color is changed to be blue when the item is selected.
However, I need to provide a general background color for other widgets. So I change the style from
self.setStyleSheet('QListWidget::item:selected{background: rgb(0,0,255);}')
to
self.setStyleSheet('*{font-size: 15px;background-color: rgb(150, 150, 150);}'
'QListWidget::item:selected{background: rgb(0,0,0);}')
Then, I find QListWidget::item:selected do not work. The color do not change when I select a item.
What's the wrong with my code?
The problem is that you're setting a QWidget for the item, and since you're using a universal selector (with the wildcard), the result is that all QWidget will have that background color, including those added as item widgets for the list view.
The color you used for the :selected pseudo is only valid for the item painted by the view, since the item widget has its own background set from the universal selector, that background won't be visible.
The solution is to use a proper selector combination ensuring that the rule only matches children of the list view that have a usable selector, and set a transparent color for those widgets.
A possibility is to set a custom property for the widgets that must be set before adding the widgets to the list (otherwise, you need to set the stylesheet after adding them, or request a style.unpolish()).
self.setStyleSheet('''
QWidget {
font-size: 15px;
background: rgb(150, 150, 150);
}
QListWidget::item:selected {
background: rgb(128,128,255);
}
QListWidget QWidget[widgetItem=true] {
background: transparent;
}
''')
# ...
w1 = QWidget()
w1.setProperty('widgetItem', True)
# ...
w2 = QWidget()
w2.setProperty('widgetItem', True)
# ...
Another way to do so is to use an "empty" subclass for widgets that are going to be added to the item view:
class CustomItemWidget(QWidget): pass
class Window(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('''
QWidget {
font-size: 15px;
background: rgb(150, 150, 150);
}
QListWidget::item:selected {
background: rgb(128,128,255);
}
CustomItemWidget {
background: transparent;
}
''')
# ...
w1 = CustomItemWidget()
# ...
w2 = CustomItemWidget()
# ...
Consider that using universal selectors for colors is usually not a good idea, as it can result in inconsistent behavior, especially for complex widgets: for instance, the scroll bars of scroll areas (like QListWidget) might not be correctly styled and can even become unusable.
If you plan to have a common background color for all widgets, it's better to set the Window role of the application palette:
app = QApplication(sys.argv)
palette = app.palette()
palette.setColor(palette.Window, QColor(150, 150, 150))
app.setPalette(palette)
# ...
In this way the current style will know exactly when to use that color as background, or as a component for other UI elements.

Changing Appearance of PyQt5 Window Title and Customizing the Window Title

The code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import*
from PyQt5.QtGui import*
from PyQt5 import QtGui
from PyQt5.QtPrintSupport import *
class Pencere(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100,50,1080,1080)
self.setWindowIcon(QtGui.QIcon("note.png"))
self.setWindowTitle("M Content Re-Writer")
self.widget = QWidget(self)
self.widget.setObjectName("widget")
self.texteditor()
vbox2 = QVBoxLayout(self.widget)
vbox2.addWidget(self.button, alignment=Qt.AlignLeft)
vbox2.addWidget(self.editor, alignment=Qt.AlignLeft | Qt.AlignTop)
vbox = QVBoxLayout(self)
vbox.setContentsMargins(0, 0, 0, 0)
vbox.addWidget(self.widget)
def texteditor(self):
self.editor = QTextEdit()
self.editor.resize(500, 500)
self.editor.move(5,40)
self.button = QPushButton("re-write")
self.button.setFont(QFont('Segoe Script', 11))
self.button.setStyleSheet("border : 2px lemonchiffon; border-style : solid")
self.button.clicked.connect(self.function)
def function(self):
text = self.editor.toPlainText() # editor'de yazan yaziyi al
# path, _ = QFileDialog.getSaveFileName(self, "Save File", "", "Text documents (*.txt);All files (*.*)")
if not text: # == "":
print("none")
return
# else:
path, _ = QFileDialog.getSaveFileName(
self,
"Save file",
"",
"Text documents (*.txt);All files (*.*)")
if path:
with open(path, 'w') as murti:
murti.write(text)
qss = """
#widget {
border-image: url(2.jpg) 0 0 0 0 stretch stretch;
}
QPushButton {background-color : yellow;}
QPushButton:hover:pressed {background-color: red;}
QPushButton:hover {background-color: #0ff;}
QTextEdit {
background-image: url("hand.jpeg");
min-width: 400px;
min-height: 400px;
border: 2px solid black;
color:white;
font-size:24px;
}
"""
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(qss)
demo = Pencere()
demo.show()
sys.exit(app.exec_())
Hello, How can I make the background of the title of the GUI window appear transparent instead of white? In addition, I want to ask this: How can I change the color and font style of the M Content Re-Writer text in the title? I also added a screenshot to make it better. Thanks for your help.
Since you are modifying the window title so much, I believe it would be helpful to simply remove it create a custom one.
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
This code gets rid of the window frame (which removes the title bar.)
Now, we just need to create our own title bar. This will look something like:
self.topMenu = QLabel(self)
self.topMenu.setGeometry(0, 0, 1920, 60)
self.topMenu.setStyleSheet("background-color: rgba(255,255,255, 120);")
This code creates a blank bar for everything to rest on.
From here, you just need to create a label for text, followed by three buttons for closing, minimizing, and full screening the window.

qpushbutton icon left alignment text center alignment

I can not correctly align icons and button texts.
I have generate the app gui with Designer, by default they appear like this:
I add some code,
win.pb_ejecutar.setStyleSheet("QPushButton { text-align: left; }")
And I've got this
but what I need would be this, icon left align and text center align
I've done it by adding spaces to the name, but I find it not very elegant
Anyone help me ?? Thanks
The alignment between the icon and the text are the same, so there is no solution with Qt Style Sheet, so the other alternative is to use a QProxyStyle:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, element, option, painter, widget=None):
if element == QtWidgets.QStyle.CE_PushButtonLabel:
icon = QtGui.QIcon(option.icon)
option.icon = QtGui.QIcon()
super(ProxyStyle, self).drawControl(element, option, painter, widget)
if element == QtWidgets.QStyle.CE_PushButtonLabel:
if not icon.isNull():
iconSpacing = 4
mode = (
QtGui.QIcon.Normal
if option.state & QtWidgets.QStyle.State_Enabled
else QtGui.QIcon.Disabled
)
if (
mode == QtGui.QIcon.Normal
and option.state & QtWidgets.QStyle.State_HasFocus
):
mode = QtGui.QIcon.Active
state = QtGui.QIcon.Off
if option.state & QtWidgets.QStyle.State_On:
state = QtGui.QIcon.On
window = widget.window().windowHandle() if widget is not None else None
pixmap = icon.pixmap(window, option.iconSize, mode, state)
pixmapWidth = pixmap.width() / pixmap.devicePixelRatio()
pixmapHeight = pixmap.height() / pixmap.devicePixelRatio()
iconRect = QtCore.QRect(
QtCore.QPoint(), QtCore.QSize(pixmapWidth, pixmapHeight)
)
iconRect.moveCenter(option.rect.center())
iconRect.moveLeft(option.rect.left() + iconSpacing)
iconRect = self.visualRect(option.direction, option.rect, iconRect)
iconRect.translate(
self.proxy().pixelMetric(
QtWidgets.QStyle.PM_ButtonShiftHorizontal, option, widget
),
self.proxy().pixelMetric(
QtWidgets.QStyle.PM_ButtonShiftVertical, option, widget
),
)
painter.drawPixmap(iconRect, pixmap)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
proxy_style = ProxyStyle(app.style())
app.setStyle(proxy_style)
w = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(w)
icons = [
app.style().standardIcon(standardIcon)
for standardIcon in (
QtWidgets.QStyle.SP_MediaPlay,
QtWidgets.QStyle.SP_MediaPause,
QtWidgets.QStyle.SP_MediaSeekBackward,
QtWidgets.QStyle.SP_MediaSeekForward,
)
]
for text, icon in zip("Play Pause Backward Forward".split(), (icons)):
button = QtWidgets.QPushButton(text)
button.setIcon(icon)
lay.addWidget(button)
w.show()
sys.exit(app.exec_())

How to make a widget's height a fixed proportion to its width

I'm working on a PyQt5 application that needs to have a banner along the top. The banner is just a wide image, whose width should always be the width of the window, and whose height should be a fixed proportion. In other words, the banner image's height should depend on the width of the window. The widget beneath the banner (the main content) should stretch to fill all available vertical space.
I've basically ported this SO answer to PyQt5:
class Banner(QWidget):
def __init__(self, parent):
super(Banner, self).__init__(parent)
self.setContentsMargins(0, 0, 0, 0)
pixmap = QPixmap('banner-1071797_960_720.jpg') # see note below
self._label = QLabel(self)
self._label.setPixmap(pixmap)
self._label.setScaledContents(True)
self._label.setFixedSize(0, 0)
self._label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self._resizeImage()
def resizeEvent(self, event):
super(Banner, self).resizeEvent(event)
self._resizeImage()
def _resizeImage(self):
pixSize = self._label.pixmap().size()
pixSize.scale(self.size(), Qt.KeepAspectRatio)
self._label.setFixedSize(pixSize)
(For this example, I'm using this free banner image, but there's nothing special about it.)
I've put the banner in the application code below, where a label serves as a placeholder for the main content:
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = QWidget()
widget.setContentsMargins(0, 0, 0, 0)
layout = QVBoxLayout(widget)
banner = Banner(widget)
bannerSizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
bannerSizePolicy.setHeightForWidth(True)
banner.setSizePolicy(bannerSizePolicy)
layout.addWidget(banner)
label = QLabel('There should be a banner above')
label.setStyleSheet('QLabel { background-color: grey; color: white; }');
layout.addWidget(label)
layout.setStretch(0, 1)
widget.resize(320, 200)
widget.move(320, 200)
widget.setWindowTitle('Banner Tester')
widget.show()
sys.exit(app.exec_())
The problem is, the label fills 100% of the window—the banner isn't visible at all.
I've tried many different size policies and stretch factors, and removing size policies altogether, but haven't found how to do what I need. The image in the banner should be proportionately scaled to fit the window's width, and the label should fill the remaining vertical space in the window.
Ideas?
#kuba-ober's comment was right: I had to implement hasHeightForWidth() and heightForWidth() in the Banner class.
Here's the modified code, which works the way I want. All the modifications have comments within the code.
class Banner(QWidget):
def __init__(self, parent):
super(Banner, self).__init__(parent)
self.setContentsMargins(0, 0, 0, 0)
pixmap = QPixmap('banner-1071797_960_720.jpg')
# First, we note the correct proportion for the pixmap
pixmapSize = pixmap.size()
self._heightForWidthFactor = 1.0 * pixmapSize.height() / pixmapSize.width()
self._label = QLabel('pixmap', self)
self._label.setPixmap(pixmap)
self._label.setScaledContents(True)
self._label.setFixedSize(0, 0)
self._label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self._resizeImage(self.size())
def hasHeightForWidth(self):
# This tells the layout manager that the banner's height does depend on its width
return True
def heightForWidth(self, width):
# This tells the layout manager what the preferred and minimum height are, for a given width
return math.ceil(width * self._heightForWidthFactor)
def resizeEvent(self, event):
super(Banner, self).resizeEvent(event)
# For efficiency, we pass the size from the event to _resizeImage()
self._resizeImage(event.size())
def _resizeImage(self, size):
# Since we're keeping _heightForWidthFactor, we can code a more efficient implementation of this, too
width = size.width()
height = self.heightForWidth(width)
self._label.setFixedSize(width, height)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = QWidget()
widget.setContentsMargins(0, 0, 0, 0)
layout = QVBoxLayout(widget)
banner = Banner(widget)
# Turns out we don't need the bannerSizePolicy now
# bannerSizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
# bannerSizePolicy.setHeightForWidth(True)
# banner.setSizePolicy(bannerSizePolicy)
layout.addWidget(banner)
label = QLabel('There should be a banner above')
label.setStyleSheet("QLabel { background-color: grey; color: white; }");
layout.addWidget(label)
layout.setStretch(1, 1)
widget.resize(320, 200)
widget.move(320, 200)
widget.setWindowTitle('Banner Tester')
widget.show()
sys.exit(app.exec_())

Categories

Resources