PyQt рисует QPixmap по QPainterPath

Я хочу рисовать шаблоны (предпочтительно QPixmap) вдоль QGraphicsPathItem внутри моего QGraphicsView. Это должно работать аналогично заполнению QBrush текстурой с использованиемQBrush.setTexture("example.png"). Есть ли способ сделать это на QPen? Я не смог найти ничего связанного с этим в документации PyQt.

Текстуру нужно разместить так же, как на этом примере изображения.

Не имеет значения, сохраняет ли текстура собственную ширину или масштабируется до ширины QPen.

Есть ли способ обхода этого? Я думал об использовании QTransform для преобразования QPixmap в форму желаемого QGraphicsPathItem.

2 ответа

Короче говоря, нет простого способа добиться этого, особенно если требуется использовать QPixmap.

Это не значит, что это совершенно невозможно, но сделать это, используя общие возможности Qt, довольно сложно.

По сути, вы хотите добиться «наложения текстур». Даже если это делается на двумерной поверхности (что можно легко сделать с помощью простых преобразований), то же самое для криволинейных путей требует гораздо более сложных вычислений. Хотя простые кривые (например, круги и эллипсы) можно легко получить с помощью относительно простых матричных преобразований, на самом деле эти кривые являются упрощением чрезвычайно сложных математических задач, которые в конечном итоге возникнут в случае квадратичных или кубических кривых.

