I've found a way to do css based cell styling in a TableView based on contents in the cell. The following code shows an example:
#!/usr/bin/python3
from PyQt5 import QtWidgets, QtGui, QtCore
class_values = ["zero", "one", "two"]
class Cell(QtWidgets.QWidget):
def initFromItem(self, item):
self.setProperty('dataClass', class_values[int(item.text())])
class TDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, *a):
super(TDelegate, self).__init__(*a)
self.cell = Cell(self.parent())
def paint(self, painter, option, index):
item = index.model().itemFromIndex(index)
self.cell.initFromItem(item)
self.initStyleOption(option, index)
style = option.widget.style() if option.widget else QtWidgets.QApplication.style()
style.unpolish(self.cell)
style.polish(self.cell)
style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter, self.cell)
class TTableModel(QtGui.QStandardItemModel):
def __init__(self, parent=None):
super(TTableModel, self).__init__(parent)
for i in range(5):
self.appendRow([QtGui.QStandardItem(str((x+i) % 3)) for x in range(5)])
class TTableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super(TTableView, self).__init__(parent)
self.setItemDelegate(TDelegate(self))
class Main(QtWidgets.QMainWindow):
def __init__(self):
super(Main, self).__init__()
self.table = TTableView(self)
self.model = TTableModel(self)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyleSheet("""
Cell[dataClass=zero]::item { background-color: gray; }
Cell[dataClass=one]::item { background-color: green; font-style: italic }
Cell[dataClass=two]::item { font-weight: bold }
""")
mainWin = Main()
mainWin.show()
sys.exit(app.exec_())
This generates a table like this:
TableView with per cell styling
The problem is that while the colours work, the font styling has no effect. What am I doing wrong? How could I improve my code? And how does it work? For example, why does the CSS selector have to include the ::item. All answers gratefully received. But please bear in mind that the need for CSS based styling is essential to the project.
This is due to a bug in qt (v5.9.5) that ignores all font styling information when creating a CE_ItemViewItem (see QStyleSheetStyle::drawControl). Cheating by creating something else like a CE_ToolBoxTabLabel (which does correct handling of fonts in drawControl) does get you font formatting, but gets you on the colour because the rendering uses the button face palette, not the one specified in the option (or associated CSS). So you can have one or the other but not both. I know of no workaround.
As to how this works. In QStyleSheetStyle::drawControl for a CE_ItemViewItem the CSS for the subrole of ::item is looked up and if present, applied to a copy of the option (but not the font styling), and then the Item is drawn based on the updated option and its updated palette. Unfortunately there is no way to break into this code since there is no way to apply stylesheets from PyQt (since QStyleSheet is not part of the public API of Qt).
Related
I am trying to override the paintEvent() of QMenu to make it have rounded corners.
The context menu should look something like this.
Here is the code I have tried But nothing appears:
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = AddContextMenu(self)
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.painter = QtGui.QPainter(self)
self.setMinimumSize(150, 200)
self.pen = QtGui.QPen(QtCore.Qt.red)
#self.setStyleSheet('color:white; background:gray; border-radius:4px; border:2px solid white;')
def paintEvent(self, event) -> None:
self.pen.setWidth(2)
self.painter.setPen(self.pen)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.blue))
self.painter.drawRoundedRect(10, 10, 100, 100, 4.0, 4.0)
self.update()
#self.repaint()
#super(AddContextMenu, self).paintEvent(event)
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Note: setting a style sheet doesn't work for me:
this is what I get when using the style sheet It isn't completely rounded.
This is the paintEvent after #musicamante suggestion(This is just for him/her to check)
def paintEvent(self, event) -> None:
painter = QtGui.QPainter(self)
#self.pen.setColor(QtCore.Qt.white)
#painter.setFont(QtGui.QFont("times", 22))
#painter.setPen(self.pen)
#painter.drawText(QtCore.QPointF(0, 0), 'Hello')
self.pen.setColor(QtCore.Qt.red)
painter.setPen(self.pen)
painter.setBrush(QtCore.Qt.gray)
painter.drawRoundedRect(self.rect(), 20.0, 20.0)
and in the init()
self.pen = QtGui.QPen(QtCore.Qt.red)
self.pen.setWidth(2)
I cannot comment on the paintEvent functionality, but it is possible to implement rounded corners using style-sheets. Some qmenu attributes have to be modified in order to disable the default rectangle in the background, which gave you the unwanted result.
Here is a modified version of your Example using style-sheets + custom flags (no frame + transparent background):
from PyQt5 import QtWidgets, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QtWidgets.QMenu()
# disable default frame and background
cmenu.setWindowFlags(QtCore.Qt.FramelessWindowHint)
cmenu.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# set stylesheet, add some padding to avoid overlap of selection with rounded corner
cmenu.setStyleSheet("""
QMenu{
background-color: rgb(255, 255, 255);
border-radius: 20px;
}
QMenu::item {
background-color: transparent;
padding:3px 20px;
margin:5px 10px;
}
QMenu::item:selected { background-color: gray; }
""")
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Setting the border radius in the stylesheet for a top level widget (a widget that has its own "window") is not enough.
While the solution proposed by Christian Karcher is fine, two important considerations are required:
The system must support compositing; while this is true for most modern OSes, at least on Linux there is the possibility that even an up-to-date system does not support it by choice (I disabled on my computer); if that's the case, setting the WA_TranslucentBackground attribute will not work.
The FramelessWindowHint should not be set on Linux, as it may lead to problems with the window manager, so it should be set only after ensuring that the OS requires it (Windows).
In light of that, using setMask() is the correct fix whenever compositing is not supported, and this has to happen within the resizeEvent(). Do note that masking is bitmap based, and antialiasing is not supported, so rounded borders are sometimes a bit ugly depending on the border radius.
Also, since you want custom colors, using stylesheets is mandatory, as custom painting of a QMenu is really hard to achieve.
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.setMinimumSize(150, 200)
self.radius = 4
self.setStyleSheet('''
QMenu {{
background: blue;
border: 2px solid red;
border-radius: {radius}px;
}}
QMenu::item {{
color: white;
}}
QMenu::item:selected {{
color: red;
}}
'''.format(radius=self.radius))
def resizeEvent(self, event):
path = QtGui.QPainterPath()
# the rectangle must be translated and adjusted by 1 pixel in order to
# correctly map the rounded shape
rect = QtCore.QRectF(self.rect()).adjusted(.5, .5, -1.5, -1.5)
path.addRoundedRect(rect, self.radius, self.radius)
# QRegion is bitmap based, so the returned QPolygonF (which uses float
# values must be transformed to an integer based QPolygon
region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon())
self.setMask(region)
Some side notes about your paintEvent implementation, not necessary in this specific case for the above reason, but still important (some points are related to portions of code that have been commented, but the fact that you tried them makes worth mentioning those aspects):
The QPainter used for a widget must never be instanciated outside a paintEvent(): creating the instance in the __init__ as you did is a serious error and might even lead to crash. The painter can only be created when the paintEvent is received, and shall never be reused. This clearly makes useless to set it as an instance attribute (self.painter), since there's no actual reason to access it after the paint event.
If the pen width is always the same, then just set it in the constructor (self.pen = QtGui.QPen(QtCore.Qt.red, 2)), continuously setting it in the paintEvent is useless.
QPen and QBrush can directly accept Qt global colors, so there's no need to create a QBrush instance as the painter will automatically (internally and fastly) set it: self.painter.setBrush(QtCore.Qt.blue).
self.update() should never be called within a paintEvent (and not even self.repaint() should). The result in undefined and possibly dangerous.
If you do some manual painting with a QPainter and then call the super paintEvent, the result is most likely that everything painted before will be hidden; as a general rule, the base implementation should be called first, then any other custom painting should happen after (in this case it obviously won't work, as you'll be painting a filled rounded rect, making the menu items invisible).
I have implemented round corners menu using QListWidget and QWidget. You can download the code in https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/master/examples/menu/demo.py.
I have adopted the code available on
Drag'N'Drop custom widget items between QListWidgets
to work with PySide6, after few modifications I have managed to run the code and it works as it should.
But there is one bug or flaw with this code.
If an item inside a QListWidget drags and drops over another item in the same QListWidget
object the items starts to disappear behind each other or get stacked over each other or sometimes being shown as a blank item.
Like below
I am not sure what is wrong or missing with the implemented code that causes the issue.
In addition I would like to disable Copying while Drag and Dropby all means, if the user holds the CTRL button while dragging+dropping internally the flash symbol changes to
changes to + symbol and items are added to the list.
This feature should get disabled and I do not know how to do it, if it is not
possible to disable the copy feature while dropping I would like to know if there is any workaround available, since no more than one of each item should exist in either of QWidgetList objects [listWidgetA & listWidgetB] no duplicates of any items is allowed in both lists.
To summarise I would like following issues to get solved.
The flaw or issue with disappearing or stacking items while dragging and dropping the items in the same list.
The possibility of disabling copying the items while holding CTRL key while dragging and dropping or suggestion for a workaround that prevents the items to get copied in the same list or other list.
Below I have enclosed the faulty code.
from PySide6 import QtGui, QtCore, QtWidgets
import sys, os, pathlib
class ThumbListWidget(QtWidgets.QListWidget):
def __init__(self, type, parent=None):
super(ThumbListWidget, self).__init__(parent)
self.setIconSize(QtCore.QSize(124, 124))
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
self.setDefaultDropAction(QtCore.Qt.MoveAction)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setAcceptDrops(True)
self.model().rowsInserted.connect(
self.handleRowsInserted, QtCore.Qt.QueuedConnection)
def handleRowsInserted(self, parent, first, last):
print(f"first:{first} last:{last} parent:{parent}")
for index in range(first, last + 1):
item = self.item(index)
if item is not None and self.itemWidget(item) is None:
index, name, icon = item.data(QtCore.Qt.UserRole)
widget = QCustomQWidget()
widget.setTextUp(index)
widget.setTextDown(name)
widget.setIcon(icon)
item.setSizeHint(widget.sizeHint())
self.setItemWidget(item, widget)
class Dialog_01(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.listItems = {}
myQWidget = QtWidgets.QWidget()
myBoxLayout = QtWidgets.QHBoxLayout()
myQWidget.setLayout(myBoxLayout)
self.setCentralWidget(myQWidget)
self.myQListWidget = ThumbListWidget(self)
myBoxLayout.addWidget(self.myQListWidget)
images_files_dir = pathlib.Path(__file__).parent.absolute().joinpath("custom_qlistwidget")
for data in [
('No.1', 'Meyoko', pathlib.Path(images_files_dir).joinpath('among-us-small-green.png')),
('No.2', 'Nyaruko', pathlib.Path(images_files_dir).joinpath('among-us-small-yellow.png')),
('No.3', 'Louise', pathlib.Path(images_files_dir).joinpath('among-us-small-red.png'))]:
myQListWidgetItem = QtWidgets.QListWidgetItem(self.myQListWidget)
# store the data needed to create/re-create the custom widget
myQListWidgetItem.setData(QtCore.Qt.UserRole, data)
self.myQListWidget.addItem(myQListWidgetItem)
self.listWidgetB = ThumbListWidget(self)
myBoxLayout.addWidget(self.listWidgetB)
class QCustomQWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
# super(QCustomQWidget, self).__init__(parent)
super().__init__(parent)
self.textQVBoxLayout = QtWidgets.QVBoxLayout()
self.textUpQLabel = QtWidgets.QLabel()
self.textDownQLabel = QtWidgets.QLabel()
self.textQVBoxLayout.addWidget(self.textUpQLabel)
self.textQVBoxLayout.addWidget(self.textDownQLabel)
self.allQHBoxLayout = QtWidgets.QHBoxLayout()
self.iconQLabel = QtWidgets.QLabel()
self.allQHBoxLayout.addWidget(self.iconQLabel, 0)
self.allQHBoxLayout.addLayout(self.textQVBoxLayout, 1)
self.setLayout(self.allQHBoxLayout)
# setStyleSheet
self.textUpQLabel.setStyleSheet('''
color: rgb(0, 0, 255);
''')
self.textDownQLabel.setStyleSheet('''
color: rgb(255, 0, 0);
''')
def setTextUp(self, text):
self.textUpQLabel.setText(text)
def setTextDown(self, text):
self.textDownQLabel.setText(text)
def setIcon(self, imagePath):
self.iconQLabel.setPixmap(QtGui.QPixmap(imagePath))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
dialog_1 = Dialog_01()
dialog_1.show()
dialog_1.resize(480, 320)
app.exec()
Using the DragDrop flag automatically allows the possibility of duplicating internal items, since that is enabled by the Ctrl modifier, you just need to check for it in both dragMoveEvent and dropEvent, and eventually ignore it:
class ThumbListWidget(QtWidgets.QListWidget):
# ...
def dragMoveEvent(self, event):
if event.keyboardModifiers():
event.ignore()
else:
super().dragMoveEvent(event)
def dropEvent(self, event):
if event.keyboardModifiers():
event.ignore()
else:
super().dropEvent(event)
About the other point, I was able to reproduce the "empty" cell issue, but only randomly. It might be a bug caused by the interaction between the geometries of the list widget and the item widgets (especially considering that there's a QLabel with a pixmap which might cause some layout and polishing issues), but it's really difficult to find the actual cause, and for that reason it's almost impossible to find a proper solution or workaround.
If you are able to find a way to reproduce the issue consistently, I suggest you to update your question accordingly (but be aware that wouldn't mean that we could reproduce it too).
Consider that while using item widgets seems reasonable in many simple situations, a better solution is to use a custom delegate and override its paint function to draw the contents.
In the following Python 2.7, PyQt4 example, I generate 2 QTableWidgets. Table1 has no ItemDelegate and the table2 has HTMLDelegate.
Selected background color works if the table has focus, but when the table loses focus, the blue selection turns gray on table2. I want table2 selection to work like table1 when focus is lost.
How can I maintain blue selection appearance regardless of focus when using itemdelegate?
import sip
sip.setapi('QVariant', 2)
from PyQt4 import QtCore, QtGui
import random
from html import escape
words = [
"Hello",
"world",
"Stack",
"Overflow",
"Hello world",
]
class HTMLDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(HTMLDelegate, self).__init__(parent)
self.doc = QtGui.QTextDocument(self)
def paint(self, painter, option, index):
col = index.column()
row = index.row()
painter.save()
options = QtGui.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
text = index.data()
self.doc.setHtml(text)
options.text = ""
style = (
QtGui.QApplication.style()
)
style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
if option.state & QtGui.QStyle.State_Selected:
ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText), )
else:
ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(QtGui.QPalette.Active, QtGui.QPalette.Text), )
textRect = (options.rect)
constant = 4
margin = (option.rect.height() - options.fontMetrics.height()) // 2
margin = margin - constant
textRect.setTop(textRect.top() + margin)
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
return QSize(self.doc.idealWidth(), self.doc.size().height())
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
hlay = QtGui.QHBoxLayout()
lay = QtGui.QVBoxLayout(self)
self.table1 = QtGui.QTableWidget(4, 2)
self.table2 = QtGui.QTableWidget(4, 2)
lay.addLayout(hlay)
lay.addWidget(self.table1)
lay.addWidget(self.table2)
# define itemdelegate for table1, but not for table2
self.table2.setItemDelegate(HTMLDelegate(self.table2))
# fill table1
for i in range(self.table1.rowCount()):
for j in range(self.table1.columnCount()):
it = QtGui.QTableWidgetItem(random.choice(words))
self.table1.setItem(i, j, it)
# fill table2
for i in range(self.table2.rowCount()):
for j in range(self.table2.columnCount()):
it = QtGui.QTableWidgetItem(random.choice(words))
self.table2.setItem(i, j, it)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
app.setStyle("Plastique") # set style
stylesheet = """
QPushButton:hover, QComboBox:hover
{
background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #cbc9c5, stop: 1 #b9b7b5);
border: 2px solid #78879b;
border-radius: 3px;
}
QTableWidget::item:selected
{
background: #0078d7;
color: white;
}
QTableWidget
{
font: 9pt "Segoe UI";
}
QHeaderView
{
font: 9pt "Segoe UI";
}
"""
app.setStyleSheet(stylesheet)
myapp = Widget()
myapp.show()
rc = app.exec_()
myapp.close()
sys.exit(rc)
You're using QStyleOptionViewItem, which in Qt4 is a very basic class, while what you'll need is QStyleOptionViewItemV4, which implements a lot of useful things, including decorations (as in item icon) and their positioning, the item position according to other items, and, most importantly, the widget the delegate is used in and its full QStyle painting capabilities.
The style.drawControl method, like most of QStyle methods, also has a widget argument; it is normally ignored, but is very important in this kind of situations, expecially when there are stylesheets at play.
I suggest you to use the option's class of the paint() method as a reference to create your own option, which will automatically use the latest QStyleOption available for view item, and will also make a possible future transition to Qt5 much easier.
Keep in mind that, according to the documentation (which is somehow obscure about that), the widget property is available only from version 3 of QStyleOptionViewItem, but according to my tests the correct background painting will fail anyway with that version. If, for any reason, you're stuck with a very old version of Qt which doesn't provide QStyleOptionViewItemV4, I'm afraid that the only option you'll have is to keep the background color reference somewhere (there's no way to access to stylesheet colors from code, and it doesn't match the QTableView Highlight palette role) and manually paint the background by yourself.
def paint(self, painter, option, index):
#...
newOption = option.__class__(option)
self.initStyleOption(newOption, index)
#...
style = newOption.widget.style() if newOption.widget else QtGui.QApplication.style()
style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter, newOption.widget)
# ...
PS: I'd suggest you to not use too similar names for objects: I actually lost sometime finding the source of the problem because I often got confused between "option" and "options": that's why I changed it, which is much better for readability and debugging purposes too.
I have a bunch of widgets in a layout, and the layout is the child of a QFrame. This allows me to create a border around this layout. Now when any of the children receive focus, I would like to change the border color of the QFrame to indicate to the user that is where the focus currently is. How best to do this without subclassing the focuInEvent/focusOutEvent of every child with callbacks to the stylesheet of their parent widget (the QFrame)? When testing to focusInEvent of the QFrame I can never get it to trigger. Is there some sort of child focus event or something?
I think I came up with a pretty good solution for this after trying a few things out and learning a ton more about eventFilter's. Basically I found that you need to install an event filter in the parent and catch all focus events of the children. It's easier to show an example, this is a bit more complicated then it perhaps needs to be but it illustrates some important points:
import os
import sys
from PyQt4 import QtGui, QtCore
class BasePanel(QtGui.QWidget):
"""This is more or less abstract, subclass it for 'panels' in the main UI"""
def __init__(self, parent=None):
super(BasePanel, self).__init__(parent)
self.frame_layout = QtGui.QVBoxLayout()
self.frame_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.frame_layout)
self.frame = QtGui.QFrame()
self.frame.setObjectName("base_frame")
self.frame.setFrameStyle(QtGui.QFrame.Box | QtGui.QFrame.Plain)
self.frame.setLineWidth(1)
self.frame_layout.addWidget(self.frame)
self.base_layout = QtGui.QVBoxLayout()
self.frame.setLayout(self.base_layout)
self.focus_in_color = "rgb(50, 255, 150)"
self.focus_out_color = "rgb(100, 100, 100)"
self.frame.setStyleSheet("#base_frame {border: 1px solid %s}" % self.focus_out_color)
self.installEventFilter(self) # this will catch focus events
self.install_filters()
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.FocusIn:
self.frame.setStyleSheet("#base_frame {border: 1px solid %s}" % self.focus_in_color)
elif event.type() == QtCore.QEvent.FocusOut:
self.frame.setStyleSheet("#base_frame {border: 1px solid %s}" % self.focus_out_color)
return False # passes this event to the child, i.e. does not block it from the child widgets
def install_filters(self):
# this will install the focus in/out event filter in all children of the panel
for widget in self.findChildren(QtGui.QWidget):
widget.installEventFilter(self)
class LeftPanel(BasePanel):
def __init__(self, parent=None):
super(LeftPanel, self).__init__(parent)
title = QtGui.QLabel("Left Panel")
title.setAlignment(QtCore.Qt.AlignCenter)
self.base_layout.addWidget(title)
edit = QtGui.QLineEdit()
self.base_layout.addWidget(edit)
class RightPanel(BasePanel):
def __init__(self, parent=None):
super(RightPanel, self).__init__(parent)
title = QtGui.QLabel("Right Panel")
title.setAlignment(QtCore.Qt.AlignCenter)
self.base_layout.addWidget(title)
edit = QtGui.QLineEdit()
self.base_layout.addWidget(edit)
class MainApp(QtGui.QMainWindow):
def __init__(self):
super(MainApp, self).__init__()
main_layout = QtGui.QHBoxLayout()
central_widget = QtGui.QWidget()
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
left_panel = LeftPanel()
main_layout.addWidget(left_panel)
right_panel = RightPanel()
main_layout.addWidget(right_panel)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = MainApp()
ex.show()
sys.exit(app.exec_())
The only other answer (by Spencer) is using a sledgehammer to crack a nut. Unleash the power of CSS. Here's an extract from some code where I get the focused Qwidget to have a khaki background and the selected item (if applicable, e.g. in QTreeView) to have a dark kharki background. NB check out about 100 very useful colour names to use in a PyQt5 CSS context.
NB in what follows self is the QMainWindow object.
for widget in self.findChildren(QtWidgets.QWidget):
# exclude certain types from getting fancy CSS
if isinstance(widget, QtWidgets.QMenuBar) or isinstance(widget, QtWidgets.QScrollBar):
continue
widget.setStyleSheet("""
QWidget {background: azure;} # this is actually an off-white: see list above
QWidget::focus {background: khaki;} # background turns khaki only on focus!
# ... so obviously you can add some change to the border here too if you want
QWidget::item::focus {background: darkkhaki;}
""")
# NB this next stuff is not relevant to the "how to get a nice focus colouring" question,
# but just to illustrate some of the power and flexibility of CSS
self.ui.menubar.setStyleSheet('QWidget {border-bottom: 1px solid black;}')
# bolding and colour for an isolated element: note that you don't
# have to stipulate "QLabel {...}" unless it makes sense.
self.get_details_panel().ui.breadcrumbs_label.setStyleSheet('font-weight: bold; color: slategrey')
self.get_details_panel().setFrameStyle(QtWidgets.QFrame.Box)
# with this we identify the specific object to stop the style propagating to descendant objects
self.get_details_panel().setStyleSheet('QFrame#"details panel"{border-top: 1px solid black; }')
... NB in the last case the object ("details panel", a QFrame subclass) has been given an object name, which you can then use in CSS (i.e. in CSS terminology, its "id"):
self.setObjectName('details panel')
I would like to know how one can create a custom widget in pyqt. I've seen many different examples for C++, and a couple non descript examples for pyqt, but nothing that really explains how to do it and implement it. There is especially no examples that basically aren't just modified qt-designer output, and I'm writing my code from scratch so that's not very helpful.
So far, the best example I could find was basically just someone modifying qt-designer code and not really explaining what any of it was doing.
Could someone please show me an example of how to create a custom widget?
Edit:
I'm attempting to create a widget with an embedded QStackedWidget, and buttons on the bottom to cycle the pages.
I also planned on having a seperate widget for each page, but considering I can't actually accomplish step one, I figured I would cross that bridge when I get to it.
In the following it is shown how to implement a QStackedWidget with 2 buttons, the basic idea is to layout the design, for this we analyze that a QVBoxLayout must be placed to place the QStackedWidget and another layout, this second layout will be a QHBoxLayout to have the buttons. Then we connect the signals that handle the transition between pages. Also in this example I have created 3 types of widgets that will be placed on each page.
from PyQt5.QtWidgets import *
class Widget1(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent=parent)
lay = QVBoxLayout(self)
for i in range(4):
lay.addWidget(QPushButton("{}".format(i)))
class Widget2(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent=parent)
lay = QVBoxLayout(self)
for i in range(4):
lay.addWidget(QLineEdit("{}".format(i)))
class Widget3(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent=parent)
lay = QVBoxLayout(self)
for i in range(4):
lay.addWidget(QRadioButton("{}".format(i)))
class stackedExample(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent=parent)
lay = QVBoxLayout(self)
self.Stack = QStackedWidget()
self.Stack.addWidget(Widget1())
self.Stack.addWidget(Widget2())
self.Stack.addWidget(Widget3())
btnNext = QPushButton("Next")
btnNext.clicked.connect(self.onNext)
btnPrevious = QPushButton("Previous")
btnPrevious.clicked.connect(self.onPrevious)
btnLayout = QHBoxLayout()
btnLayout.addWidget(btnPrevious)
btnLayout.addWidget(btnNext)
lay.addWidget(self.Stack)
lay.addLayout(btnLayout)
def onNext(self):
self.Stack.setCurrentIndex((self.Stack.currentIndex()+1) % 3)
def onPrevious(self):
self.Stack.setCurrentIndex((self.Stack.currentIndex()-1) % 3)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = stackedExample()
w.show()
sys.exit(app.exec_())
Here are some nice advises, examples and approaches.
I think you can divide a custom Widget or any Custom "thing" you want in three ways.
Behavior: When you override its default methods with the behavior you want.
Layout: All the qt objects, be Items, or Widgets you add inside the layout will follow it's position rules and its policies.
StyleSheet: In case of Widget objects where you set the style of the Widget let's say setting its "CSS", just to be concise. Here are some references and examples.
Note: In case of non Widget objects you will not be able to set a StyleSheet so you will have to override some paint methods, create your own Painters and so on.
Here are some random examples with some comments along approaching the 3 topics I mentioned above:
import random
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MovableWidget(QWidget):
def __init__(self):
super(MovableWidget, self).__init__()
#remove the frame
self.setWindowFlags(Qt.CustomizeWindowHint)
self.pressing = False
# overriding the three next methods is a way to customize your Widgets
# not just in terms of appearance but also behavioral.
def mousePressEvent(self, QMouseEvent):
#the pos of the widget when you first pressed it.
self.start = QMouseEvent.pos()
#to make sure you are holding mouse button down
self.pressing = True
def mouseMoveEvent(self, QMouseEvent):
# You can Verify if it's also the left button and some other things
# you need.
if self.pressing : #and QMouseEvent.type() == Qt.LeftButton
self.end = QMouseEvent.pos()
self.delta = self.mapToGlobal(self.end-self.start)
self.move(self.delta)
self.end = self.start
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
# inherits from QDialog and from MovableWidget so we can have its properties.
class CustomDialog(QDialog, MovableWidget):
def __init__(self):
super(CustomDialog, self).__init__()
#Make the Dialog transparent
self.setAttribute(Qt.WA_TranslucentBackground)
# the widget will dispose itself according to the layout rules he's
# inserted into.
self.inner_widget = QWidget()
self.inner_widget.setFixedSize(300,300)
self.inner_layout = QHBoxLayout()
self.inner_widget.setLayout(self.inner_layout)
self.btn_change_color = QPushButton("Roll Color")
self.btn_change_color.setStyleSheet("""
background-color: green;
""")
# will connect to a function to be executed when the button is clicked.
self.btn_change_color.clicked.connect(self.change_color)
self.inner_layout.addWidget(self.btn_change_color)
# Choose among many layouts according to your needs, QVBoxLayout,
# QHBoxLayout, QStackedLayout, ... you can set its orientation
# you can set its policies, spacing, margins. That's one of the main
# concepts you have to learn to customize your Widget in the way
# you want.
self.layout = QVBoxLayout()
# stylesheet have basically CSS syntax can call it QSS.
# it can be used only on objects that come from Widgets
# Also one of the main things to learn about customizing Widgets.
# Note: The stylesheet you set in the "father" will be applied to its
# children. Unless you tell it to be applied only to it and/or specify
# each children's style.
# The point I used inside the StyleSheet before the QDialog
# e.g .QDialog and .QWidget says it'll be applied only to that
# instance.
self.setStyleSheet("""
.QDialog{
border-radius: 10px;
}
""")
self.inner_widget.setStyleSheet("""
.QWidget{
background-color: red;
}
""")
self.layout.addWidget(self.inner_widget)
self.setLayout(self.layout)
def change_color(self):
red = random.choice(range(0,256))
green = random.choice(range(0,256))
blue = random.choice(range(0,256))
self.inner_widget.setStyleSheet(
"""
background-color: rgb({},{},{});
""".format(red,green,blue)
)
# since MovableWidget inherits from QWidget it also have QWidget properties.
class ABitMoreCustomizedWidget(MovableWidget):
def __init__(self):
super(ABitMoreCustomizedWidget, self).__init__()
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.custom_button1 = CustomButton("Button 1")
self.custom_button1.clicked.connect(self.btn_1_pressed)
self.custom_button2 = CustomButton("Button 2")
self.custom_button2.clicked.connect(self.btn_2_pressed)
self.layout.addWidget(self.custom_button1)
self.layout.addWidget(self.custom_button2)
def btn_1_pressed(self):
self.custom_button1.hide()
self.custom_button2.show()
def btn_2_pressed(self):
self.custom_button2.hide()
self.custom_button1.show()
class CustomButton(QPushButton):
# it could receive args and keys** so all the QPushButton initializer
# would work for here too.
def __init__(self, txt):
super(CustomButton, self).__init__()
self.setText(txt)
self.setStyleSheet("""
QPushButton{
background-color: black;
border-radius: 5px;
color: white;
}
QPushButton::pressed{
background-color: blue;
}
QPushButton::released{
background-color: gray;
}
""")
if __name__ == "__main__":
app = QApplication(sys.argv)
custom_dialog = CustomDialog()
custom_widget = ABitMoreCustomizedWidget()
custom_dialog.show()
custom_widget.show()
sys.exit(app.exec_())
Tips:
You are also able to make use of masks in your widget changing it's format in "crazy" ways. For example if you need a hollow ringed widget you can have a image with this format and some transparency, create a QPixMap from that and apply it as a mask to your widget. Not a trivial work but kind of cool.
Since I showed you examples with no "TopBar" with no Frame you can also have a look in this other question where I show how to create your own top bar, move around and resize concepts.