PyQt中QGridLayout内QFrame间绘制连线及悬停提示方案问询
PyQt QGridLayout中QFrame中心连线及悬停提示解决方案
问题描述
我用PyQt开发GUI,通过QGridLayout布局QFrame,每行Frame间隔一个网格(比如(0,0)、(0,2)、(1,0)),需要在各Frame中心间绘制连线,且鼠标悬停连线上时显示文本,但目前连线无法正常显示。之前做过未用QGridLayout的示例,虽能绘制但手动计算坐标太繁琐,需要解决两个问题:
- 无需手动计算坐标,实现QGridLayout中QFrame间的中心连线绘制;
- 为连线添加鼠标悬停文本提示。
解决方案
一、自动获取QFrame中心坐标绘制连线
核心问题是你在初始化阶段获取的坐标是布局生效前的初始值,且绘制载体选错了(MainWindow不是存放QFrame的直接父控件)。正确实现方式:
- 将绘制逻辑放在存放QFrame的
main_view控件中,而非MainWindow; - 在
paintEvent中实时获取每个QFrame的中心坐标,利用mapToParent将QFrame自身坐标系的中心转换为父控件的坐标系坐标; - 无需额外监听控件移动,
paintEvent会在布局更新时自动触发重绘。
二、实现连线的鼠标悬停提示
要实现悬停提示需完成三点:
- 开启控件的鼠标跟踪(
setMouseTracking(True)),确保无点击时也能触发鼠标移动事件; - 重写
mouseMoveEvent,通过向量叉积计算鼠标位置到每条连线的距离; - 当距离小于连线宽度的一半时,显示
QToolTip提示文本。
完整修改代码
import sys from PyQt5.QtCore import Qt, QPointF from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QAction, QFrame, QWidget, QGridLayout, QScrollArea, QStyle, QToolTip) from PyQt5.QtGui import QPainter, QPen, QFont class MainView(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setMouseTracking(True) # 开启鼠标跟踪 self.gridLayout = QGridLayout(self) self.lines = [] # 存储连线信息:(起点Frame, 终点Frame, 提示文本) # 创建QFrame self.square1 = QFrame() self.square1.setFixedSize(150, 150) self.square1.setStyleSheet("background-color:orange") self.square2 = QFrame() self.square2.setFixedSize(150, 150) self.square2.setStyleSheet("background-color:green") self.square3 = QFrame() self.square3.setFixedSize(150, 150) self.square3.setStyleSheet("background-color:blue") self.square4 = QFrame() self.square4.setFixedSize(150, 150) self.square4.setStyleSheet("background-color:cyan") # 添加按钮 btn1 = QPushButton('Orange', self.square1) btn2 = QPushButton('green', self.square2) btn3 = QPushButton('blue', self.square3) btn4 = QPushButton('cyan', self.square4) btn1.clicked.connect(lambda: print("This is Orange button")) btn2.clicked.connect(lambda: print("This is Green button")) btn3.clicked.connect(lambda: print("This is Blue button")) btn4.clicked.connect(lambda: print("This is Cyan button")) # 布局QFrame self.gridLayout.addWidget(self.square1, 0, 0) self.gridLayout.addWidget(self.square2, 0, 2) self.gridLayout.addWidget(self.square3, 1, 0) self.gridLayout.addWidget(self.square4, 1, 2) # 定义需要绘制的连线及提示文本 self.lines = [ (self.square1, self.square2, "橙色→绿色"), (self.square1, self.square3, "橙色→蓝色"), (self.square2, self.square4, "绿色→青色"), (self.square3, self.square4, "蓝色→青色") ] def paintEvent(self, event): super().paintEvent(event) painter = QPainter(self) pen = QPen(Qt.red, 2, Qt.SolidLine) painter.setPen(pen) # 遍历所有连线,实时获取中心坐标并绘制 for frame1, frame2, _ in self.lines: center1 = frame1.mapToParent(frame1.rect().center()) center2 = frame2.mapToParent(frame2.rect().center()) painter.drawLine(center1, center2) def distance_point_to_line(self, point, line_start, line_end): # 向量叉积法计算点到直线的距离 line_vec = line_end - line_start point_vec = point - line_start cross_product = line_vec.x() * point_vec.y() - line_vec.y() * point_vec.x() line_length = line_vec.manhattanLength() if line_vec.manhattanLength() != 0 else 1 return abs(cross_product) / line_length def mouseMoveEvent(self, event): super().mouseMoveEvent(event) mouse_pos = event.pos() tip_text = "" # 检查每条连线是否被鼠标悬停 for frame1, frame2, tip in self.lines: center1 = QPointF(frame1.mapToParent(frame1.rect().center())) center2 = QPointF(frame2.mapToParent(frame2.rect().center())) dist = self.distance_point_to_line(QPointF(mouse_pos), center1, center2) # 距离小于线宽时判定为悬停 if dist <= 2: tip_text = tip break if tip_text: QToolTip.setFont(QFont("SansSerif", 10)) QToolTip.showText(event.globalPos(), tip_text, self) else: QToolTip.hideText() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("My App window title") self.setStyleSheet("background-color: rgb(176,196,222);") self.centralwidget = QScrollArea() self.setCentralWidget(self.centralwidget) menuBar = self.menuBar() fileMenu = menuBar.addMenu('&File') openFile = QAction(self.style().standardIcon(QStyle.SP_MessageBoxCritical), 'Open', self) fileMenu.addAction(openFile) self.width = 900 self.height = 500 self.resize(self.width, self.height) self.main_view = MainView() self.main_view.setFixedSize(self.width, self.height) self.centralwidget.setWidget(self.main_view) if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())
代码关键说明
MainView作为QFrame的容器,同时负责连线绘制和鼠标事件处理,确保坐标计算的准确性;paintEvent中实时获取Frame中心坐标,保证布局变化或控件移动时连线自动更新;- 用
lines列表统一管理所有连线及其提示文本,便于后续扩展维护; - 点到直线的距离计算采用向量叉积法,判断逻辑高效且准确。
内容的提问来源于stack exchange,提问作者A.G.Progm.Enthusiast
相关产品推荐
相关产品推荐

