0

I have a QTableWidget with some rows.

In each row, one of the cells has another widget set via setCellWidget.

I would like to style this cellWidget based on whether or not the row is selected. For reference, the cellWidget is another QTableWidget but it is disabled/not editable and essentially read-only.

I have found the syntax for accessing sub-controls (in particular, accessing the item of the parent QTableWidget) -- namely MainTable::item https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-sub-controls

I have also found the (more standard) css-syntax for accessing the pseudo-state of the control -- namely MainTable::item:selected. https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-pseudo-states

If I naively use this to style the selected item (tablerow) as yellow as below

  def add_file(self, row, element):
    """populate a new row in the table"""
    # self is the parent QTableWidget
    self.setRowHeight(row, self.ICON_SIZE.height())
    img = self.create_thumbnail(element['filepath'])

    # add an image to the first column
    item = QTableWidgetItem("",0)
    item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
    item.setData(Qt.DecorationRole, img)
    item.setData(Qt.TextAlignmentRole, Qt.AlignHCenter|Qt.AlignCenter)
    item.setData(Qt.SizeHintRole, self.ICON_SIZE)
    self.setItem(row, self.THUMBCOL, item)

    # StatsTable is another nested QTableWidget
    stats = StatsTable(element)
    # add the new nested table to the outer main tables second column
    self.setCellWidget(row, self.STATSCOL, stats)

    self.setStyleSheet("""
MainTable::item:selected 
{
  background: yellow;
  color: purple;
}
""")

The entire row except for the cellWidget will have a yellow background.

Now if I modify the QSS-selector in an attempt to access the child widget, I get unexpected results:

MainTable::item:selected QTableWidget
{
  background: yellow;
  color: purple;
}

this results in every row having its cellWidget-table given a yellow background independent of the selection-status of the row (unlike before where only the selected row sans the nested table had a yellow background).

Is there something simple I am overlooking here, or do I have to create some callbacks to manually apply and unapply the style when a row is selected?

this is a selected row with the first QSS applied

this is a selected row with the second QSS applied

