python-3.x 从简单直线绘制闭合多边形

xfb7svmp  于 4个月前  发布在  Python
关注(0)|答案(1)|浏览(76)

我试图创建一个程序,允许用户通过使用Pyqt5绘制简单的线条来绘制不同类型的形状。

程序必须检测形状是否闭合,然后自动用定义的颜色(例如本代码中的红色)填充形状内部的区域
**问题:**该程序只理解封闭的形状,如果线绘制在一个特定的顺序,如果有人可以帮助我使这个功能的工作,而不管顺序,其中的线绘制?
将在下面包含一个示例,以便您沿着代码更好地理解我的意思。tnx


的数据


import sys
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsLineItem, QMenu
from PyQt5.QtCore import Qt, QPointF
from PyQt5.QtGui import QPainter, QPen, QBrush, QColor , QPolygonF

class UserLineItem(QGraphicsLineItem):
    pass

class MeasuredGraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()

        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)

        self.line_start = None
        self.current_line = None
        self.current_shape = []

        self.draw_background_grid()

        self.setRenderHint(QPainter.Antialiasing, True)
        self.setRenderHint(QPainter.SmoothPixmapTransform, True)
        self.setRenderHint(QPainter.HighQualityAntialiasing, True)
        self.setRenderHint(QPainter.TextAntialiasing, True)
        self.setRenderHint(QPainter.SmoothPixmapTransform, True)

        self.resize(800, 600)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)

    def draw_background_grid(self):
        dotted_line_pen = QPen(Qt.lightGray, 1, Qt.DotLine)

        self.scene.addLine(-500, 0, 500, 0, dotted_line_pen)
        self.scene.addLine(0, -500, 0, 500, dotted_line_pen)

        grid_spacing = 25
        for i in range(-500 + grid_spacing, 501 - grid_spacing, grid_spacing):
            self.scene.addLine(i, -500, i, 500, dotted_line_pen)
            self.scene.addLine(-500, i, 500, i, dotted_line_pen)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            item = self.itemAt(event.pos())
            if isinstance(item, UserLineItem):
                pass
            else:
                self.line_start = self.snap_to_grid(self.mapToScene(event.pos()))
                if self.line_start.x() < -500 or self.line_start.x() > 500 or \
                        self.line_start.y() < -500 or self.line_start.y() > 500:
                    return
                self.current_line = UserLineItem()
                self.current_line.setPen(QPen(Qt.black, 5))  # Set pen width
                self.scene.addItem(self.current_line)
                self.current_shape.append(self.line_start)

        elif event.button() == Qt.RightButton:
            # Only show context menu if right-clicked on a UserLineItem
            item = self.itemAt(event.pos())
            if isinstance(item, UserLineItem):
                self.show_context_menu(event.pos())

        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.line_start and self.current_line:
            end_point = self.snap_to_grid(self.mapToScene(event.pos()))
            if end_point.x() < -500 or end_point.x() > 500 or \
                    end_point.y() < -500 or end_point.y() > 500:
                return
            self.current_line.setLine(self.line_start.x(), self.line_start.y(),
                                      end_point.x(), end_point.y())

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton and self.line_start:
            if self.current_line and self.line_start != self.current_shape[0]:
                end_point = self.snap_to_grid(self.mapToScene(event.pos()))
                if end_point.x() < -500 or end_point.x() > 500 or \
                        end_point.y() < -500 or end_point.y() > 500:
                    return

                self.current_shape.append(end_point)

                if len(self.current_shape) > 2 and self.current_shape[0] == self.current_shape[-1]:
                    self.fill_shape()
                    self.current_shape = []  # Reset the current shape

            self.line_start = None
            self.current_line = None

        super().mouseReleaseEvent(event)

    def snap_to_grid(self, point):
        grid_spacing = 25
        x = round(point.x() / grid_spacing) * grid_spacing
        y = round(point.y() / grid_spacing) * grid_spacing
        return QPointF(x, y)

    def fill_shape(self):
        poly = self.scene.addPolygon(QPolygonF(self.current_shape), QPen(Qt.black), QBrush(Qt.red))

    def show_context_menu(self, pos):
        item = self.itemAt(pos)
        if isinstance(item, UserLineItem):
            global_pos = self.mapToGlobal(pos)
            context_menu = QMenu(self)
            delete_action = context_menu.addAction('Delete')
            action = context_menu.exec_(global_pos)

            if action == delete_action:
                self.scene.removeItem(item)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    view = MeasuredGraphicsView()
    view.show()
    sys.exit(app.exec_())

