sdbbs
sdbbs

Reputation: 5492

Creating a custom diagonal line QBrush tiled pattern in PyQt5?

So, at first I wanted to use a gradient background to emphasize certain cells / items in my QTableView, but as it can be seen in Stretchable QLinearGradient as BackgroundRole for resizeable QTableView cells in PyQt5?, I could not quite get that to work.

Then I thought maybe I could somehow use CSS to define the gradient background and Qt properties to control which items it is shown on, however the problem is that in a QTableView, the cells / items are QStyledItemDelegate, and as noted in https://forum.qt.io/topic/84304/qtreewidget-how-to-implement-custom-properties-for-stylesheet :

Dynamic properties can only be applied to QWidgets!

Is it possible to apply the property on the selected item only?

no, not directly. Unless you subclass a custom QStyledItemDelegate and initialize the StyleOption for the painted index according to your widget's property.

So then looking at https://doc.qt.io/qt-5/qbrush.html I saw there are different patterns that can be chosen for QBrush, and in fact I liked Qt.BDiagPattern diagonal line pattern / hatch - but the lines were too thin for my taste.

So, I wanted to customize the line thickness for the BDiagPattern, and I found Can I customize own brushstyle? which has an example of how to draw a QPixmap and use it as a texture pattern that repeats / tiles, however, it does not demonstrate how to do diagonal lines.

So, I came up with this example:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QPointF, QEvent)
from PyQt5.QtGui import (QColor, QGradient, QLinearGradient, QBrush, QTransform)
from PyQt5.QtWidgets import QApplication
# starting point from https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent):
        super(TableModel, self).__init__()
        self._data = data
        self.parent = parent
        self.bg_col1 = QColor("#A3A3FF")
        self.bg_col2 = QColor("#FFFFA3")
    #
    def create_texture(self): # https://stackoverflow.com/q/62799632
        pixmap = QtGui.QPixmap(QtCore.QSize(16, 16))
        pixmap.fill(QColor(0,0,0,0)) # without .fill, bg is black; can use transparent though
        painter = QtGui.QPainter()
        painter.begin(pixmap)
        painter.setBrush(QtGui.QBrush(QtGui.QColor("blue")))
        painter.setPen(QtGui.QPen(self.bg_col1, 5, Qt.SolidLine))
        painter.drawLine(pixmap.rect().bottomLeft(), pixmap.rect().topRight())
        painter.end()
        return pixmap
    def data(self, index, role):
        if role == Qt.DisplayRole:
            # See below for the nested-list data structure.
            # .row() indexes into the outer list,
            # .column() indexes into the sub-list
            return self._data[index.row()][index.column()]
        if role == Qt.BackgroundRole:
            if index.column() == 2:
                print( f"{self.parent.table.itemDelegate(index)=}" )
                brush = QBrush(self.create_texture())
                brush.setTransform(QTransform(QTransform.fromScale(4, 4))) # zoom / scale - https://stackoverflow.com/q/41538932; scales pixels themselves
                return brush
    #
    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)
    #
    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.table = QtWidgets.QTableView()
        data = [
          [4, 9, 2, 2],
          [1, 0, 0, 0],
          [3, 5, 0, 0],
          [3, 3, 2, 2],
          [7, 8, 9, 9],
        ]
        self.model = TableModel(data, self)
        self.table.setModel(self.model)
        self.setCentralWidget(self.table)

app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

... however, as you can see from the output screenshot:

table with custom brush background

... the diagonal lines don't quite "flow" into each other - in other words, the pattern does not tile.

So, how can a draw a diagonal line pattern that tiles nicely, with custom line thickness, using QBrush and QPixmap?

Upvotes: 0

Views: 71

Answers (1)

sdbbs
sdbbs

Reputation: 5492

Just wanted to document this, as it took some hours of digging to get something that I thought works. Well, there are two things here:

  • For one, the diagonal line from bottom left to top right needs to be offset, so it "centers" nicely along the diagonal
  • But for the pattern to tile / repeat nicely as a whole, we also must draw similar diagonal "lines" (or line portions) at the top left and bottom right corners of the pixmap as well

Here is the corrected code:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QPointF, QEvent)
from PyQt5.QtGui import (QColor, QGradient, QLinearGradient, QBrush, QTransform)
from PyQt5.QtWidgets import QApplication
# starting point from https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent):
        super(TableModel, self).__init__()
        self._data = data
        self.parent = parent
        self.bg_col1 = QColor("#A3A3FF")
        self.bg_col2 = QColor("#FFFFA3")
    #
    def create_texture(self): # https://stackoverflow.com/q/62799632
        pixmap = QtGui.QPixmap(QtCore.QSize(16, 16))
        pixmap.fill(QColor(0,0,0,0)) # without .fill, bg is black; can use transparent though
        painter = QtGui.QPainter()
        painter.begin(pixmap)
        painter.setBrush(QtGui.QBrush(QtGui.QColor("blue")))
        painter.setPen(QtGui.QPen(self.bg_col1, 5, Qt.SolidLine))
        painter.drawLine(pixmap.rect().bottomLeft()+QPointF(1,0), pixmap.rect().topRight()+QPointF(1,0))
        painter.drawLine(pixmap.rect().bottomRight()+QPointF(-1+2,1), pixmap.rect().bottomRight()+QPointF(1+2,-1))
        painter.drawLine(pixmap.rect().topLeft()+QPointF(-1+0,1), pixmap.rect().topLeft()+QPointF(1+0,-1))
        painter.end()
        return pixmap
    def data(self, index, role):
        if role == Qt.DisplayRole:
            # See below for the nested-list data structure.
            # .row() indexes into the outer list,
            # .column() indexes into the sub-list
            return self._data[index.row()][index.column()]
        if role == Qt.BackgroundRole:
            if index.column() == 2:
                brush = QBrush(self.create_texture())
                brush.setTransform(QTransform(QTransform.fromScale(4, 4))) # zoom / scale - https://stackoverflow.com/q/41538932; scales pixels themselves
                return brush
    #
    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)
    #
    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.table = QtWidgets.QTableView()
        data = [
          [4, 9, 2, 2],
          [1, 0, 0, 0],
          [3, 5, 0, 0],
          [3, 3, 2, 2],
          [7, 8, 9, 9],
        ]
        self.model = TableModel(data, self)
        self.table.setModel(self.model)
        self.setCentralWidget(self.table)

app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

I kinda brute-force looked up the offsets in the code above, but I find the end result decent:

table with tiled hatch pattern screenshot

Upvotes: 0

Related Questions