公司动态

PySide6, 将 QWidget 嵌入 GraphicsView 作为图元显示

📅 2026/8/14 15:08:18
PySide6, 将 QWidget 嵌入 GraphicsView 作为图元显示
核心原理Qt 提供QGraphicsProxyWidget代理类原生QGraphicsItem无法直接承载普通 QWidget按钮、输入框、下拉框、窗体控件QGraphicsProxyWidget是QGraphicsItem的子类专门充当桥梁把任意 QWidget 包装成标准图元可放入QGraphicsScene跟随场景平移、缩放、旋转、选中、拖拽。demofrom PySide6.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene, QGraphicsProxyWidget, QPushButton, QLineEdit, QWidget, QVBoxLayout, QGraphicsItem) from PySide6.QtCore import Qt, QPointF from PySide6.QtGui import QPainter # 可拖拽、支持输入实时刷新的增强代理类 class DraggableProxyWidget(QGraphicsProxyWidget): def __init__(self): super().__init__() # 开启标记 self.setFlags( QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable ) self._drag_start_pos: QPointF None def mousePressEvent(self, event): # 左键按下记录拖拽起点同时让内部控件获取焦点 if event.button() Qt.LeftButton: self._drag_start_pos event.scenePos() self.setFocus(Qt.MouseFocusReason) super().mousePressEvent(event) def mouseMoveEvent(self, event): # 拖拽逻辑偏移量叠加到自身位置 if self._drag_start_pos is not None and event.buttons() Qt.LeftButton: delta event.scenePos() - self._drag_start_pos self.setPos(self.pos() delta) self._drag_start_pos event.scenePos() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): self._drag_start_pos None super().mouseReleaseEvent(event) # 控件内容改变强制刷新 def contentChanged(self): self.update() if __name__ __main__: app QApplication([]) scene QGraphicsScene(0, 0, 900, 600) view QGraphicsView(scene) view.setRenderHint(QPainter.Antialiasing) view.setRenderHint(QPainter.SmoothPixmapTransform) # 容器控件 container QWidget() layout QVBoxLayout(container) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(8) btn QPushButton(测试按钮) edit QLineEdit(输入框控件) layout.addWidget(btn) layout.addWidget(edit) # 使用自定义可拖拽代理 proxy DraggableProxyWidget() proxy.setWidget(container) proxy.setPos(200, 150) # 绑定输入框文本变化实时刷新界面 def on_text_change(): proxy.contentChanged() edit.textChanged.connect(on_text_change) # 按钮测试槽函数 def btn_click(): print(f当前输入框内容{edit.text()}) btn.clicked.connect(btn_click) scene.addItem(proxy) view.resize(900, 600) view.show() app.exec()一、基础用法最简示例1. 关键 API# 1. 用已有控件创建代理 proxy QGraphicsProxyWidget(parent_itemNone) proxy.setWidget(widget) # 2. 快捷构造 proxy QGraphicsProxyWidget(widget) # 常用控制 proxy.setPos(x, y) # 设置在场景中的位置 proxy.setZValue(5) # 层级置顶 proxy.setFlags(可拖拽、选中等Item标记) proxy.widget() # 获取内部绑定的QWidget proxy.setGeometry(rect) # 强制修改控件大小2. 变换旋转、缩放、平移和普通 Item 用法完全一致from PySide6.QtGui import QTransform t QTransform() t.rotate(15) # 旋转15° t.scale(0.8, 0.8) # 整体缩小 proxy.setTransform(t) proxy.setTransformOriginPoint(proxy.boundingRect().center()) # 中心旋转3. 父子嵌套代理 Item 可以绑定父 Item跟随父图元一起移动旋转proxy.setParentItem(父GraphicsItem)4. 信号槽互通内部控件信号正常使用不受代理影响def click_func(): print(按钮点击) btn.clicked.connect(click_func)三、重要属性与坑点处理1. 控件缩放模糊问题场景视图放大后内置 Widget 会模糊 原因Widget 是光栅实时渲染放大后像素拉伸。 解决轻度缩放开启视图抗锯齿view.setRenderHint(QPainter.SmoothPixmapTransform)大幅缩放不使用代理控件改用纯 QGraphicsItem 绘制自定义 UI固定大小不跟随缩放添加标记proxy.setFlag(QGraphicsItem.ItemIgnoresTransformations)设置后视图缩放旋转时控件大小永远不变适合悬浮标签、工具栏。2. 焦点、输入框无法输入常见原因视图没有开启键盘焦点代理没有开启焦点标记。 修复proxy.setFlag(QGraphicsItem.ItemIsFocusable) # 点击自动获取焦点 def proxy_mousePress(event): proxy.setFocus(Qt.MouseFocusReason) event.accept() proxy.mousePressEvent proxy_mousePress3. 控件销毁注意一个 QWidget 只能绑定一个 Proxy重复 setWidget 会报错 移除图元不要直接 delete 控件标准释放流程scene.removeItem(proxy) proxy.setWidget(None) proxy.deleteLater()4. 性能问题大量 ProxyWidget几十个以上渲染卡顿原理每个代理控件都会独立渲染窗口开销远高于普通图形 Item优化方案静态 UI 尽量用 QGraphicsTextItem、QGraphicsPixmapItem 代替控件频繁刷新控件单独隔离不要批量旋转缩放大量代理静态控件截图转为 PixmapItem 缓存。四、高级用法1. 自定义 QWidget 封装成可复用图元class WidgetItem(QGraphicsProxyWidget): def __init__(self): super().__init__() self.w QWidget() lay QVBoxLayout(self.w) self.btn QPushButton(自定义控件) lay.addWidget(self.btn) self.setWidget(self.w) self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)2. Proxy 嵌入普通自定义 GraphicsItem子控件base_item QGraphicsRectItem(0,0,300,200) proxy QGraphicsProxyWidget() proxy.setWidget(QPushButton(内嵌按钮)) proxy.setParentItem(base_item) proxy.setPos(20,20) scene.addItem(base_item)3. 拖动缩放控件封装配合鼠标事件实现控件拖拽拉伸Proxy 本身支持拖拽只需绑定 ItemIsMovable无需额外编写拖拽逻辑。五、一个demo在相机的画面上动态迭加状态窗口import cv2 from PySide6.QtCore import QThread, Signal from PySide6.QtGui import QPainter, QImage, QPixmap from PySide6.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene, QGraphicsProxyWidget, QWidget, QGraphicsItem, QGraphicsRectItem, QGraphicsPixmapItem, QVBoxLayout, QPushButton) # 相机线程 class CameraThread(QThread): 摄像头读取子线程 frame_signal Signal(QImage) def __init__(self, cam_index0): super().__init__() self.cam_index cam_index self._run_flag True def run(self): cap cv2.VideoCapture(self.cam_index) # 可选设置分辨率 cap.set(cv2.CAP_PROP_FRAME_WIDTH,1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT,720) while self._run_flag: ret, frame cap.read() if not ret: continue # BGR - RGB frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch frame_rgb.shape bytes_per_line ch * w q_img QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.frame_signal.emit(q_img) cap.release() def stop(self): self._run_flag False self.wait() # 通过代理将QWidget装入QGraphicsItem后在QGraphicsView上显示(一个矩形的Item) class ProxyItem(QGraphicsRectItem): def __init__(self, widget:QWidget, parentNone): super().__init__(parent) self.widget widget # 需要代理承接的部件 self.setRect(0,0,widget.width(), widget.height()) self.setFlag(QGraphicsItem.ItemIsMovable) # 代理 self.proxy QGraphicsProxyWidget(self) self.proxy.setWidget(widget) self.proxy.setPos(0, 0) self.widget.btn_close.clicked.connect(self.remove_self) # 名称位btn_close的按钮点击后的连接 def remove_self(self): self.setPos(0, 0) self.scene().removeItem(self) # UI主窗口 class UIForm(QWidget): def __init__(self): super().__init__() # 主窗口 self.setFixedSize(1280,740) self.graphicsView QGraphicsView() self.graphicsView.setFixedSize(1080, 720) self.layout QVBoxLayout() self.layout.addWidget(self.graphicsView) self.setLayout(self.layout) self.cam_thread CameraThread(cam_index0) self.cam_thread.frame_signal.connect(self.update_frame) self.cam_thread.start() w, h self.graphicsView.width(), self.graphicsView.height() # 场景 self.scene QGraphicsScene(0, 0, w, h) self.graphicsView.setScene(self.scene) self.pixmap_item QGraphicsPixmapItem() self.scene.addItem(self.pixmap_item) # 视图 self.view QGraphicsView(self.scene) self.view.setFixedSize(w, h) self.view.setRenderHint(QPainter.Antialiasing) self.view.setRenderHint(QPainter.SmoothPixmapTransform) self.btn_add QPushButton(Add SonForm, self) self.btn_add.setGeometry(520, 580, 120, 40) def update_frame(self, qimg:QImage): pix QPixmap.fromImage(qimg) self.pixmap_item.setPixmap(pix) def closeEvent(self, event): self.cam_thread.stop() event.accept() # 需要在UI主窗口中的QGraphicsView中代理显示的子窗口 class SonForm(QWidget): def __init__(self, parentNone): super().__init__(parent) self.setFixedSize(300,200) self.layout QVBoxLayout() self.btn_close QPushButton(Close SonForm, self) self.btn_close.setGeometry(90, 130, 120, 40) if __name__ __main__: app QApplication([]) # 子窗口 son_form SonForm() # 主窗口 master_form UIForm() # 代理 proxyItem ProxyItem(son_form) master_form.btn_add.clicked.connect(lambda :master_form.scene.addItem(proxyItem)) master_form.show() app.exec()