См. соответствующий вопрос. Как спроецировать прямоугольную текстуру в область изогнутой формы?, в котором ОП объясняет, что этого можно достичь с помощью сплайновых преобразований тонкой пластины. Qt не предоставляет средств для этого, поэтому вам придется полагаться на внешние библиотеки: похоже, что модуль scikit-image может предоставить такую ​​​​функцию (см. проблему 2429 , но это все еще делается из Python, что может привести к очень медленная обработка сложных путей, поскольку каждый образец каждой кривой необходимо правильно сопоставить.

Тем не менее, существует возможность, если шаблон прост, как тот, о котором идет речь, все это можно упростить до одномерной структуры (линейной последовательности цветов с заданной шириной): данный шаблон можно затем интерпретировать. в виде пунктирной линии, чередующей два цвета, каждый из которых имеет ширину 15 пикселей.

В этом случае хитрость заключается в том, чтобы правильно использовать возможности QPen для отображения «штрихов», что значительно упрощает весь подход, даже если и не является полностью надежным. Просто помните, что узоры штрихов основаны на ширине пера, поэтому их фактическую длину необходимо разделить на нее.

Концепция основана на основном QGraphicsPathItem, который использует штриховой шаблон для рисования своих основных сегментов («первый цвет» шаблона) и переопределяет егометод, чтобы нарисовать оставшиеся, задав для них относительный образец штриха и смещение на основе ранее нарисованных штрихов.

Обратите внимание, что этот подход далек от совершенства, поскольку рисование выполняется слоями, а сложные контуры могут создавать графические артефакты, поскольку наложенные «пунктирные линии» всегда рисуются одна над другой.

Тем не менее, он может обеспечить приемлемые результаты для простых случаев, и некоторый уровень частичного улучшения все еще возможен.

Следующий пример класса принимает несколько возможных аргументов: кроме аргументов по умолчанию, которые требует QGraphicsPathItem, он также принимает итерацию для «шаблона» и «ширины пера», что является фундаментальным для обеспечения единообразия рисования.

«Узор» состоит из пар цветов и экстентов, указывающих «размер» каждого цвета в результате рисования.

Затем, когда установлено более пары цветов/размеров, над основным контуром рисуются дополнительные «пунктирные пути».

      class PatternPath(QGraphicsPathItem):
    _pattern = None
    def __init__(self, path=None, penWidth=5, pattern=None, parent=None):
        super().__init__(path, parent)
        self.penWidth = max(1, penWidth)
        self.otherPens = []
        self.setPattern(pattern)

    def setPattern(self, pattern):
        if not pattern:
            pattern = [(Qt.black, 1)]
        if self._pattern != pattern:
            self._pattern = pattern
            if len(pattern) == 1:
                self.setPen(QPen(pattern[0], self.penWidth, cap=Qt.FlatCap))
                return

            patternSize = 0
            for _, size in pattern:
                patternSize += size
            patternSize /= self.penWidth

            self.otherPens.clear()
            color, size = pattern[0]
            size /= self.penWidth
            basePen = QPen(color, self.penWidth, cap=Qt.FlatCap)
            basePen.setDashPattern((size, patternSize - size))
            self.setPen(basePen)

            delta = size
            for color, size in pattern[1:]:
                size /= self.penWidth
                pen = QPen(color, self.penWidth, cap=Qt.FlatCap)
                pen.setDashPattern((size, patternSize - size))
                pen.setDashOffset(delta)
                delta += size
                self.otherPens.append(pen)

    def paint(self, qp, opt, widget=None):
        super().paint(qp, opt, widget)
        if self.otherPens:
            path = self.path()
            for pen in self.otherPens:
                qp.setPen(pen)
                qp.drawPath(path)

Вот простой пример, который случайным образом создает пути и шаблоны, чтобы показать результат:

      from random import randrange, choice
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

pathFuncs = (
    (QPainterPath.lineTo, 1), 
    (QPainterPath.quadTo, 2), 
    (QPainterPath.cubicTo, 3)
)


def createPath(count=5, rect=QRectF(0, 0, 500, 500)):
    xRange = (rect.x(), rect.width())
    yRange = (rect.y(), rect.height())
    randPoint = lambda: QPointF(randrange(*xRange), randrange(*yRange))
    path = QPainterPath(randPoint())
    for i in range(count):
        func, pointCount = choice(pathFuncs)
        func(path, *(randPoint() for _ in range(pointCount)))
    return path

def createPattern(count=2, colorSize=5):
    colorData = []
    for i in range(max(1, count)):
        colorData.append((
            QColor(randrange(255), randrange(255), randrange(255)),
            colorSize
        ))
    return colorData


class PatternPath(QGraphicsPathItem):
    # as above


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    scene = QGraphicsScene()
    view = QGraphicsView(scene)
    view.setRenderHint(QPainter.Antialiasing)
    pathItem = PatternPath(createPath(2), penWidth=25, 
        pattern=createPattern(4, colorSize=15))
    scene.addItem(pathItem)
    view.resize(600, 600)
    view.show()
    sys.exit(app.exec())

Вот возможный результат:

Как видите, в правом нижнем углу есть некоторые проблемы, вызванные наложенным рисунком, описанным выше. Это более понятно для более сложного пути:

На изображении выше ясно показаны проблемы, связанные с «многослойной» живописью.

Тем не менее, если рисуемые вами пути не так сложны, решение вполне приемлемо, учитывая его скорость и результаты.

Возможно, можно частично улучшить результат, разделив общий путь на более мелкие, но вам необходимо учитывать частичные смещения тире и потенциальные проблемы с альфа-каналами или стилями пера/заглавными буквами.

Что касается последнего пункта, как вы можете видеть, я всегда использовалрежим, который необходим для правильных «многострочных» подходов, подобных этому. Это, очевидно, означает, что использованиеилистили также создают проблемы с рисованием, поэтому эти аспекты необходимо учитывать.

Я все еще не нашел подходящего решения. Но для людей, которым не требуется выравнивание шаблона вдоль пути, вы можете использовать этот фрагмент кода для своего QPen:

pen = QPen()
patternBrush = QBrush(QPixmap('patter.png'))
pen.setBrush(patternBrush)
pen.setWidth(5)

Он заполняет ваш QGraphicsPathItem следующим образом

Я надеюсь, что это поможет другим.

Другие вопросы по тегам