基于PyQt5的多边形分区绘制与交互编辑功能开发需求
基于PyQt5的分区绘图程序实现方案
功能需求
- 在已填充的多边形内绘制线条,将其分割为多个子区域,新区域默认使用蓝色填充
- 添加右键菜单,支持修改任意分区的填充颜色
- 实现鼠标悬停效果,悬停时高亮显示分区边界,提升区域感知度
操作流程示例
- 创建基础闭合多边形:通过左键点击绘制连续线条,闭合后程序自动用红色填充内部区域
- 划分子区域:在红色填充区域内,绘制连接多边形边界的线条,程序会自动分割出多个蓝色填充的子区域
修改后的完整代码
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_())
关键修改说明
- 自定义RegionPolygonItem类:
- 继承QGraphicsPolygonItem,实现悬停事件,悬停时加粗边界颜色
- 内置右键菜单,调用QColorDialog修改填充颜色
- 区域分割逻辑优化:
- 重写processNewLine和findClosedPolygons方法,支持在已有区域内绘制分割线,自动识别新的闭合子区域
- 新分割出的区域默认使用蓝色填充,初始创建的区域保持红色
- 交互逻辑调整:
- 完善右键菜单处理,区分线条和区域的不同操作
- 优化顶点线条管理,确保每个顶点最多连接2条线条,避免无效绘制
内容的提问来源于stack exchange,提问作者Ahoora
相关产品推荐
相关产品推荐

