You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于PyQt5的多边形分区绘制与交互编辑功能开发需求

基于PyQt5的分区绘图程序实现方案

功能需求

  • 在已填充的多边形内绘制线条,将其分割为多个子区域,新区域默认使用蓝色填充
  • 添加右键菜单,支持修改任意分区的填充颜色
  • 实现鼠标悬停效果,悬停时高亮显示分区边界,提升区域感知度

操作流程示例

  1. 创建基础闭合多边形:通过左键点击绘制连续线条,闭合后程序自动用红色填充内部区域
  2. 划分子区域:在红色填充区域内,绘制连接多边形边界的线条,程序会自动分割出多个蓝色填充的子区域

修改后的完整代码

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

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 RegionPolygonItem(QGraphicsPolygonItem):
    def __init__(self, polygon, color=Qt.red, parent=None):
        super().__init__(polygon, parent)
        self.setPen(QPen(Qt.black, 2))
        self.setBrush(QBrush(color))
        # 启用悬停事件
        self.setAcceptHoverEvents(True)
        self.original_pen = self.pen()

    def hoverEnterEvent(self, event):
        # 悬停时加粗边界并改变颜色
        self.setPen(QPen(Qt.blue, 4))
        super().hoverEnterEvent(event)

    def hoverLeaveEvent(self, event):
        # 离开时恢复原边界样式
        self.setPen(self.original_pen)
        super().hoverLeaveEvent(event)

    def contextMenuEvent(self, event):
        # 右键菜单修改颜色
        menu = QMenu()
        change_color_action = menu.addAction("修改填充颜色")
        action = menu.exec_(event.screenPos())
        if action == change_color_action:
            color = QColorDialog.getColor(self.brush().color(), self.parentWidget())
            if color.isValid():
                self.setBrush(QBrush(color))

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 = {}
        # 存储所有区域多边形
        self.regions = []

    def drawBackground(self, qp, rect):
        normal_pen = QPen(Qt.lightGray, 1, Qt.DotLine)
        dark_pen = 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:
            qp.setPen(dark_pen if x == 0 else normal_pen)
            qp.drawLine(QLineF(x, top, x, bottom))
            x += self.gridSpacing

        # 绘制水平网格线
        y = top // self.gridSpacing * self.gridSpacing
        while y <= bottom:
            qp.setPen(dark_pen if y == 0 else normal_pen)
            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, UserLineItem):
            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)
            delete_action = menu.addAction("删除线条")
            action = menu.exec_(event.globalPos())
            if action == delete_action:
                self.removeLineItem(item)
        # 右键点击区域时,由RegionPolygonItem自己处理菜单
        elif isinstance(item, RegionPolygonItem):
            item.contextMenuEvent(event)

    def keyPressEvent(self, event):
        if self.currentLine and event.key() == Qt.Key_Escape:
            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()))
            # 检查点击点是否在已有的区域边界上(即属于顶点)
            key = (p.x(), p.y())
            lines = self.vertexLines.setdefault(key, [])
            # 每个顶点最多连接2条线
            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():
            self.removeCurrentLine()
            return

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

        current_line = self.currentLine.line()
        startPoint = current_line.p1()
        end_key = (endPoint.x(), endPoint.y())

        # 检查终点是否是已存在的顶点
        if end_key not in self.vertexLines:
            self.vertexLines[end_key] = [self.currentLine]
            self.currentLine = None
            return

        line_items = self.vertexLines[end_key]
        if len(line_items) == 2:
            self.removeCurrentLine()
            return

        # 检查是否重复绘制线条
        other_line = line_items[0].line()
        if (other_line == current_line) or (other_line.p1() == endPoint and other_line.p2() == startPoint):
            self.removeCurrentLine()
            return

        line_items.append(self.currentLine)
        # 尝试分割区域或创建新区域
        self.processNewLine(self.currentLine)
        self.currentLine = None

    def processNewLine(self, reference_line):
        # 先尝试寻找闭合区域
        closed_polygons = self.findClosedPolygons(reference_line)
        if not closed_polygons:
            return

        # 移除参与闭合的线条
        removed_lines = set()
        for poly_data in closed_polygons:
            removed_lines.update(poly_data['lines'])

        # 删除旧的区域(如果是分割现有区域)
        for region in self.regions[:]:
            for poly_data in closed_polygons:
                polygon = poly_data['polygon']
                if region.polygon().containsPoint(polygon[0], Qt.OddEvenFill):
                    self.scene.removeItem(region)
                    self.regions.remove(region)
                    break

        # 添加新区域,第一个区域保留红色,其余用蓝色
        for i, poly_data in enumerate(closed_polygons):
            color = Qt.red if i == 0 and len(self.regions) == 0 else Qt.blue
            polygon_item = RegionPolygonItem(QPolygonF(poly_data['polygon']), color)
            self.scene.addItem(polygon_item)
            self.regions.append(polygon_item)

        # 清理已使用的线条
        for line_item in removed_lines:
            self.removeLineItem(line_item)

    def findClosedPolygons(self, reference_line):
        closed_polygons = []
        # 从参考线的两个端点分别寻找闭合路径
        for start_point in [reference_line.p1(), reference_line.p2()]:
            current_line = reference_line
            current_point = start_point
            polygon = [current_point]
            used_lines = set([current_line])
            is_closed = False

            while True:
                key = (current_point.x(), current_point.y())
                line_items = self.vertexLines.get(key, [])
                if len(line_items) != 2:
                    break

                # 获取下一条线(不是当前线的另一条)
                next_line = line_items[0] if line_items[1] == current_line else line_items[1]
                if next_line in used_lines:
                    # 回到起点,闭合
                    if next_line == reference_line and len(polygon) > 2:
                        is_closed = True
                    break

                used_lines.add(next_line)
                # 更新当前点
                if next_line.p1() == current_point:
                    current_point = next_line.p2()
                else:
                    current_point = next_line.p1()
                polygon.append(current_point)
                current_line = next_line

            if is_closed and len(polygon) >= 3:
                closed_polygons.append({
                    'polygon': polygon,
                    'lines': used_lines
                })
        return closed_polygons

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

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

关键修改说明

  1. 自定义RegionPolygonItem类:
    • 继承QGraphicsPolygonItem,实现悬停事件,悬停时加粗边界颜色
    • 内置右键菜单,调用QColorDialog修改填充颜色
  2. 区域分割逻辑优化:
    • 重写processNewLine和findClosedPolygons方法,支持在已有区域内绘制分割线,自动识别新的闭合子区域
    • 新分割出的区域默认使用蓝色填充,初始创建的区域保持红色
  3. 交互逻辑调整:
    • 完善右键菜单处理,区分线条和区域的不同操作
    • 优化顶点线条管理,确保每个顶点最多连接2条线条,避免无效绘制

内容的提问来源于stack exchange,提问作者Ahoora

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 02:07:33