我正在使用 PyQt 和 PyQtGraph 构建一个相对简单的绘图 UI。作为其中的一部分,我有一个图形视图(pyqtgraph 的 graphicslayoutwidget),它有用户动态添加的 PlotItems。
我想要实现的是允许用户通过双击来选择一个 PlotItem。
如果用户双击了小部件窗口中的某处,这很简单,但我似乎无法弄清楚如何返回被点击的内容。
我的大部分搜索结果都试图为某些按钮重新实现 mousePressEvent。我读过一些关于事件过滤器的内容,但我不确定这是否是必要的解决方案。
我不确定还有哪些其他信息可能有助于回答这个问题,所以如果不清楚我在问什么,请告诉我,以便我澄清。
编辑:
这个的副本:
一种策略是连接到
GraphicsScene.sigMouseClicked
,然后询问场景哪些项目在鼠标光标下。
这应该让你走到那里:
import pyqtgraph as pg
w = pg.GraphicsWindow()
for i in range(4):
w.addPlot(0, i)
def onClick(event):
items = w.scene().items(event.scenePos())
print "Plots:", [x for x in items if isinstance(x, pg.PlotItem)]
w.scene().sigMouseClicked.connect(onClick)
在解决这个问题的很多麻烦之后,我发现
items
的方法 QGraphicsView
不能被粗心地使用,正如 stackoverflow 的许多答案中所报告的那样。
我想出了一个 custom 解决方案来获取最近的数据点到点击点。这是函数
class DSP:
@staticmethod
def map_to_nearest(x: np.ndarray, x0: Union[np.ndarray, float]) -> np.ndarray:
"""This methods takes an array of time values and map it to nearest value in nidaq time array
It returns the indices"""
if type(x0) == float:
x0 = np.array([x0])
# A bit of magic
x_tiled = np.tile(x, (x0.size, 1))
x0_tiled = np.tile(x0, (x.size, 1)).T
diff = np.abs(x_tiled - x0_tiled)
idx = np.argmin(diff, axis=1)
return idx
class MyPlotWidget(pg.PlotWidget):
def nearest_data_index_to_mouse_click(self, click_scene_pos: QPointF):
"""
:param click_scene_pos: The position of the mouse click in Scene coordinate
:return:
- int - Nearest point data index (or None)
- view_rect (A rectangle in data coordinates of pixel_dist (equivalent scene coordinates) side
"""
# QPoint to numpy array
qpoint2np = lambda x: np.array([x.x(), x.y()])
# Filter out all not data-driven items in the list. Must be customized and improved!
get_data_items_only = lambda items: [item for item in items if
any([isinstance(item, x) for x in [pg.PlotDataItem, pg.PlotCurveItem, pg.ScatterPlotItem]])]
# Half side of the rectangular ROI around the click point
pixel_dist = 5
# Numpy click point
p_click = qpoint2np(self.plot_item.mapToView(click_scene_pos))
# Rectangle ROI in scene (pixel) coordinates
scene_rect = QRectF(click_scene_pos.x() - pixel_dist, click_scene_pos.y() - pixel_dist, 2*pixel_dist, 2*pixel_dist)
# Rectangle ROI in data coordinates - NB: transforming from scene_rect to view_rect allows for the different x-y scaling!
view_rect: QRectF = self.getPlotItem().mapRectToView(scene_rect)
# Get all items canonically intercepted thourgh the methods already discussed by other answers
items = get_data_items_only(self.scene().items(scene_rect))
if len(items) == 0:
return None, None, view_rect
# Make your own decisional criterion
item = items[0]
# p0: bottom-left p1: upper-right view_rect items (DO NOT USE bottomLeft() and topRight()! The scene coordinates are different!
# Y axis is upside-down
p0 = np.array([view_rect.x(), view_rect.y() - view_rect.height()])
p1 = np.array([view_rect.x() + view_rect.width(), view_rect.y()])
# Limit the analysis to the same x-interval as the ROI
_x_limits = np.array([p0[0], p1[0]])
_item_data_x, _item_data_y = item.getData()
xi = DSP.map_to_nearest(_item_data_x, _x_limits)
# If the point is out of the interval
if xi.size == 0:
return None, None, view_rect
xi = np.arange(xi[0], xi[1]+1).astype(np.int_)
x, y = _item_data_x[xi], _item_data_y[xi]
# (2,1) limited item data array
_item_data = np.array([x,y])
subitem = pg.PlotCurveItem(x=x, y=y)
# Now intersects is used again, but this time the path is limited to a few points near the click! Some error might remains, but it may works well in most cases
if subitem.getPath().intersects(view_rect):
# Find nearest point
delta = _item_data - p_click.reshape(2,1)
min_dist_arg = np.argmin(np.linalg.norm(delta, axis=0))
return item, xi[min_dist_arg], view_rect
# View_rect is returned just to allow me to plot the ROI for debug reason
return None, None, view_rect
实现自定义数据提示
TextItem
,这是结果:
注意:
scene_rect
是自动转换的,但到目前为止我不知道 pyqtgraph
如何处理 PlotCurveItemPlotDataItem
,它也会变成一对(ScatterPlotItem
和CurvePlotItem
)来处理情节的线条和符号(标记)首先,虽然没有记录,但
QGraphicsView::items()
似乎很可能实现或使用方法QPainterPath::intersects()
。检查此方法的文档(以 QRectF 作为参数):
通过运行一些测试脚本,如果组成矩形的任何一条线穿过路径的一部分,或者如果矩形的任何部分与路径包围的任何区域重叠,则存在交叉点。
QPainterPath
似乎正在考虑always 一条封闭路径,可能是通过将最后一点与第一个点连接起来。事实上,下面的脚本:
from PySide2.QtCore import QPoint, QRectF
from PySide2.QtGui import QPainterPath, QPicture, QPainter
from PySide2.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import numpy as np
app = QApplication([])
# Path1 made of two lines
x = [0, 5, 0]
y = [0, 5, 6]
path1 = pg.PlotCurveItem(x, y, name='Path1')
rect = QRectF(1,4,1,1)
# RectItem (here omitted) is taken from https://stackoverflow.com/questions/60012070/drawing-a-rectangle-in-pyqtgraph
rect_item = RectItem(rect)
pw = pg.PlotWidget()
pw.addItem(path1)
pw.addItem(rect_item)
text = f'path1.getPath().intersects(rect): {path1.getPath().intersects(rect)}'
pw.addItem(pg.TextItem(text))
# Need to replicate the item
rect_item = RectItem(rect)
path2 =pg.PlotCurveItem(x=[0,5,4], y=[0,5,6])
pw2 = pg.PlotWidget()
pw2.addItem(path2)
pw2.addItem(rect_item)
text = f'path2.getPath().intersects(rect): {path2.getPath().intersects(rect)}'
pw2.addItem(pg.TextItem(text))
pw.show()
pw2.show()
app.exec_()
获取此图和结果作为输出:
q_label = QLabel("MyLabel")
q_label.mousePressEvent = lambda e: on_name_clicked(q_label)
def on_name_clicked(self, q_label):
print(q_label.text())