字符串

2ledvvac

2ledvvac1#

在你的实现中有许多逻辑问题,但最重要的是这里:

if len(self.current_shape) > 2 and self.current_shape[0] == self.current_shape[-1]:

字符串
上面只是检查是否至少有3个点,以及最后添加的点是否与第一个点相同。在这些简单的条件下,以下情况将在一定程度上失败:

  • 至少有一行不是从前一行的末尾开始,如果是最后一行,则无法调用fill_shape(),或者创建不同的形状;
  • 创建第二条线,其终点与第一条线的起点重合:
  • fill_shape()将以无效的点顺序调用;
  • 即使你想通过隐式地使用3个顶点来创建一个三角形,这也会阻止创建多于3条边的多边形的可能性;
  • 创建在现有顶点处开始或结束的任何其他线(该顶点已经有两条线);
  • 创建一个封闭的多边形,而其他 * 无关 * 线也存在;

为了创建有效的“标准”多边形(请参见闭合多边形链),您需要遵守以下事项:

  • 至少有3个顶点(点);
  • 每个顶点必须是唯一的,并且仅由恰好两条线(线段)共享,即它是一个线段的结束和另一个线段的开始;
  • 如果存在3个以上的顶点,则必须通过将每个段视为矢量(方向段,明确地从P1P2)来重构它们的顺序,其中每个端点仅与一个其他矢量的起点冲突;

最后一个方面是基本的:给定顶点数n,多边形的可能数量是(n-1)!/2!factorial4! = 24,如4! == 4 × (4 - 1) × (4 - 2)等)。4个点可以产生3个不同的迭代/多边形,5个点产生12个多边形,依此类推。
例如,给定4个任意点,可以得到以下结果:
x1c 0d1x的数据
现在,让我们按顺序进入正题(双关语)。
您可能对创建闭合面感兴趣(不管边数)就像你在纸上画它一样:你在脑海中想象它,然后画出它的线,即使它们不是按照最终多边形的顺序。为了实现这一点,你必须创建一个点的“Map”,并最终在所有点都成为实际顶点时创建闭合多边形。(也就是:每个点都是一个段的开始和结束)。
实现这一点的一种可能性是创建一个字典,它使用顶点作为键,行项作为值。每个真实的顶点正好对应两个行项,只要所有顶点都正好有两个行项,你就有一个封闭的多边形。
一旦一个新的段被完成,我们尝试用以下过程创建一个连续的路径:
1.取线段的两个点中的一个作为参考(不管是哪一个:只要一个顶点只对应于一个或两个向量,另一个点最终会到达闭合路径);
1.检查点是否至少对应于2条线:如果不是,则没有闭合多边形,因此我们退出整个过程;
1.得到线(段),排除当前的一个,并检查不是当前顶点的另一段的点;
1.如果另一个段的另一个点是引用(在1.中使用),则路径关闭;否则,从2重新开始;
下面的代码是你的代码的修改版本,实现了上面的内容。注意:

  • QPointF是不可散列的(所以它不能用于字典键);我使用x()y()值代替;
  • 为了简单起见,我在UserLineItem子类中添加了一些辅助函数,这样它就可以直接访问QLineF的getter和setter函数;
  • 我使用setSceneRect()来正确设置场景边界;
  • 在同一个小部件中使用CustomContextMenu策略没有太大意义,除非外部原因确实需要;我用contextMenuEvent()替代了它;
class UserLineItem(QGraphicsLineItem):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setPen(QPen(Qt.black, 5))

    def p1(self):
        return self.line().p1()

    def p2(self):
        return self.line().p2()

    def setP1(self, p):
        line = self.line()
        if line.p1() != p:
            line.setP1(p)
            self.setLine(line)

    def setP2(self, p):
        line = self.line()
        if line.p2() != p:
            line.setP2(p)
            self.setLine(line)