neither of these two has the cellWidget styled if the row is selected.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Meowf
  • 65
  • 7
  • So you practically want that the "child table" background changes only when the row of the parent table is selected? I doubt that could be achieved directly through stylesheets, you probably should change the stylesheet on the fly by connecting to the [`selectionChanged`](https://doc.qt.io/qt-5/qitemselectionmodel.html#selectionChanged) signal of the main table's `selectionModel()`. – musicamante Oct 12 '21 at 00:17
  • That is what I would like to happen yes, essentially giving it the same style as the other "normal" cells in the selected row. Updating it via code/callbacks based on selections is one workaround I thought about (possibly by setting/toggling some property on the child-table that QSS can access via an attribute-selector to selectively apply stylesheets, as opposed to adding and removing them all the time). – Meowf Oct 12 '21 at 00:22
  • And the yellow background should be the same for *all* selected items, not only the table, right? – musicamante Oct 12 '21 at 00:50
  • Not necessarily, my problem is basically how to selectively apply a style to the table (it is currently looking like the signal-method is the way to go) -- as long as I can style the background colour of the cell-table to match the colour of the other selected items in the same row, I can use the first solution in my question to adjust the colour of the other items in the row if necessary. The yellow is largely to make the selection very visible. – Meowf Oct 12 '21 at 01:00

2 Answers2

1

As an alternative to using item delegates, I added a callback to the itemSelectionChanged-signal, and iterate through the rows in the main table. I set a property value on the child-tableWidget depending on whether the row is selected or not. This property is accessed in the stylesheet.

Unfortunately it seems I have to force a recalculation of the stylesheet by setting it in its entirety, so the seemingly clever workaround is not all that clever after all.

Since my nested widget is very restricted (read only, disabled so it can not be navigated to, ...) I do not think I need the flexibility of a custom item delegate, even though it probably is a better solution. I also expect far less than 100 rows, so performance may not be an issue.

  def __init__(self, ...):
    ...
    # called whenever the main table has its selected row(s) change.
    self.itemSelectionChanged.connect(self.update_selection)

  def update_selection(self):
    for row in range(self.rowCount()):
      item = self.item(row, 0)
      widg = self.cellWidget(row, 1)
      if item.isSelected():
        widg.setProperty("row_is_selected", "true")
      else:
        widg.setProperty("row_is_selected", "false")

      # it is apparently necessary to force a recalculation anyway so the
      # above property-change is a roundabout way to adjust the style
      # compared to just setting or removing it below.
      # this forces a recalculation nonetheless.
      widg.setStyleSheet(widg.styleSheet())

  def add_file(self, row, element):
    ...
    stats.setProperty("row_is_selected", "false")
    self.setStyleSheet("""
StatsTable[row_is_selected="true"]
{
  background: yellow;
  color: purple;
}
""")

Meowf
  • 65
  • 7
  • "it is apparently necessary to force a recalculation" this is explained in the [documentation](//doc.qt.io/qt-5/stylesheet-syntax.html#setting-qobject-properties): "the qproperty syntax is evaluated only once, which is when the widget is polished by the style". Also note that while in your case you *know* that an item has been set for the first column, `item()` returns `None` on *valid* indexes if an item hasn't been set (that's why I used `selectedIndexes` instead). Finally (and unrelated), when sharing code on common places it's better to comply to the convention of 4 spaces for indentation. – musicamante Oct 12 '21 at 03:37
0

The subcontrol and pseudo elements cannot be used for parenthood selectors, and it's also impossible to set the background of a specific item based on the selection if the whole row is selected.

The background of an item view is painted using the Base palette color role, which is normally white and opaque. What you could do is to override it and make it transparent:

def add_file(self, row, element):
    # ...
    palette = stats.palette()
    palette.setColor(palette.Base, QtCore.Qt.transparent)
    stats.setPalette(palette)

Unfortunately, this will only fix the background part, and won't change the color of the displayed text. In order to achieve that, you need to know the state of the selection and update the stylesheets depending on the item selection.
You could connect to the selectionChanged of the main table's selectionModel() (or itemSelectionChanged for a QTableWidget), and then style items accordingly:

        # somewhere in the __init__
        self.TableQSS = '''
            QTableWidget
            {
              background: yellow;
              color: purple;
            }
        '''
        self.itemSelectionChanged.connect(self.updateTables)

    def updateTables(self):
        selected = self.selectedIndexes()
        for row in range(self.rowCount()):
            table = self.cellWidget(row, self.STATSCOL)
            if not isinstance(table, StatsTable):
                continue
            if self.model().index(row, self.STATSCOL) in selected:
                table.setStyleSheet(self.TableQSS)
            else:
                table.setStyleSheet('')

Consider that stylesheets and palette don't always play well together, and setting palette colors is normally the preferred solution as it's (theoretically) safer with the current style which will use the palette to define other colors, such gradients, shades, etc.
So, keep setting the palette as explained at the beginning, still connect the itemSelectionChanged signal as above, and then:

    def updateTables(self):
        # get the default colors for the text from the palette of the main 
        # table (we cannot rely on the child tables as they've been changed)
        basePalette = self.palette()
        colors = [basePalette.color(cg, basePalette.Text) for cg in range(3)]
        selected = self.selectedIndexes()
        for row in range(self.rowCount()):
            table = self.cellWidget(row, self.STATSCOL)
            if not isinstance(table, StatsTable):
                continue
            palette = table.palette()
            if self.model().index(row, self.STATSCOL) in selected:
                palette.setColor(palette.Text, QtGui.QColor('purple'))
            else:
                # restore default colors
                for cg, color in enumerate(colors):
                    palette.setColor(cg, palette.Text, color)
            table.setPalette(palette)

Note that using a nested item view is normally not a good idea, as it makes things much more complex (especially with selections and keyboard/mouse interaction) and could potentially create issues in certain situations.

Since it seems that you only need to display data, you should consider implementing your own item delegate (see QStyledItemDelegate) and eventually draw formatted text using a basic HTML table (see this answer).
Alternatively, use a QPlainTextEdit with disabled scroll bars and set in read only mode (in this case, you still need to do what explained above).

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • ```The subcontrol and pseudo elements cannot be used for parenthood selectors``` Thanks, this answer my main question (and means I have to use alternative solutions like those suggested). ```and it's also impossible to set the background of a specific item based on the selection if the whole row is selected.``` with pure QSS/CSS, I assume is the intended meaning here? – Meowf Oct 12 '21 at 01:42
  • @Meowf yes, QSS is based on CSS2.1 which has limited features compared to those provided by CSS3 (which could have provided something like `nth-child` maybe) and in any case it would be almost impossible: styling is based on generic widget state, not on its elements (except for some very specific cases, like the `:first` or `:last` pseudo states that can be used on some widgets) and item views are potentially very large and complex (consider a QTreeView). Implementing it would be extremely complex and with few benefits: delegates can be used for the same purpose and are much more customizable. – musicamante Oct 12 '21 at 02:05