class MeasuredGraphicsView(QGraphicsView):
    gridSpacing = 25
    def __init__(self):
        super().__init__()

        self.setRenderHint(QPainter.Antialiasing)
        self.resize(800, 600)

        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        self.scene.setSceneRect(-500, -500, 1000, 1000)

        self.currentLine = None
        self.vertexLines = {}

    def drawBackground(self, qp, rect):
        normal = QPen(Qt.lightGray, 1, Qt.DotLine)
        dark = QPen(Qt.black, 1, Qt.DotLine)

        rect &= self.scene.sceneRect()
        left, top, right, bottom = rect.getCoords()

        qp.save()

        x = left // self.gridSpacing * self.gridSpacing
        while x <= right:
            if x == 0:
                qp.setPen(dark)
            else:
                qp.setPen(normal)
            qp.drawLine(QLineF(x, top, x, bottom))
            x += self.gridSpacing

        y = top // self.gridSpacing * self.gridSpacing
        while y <= bottom:
            if y == 0:
                qp.setPen(dark)
            else:
                qp.setPen(normal)
            qp.drawLine(QLineF(left, y, right, y))
            y += self.gridSpacing

        qp.restore()

    def removeCurrentLine(self):
        if self.currentLine:
            self.removeLineItem(self.currentLine)
            self.currentLine = None

    def removeLineItem(self, lineItem):
        if not isinstance(lineItem, QGraphicsLineItem):
            return
        line = lineItem.line()
        for p in (line.p1(), line.p2()):
            key = p.x(), p.y()
            if key in self.vertexLines:
                items = self.vertexLines[key]
                if lineItem in items:
                    items.remove(lineItem)
                if not items:
                    del self.vertexLines[key]
        self.scene.removeItem(lineItem)

    def contextMenuEvent(self, event):
        pos = event.pos()
        item = self.itemAt(pos)
        if isinstance(item, UserLineItem):
            menu = QMenu(self)
            menu.setAttribute(Qt.WA_DeleteOnClose)

            deleteAction = menu.addAction('Delete')
            action = menu.exec_(event.globalPos())

            if action == deleteAction:
                self.removeLineItem(item)

    def keyPressEvent(self, event):
        if self.currentLine and event.key() == Qt.Key_Escape:
            # discard the current line
            self.removeCurrentLine()
        else:
            super().keyPressEvent(event)

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        if event.button() == Qt.LeftButton:
            p = self.snapToGrid(self.mapToScene(event.pos()))
            lines = self.vertexLines.setdefault((p.x(), p.y()), [])
            if len(lines) == 2:
                return
            self.currentLine = UserLineItem(QLineF(p, p))
            self.scene.addItem(self.currentLine)
            lines.append(self.currentLine)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        if self.currentLine:
            endPoint = self.snapToGrid(self.mapToScene(event.pos()))
            if not self.scene.sceneRect().contains(endPoint):
                return
            self.currentLine.setP2(endPoint)

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        if not self.currentLine or event.button() != Qt.LeftButton:
            return

        if self.currentLine.line().isNull():
            # empty line (start and end are identical)
            self.removeCurrentLine()
            return

        endPoint = self.snapToGrid(self.mapToScene(event.pos()))
        if not self.scene.sceneRect().contains(endPoint):
            # out of scene boundaries
            self.removeCurrentLine()
            return

        current = self.currentLine.line()
        startPoint = current.p1()
        key = endPoint.x(), endPoint.y()

        if not key in self.vertexLines:
            # new vertex
            self.vertexLines[key] = [self.currentLine]
            self.currentLine = None
            return

        lineItems = self.vertexLines[key]

        if len(lineItems) == 2:
            # the point already has two lines (is real a vertex)
            self.removeCurrentLine()
            return

        other = lineItems[0].line()
        if (
            other == current or (
                other.p2() == startPoint
                and other.p1() == endPoint
            )
        ):
            # a similar segment already exists
            self.removeCurrentLine()
            return

        lineItems.append(self.currentLine)

        self.fillShape(self.currentLine)

        self.currentLine = None

    def fillShape(self, reference):
        current = reference
        p = current.p1()

        polygon = [p] # the possible polygon
        toRemove = set() # line items that may be eventually removed

        while True:
            lineItems = self.vertexLines[p.x(), p.y()]
            if len(lineItems) == 1:
                # open path, ignore
                return

            toRemove |= set(lineItems)
            firstItem, lastItem = lineItems

            if firstItem == current:
                nextItem = lastItem
            else:
                nextItem = firstItem

            if nextItem == reference:
                # closed path, finalize polygon
                break

            if nextItem.p1() == p:
                p = nextItem.p2()
            else:
                p = nextItem.p1()

            polygon.append(p)
            current = nextItem

        self.scene.addPolygon(QPolygonF(polygon), QPen(Qt.black), QBrush(Qt.red))

        # remove line items and their vertexes
        for item in toRemove:
            self.removeLineItem(item)

    def snapToGrid(self, point):
        return QPointF(
            round(point.x() / self.gridSpacing) * self.gridSpacing, 
            round(point.y() / self.gridSpacing) * self.gridSpacing
        )

相关问题