def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = QGraphicsLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) line.setLine(start.x(), start.y(), event.pos().x(), event.pos().y()) pen = QPen(Qt.black, 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() line.setP2(event.pos()) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event)
class DSO_grid(QGraphicsItemGroup): def __init__(self, parent=None): super().__init__() self.showGrid() # self.hide() def showGrid(self): self.color = QColor() pen = QPen() self.color.setNamedColor("white") pen.setColor(self.color) # we will generate DSO screen grid for x in range(20, 521, 10): for y in range(20, 421, 50): self.point = QGraphicsLineItem(self) if y == 20: self.point.setLine(x, y, x, y + 3) elif y == 220: self.point.setLine(x, y - 2, x, y + 2) elif y == 420: self.point.setLine(x, y, x, y - 3) else: self.point.setLine(x, y, x, y) self.point.setPen(pen) self.point.show() for x in range(20, 521, 50): for y in range(20, 421, 10): self.point = QGraphicsLineItem(self) if x == 20: self.point.setLine(x, y, x + 3, y) elif x == 270: self.point.setLine(x - 2, y, x + 2, y) elif x == 520: self.point.setLine(x, y, x - 3, y) else: self.point.setLine(x, y, x, y) self.point.setPen(pen) self.point.show()
class ChartView(QChartView): def __init__(self, *args, **kwargs): super(ChartView, self).__init__(*args, **kwargs) self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 self.initChart() # 提示widget self.toolTipWidget = GraphicsProxyWidget(self._chart) # line 宽度需要调整 self.lineItem = QGraphicsLineItem(self._chart) pen = QPen(Qt.gray) self.lineItem.setPen(pen) self.lineItem.setZValue(998) self.lineItem.hide() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.category_len = len(axisX.categories()) self.min_x, self.max_x = -0.5, self.category_len - 0.5 self.min_y, self.max_y = axisY.min(), axisY.max() # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) pos = event.pos() # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(pos).x() y = self._chart.mapToValue(pos).y() index = round(x) # 得到在坐标系中的所有bar的类型和点 serie = self._chart.series()[0] bars = [(bar, bar.at(index)) for bar in serie.barSets() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if bars: right_top = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) # 等分距离比例 step_x = round( (right_top.x() - self.point_top.x()) / self.category_len) posx = self._chart.mapToPosition(QPointF(x, self.min_y)) self.lineItem.setLine(posx.x(), self.point_top.y(), posx.x(), posx.y()) self.lineItem.show() try: title = self.categories[index] except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show( title, bars, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide() def handleMarkerClicked(self): marker = self.sender() # 信号发送者 if not marker: return bar = marker.barset() if not bar: return # bar透明度 brush = bar.brush() color = brush.color() alpha = 0.0 if color.alphaF() == 1.0 else 1.0 color.setAlphaF(alpha) brush.setColor(color) bar.setBrush(brush) # marker brush = marker.labelBrush() color = brush.color() alpha = 0.4 if color.alphaF() == 1.0 else 1.0 # 设置label的透明度 color.setAlphaF(alpha) brush.setColor(color) marker.setLabelBrush(brush) # 设置marker的透明度 brush = marker.brush() color = brush.color() color.setAlphaF(alpha) brush.setColor(color) marker.setBrush(brush) def handleMarkerHovered(self, status): # 设置bar的画笔宽度 marker = self.sender() # 信号发送者 if not marker: return bar = marker.barset() if not bar: return pen = bar.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) bar.setPen(pen) def handleBarHoverd(self, status, index): # 设置bar的画笔宽度 bar = self.sender() # 信号发送者 pen = bar.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) bar.setPen(pen) def initChart(self): self._chart = QChart(title="柱状图堆叠") self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) self.categories = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] # names = ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"] names = ["邮件营销", ] series = QBarSeries(self._chart) for name in names: bar = QBarSet(name) # 随机数据 for _ in range(7): bar.append(randint(0, 10)) series.append(bar) bar.hovered.connect(self.handleBarHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 # x轴 axis_x = QBarCategoryAxis(self._chart) axis_x.append(self.categories) self._chart.setAxisX(axis_x, series) # chart的图例 legend = self._chart.legend() legend.setVisible(True) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart)
class ChartView(QChartView): def __init__(self, *args, **kwargs): super(ChartView, self).__init__(*args, **kwargs) self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 self.initChart() self.toolTipWidget = GraphicsProxyWidget(self._chart) # line self.lineItem = QGraphicsLineItem(self._chart) self.lineItem.setZValue(998) self.lineItem.hide() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition(QPointF(self.min_x, self.max_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition(QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / (axisX.tickCount() - 1) # self.step_y = (self.max_y - self.min_y) / (axisY.tickCount() - 1) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(event.pos()).x() y = self._chart.mapToValue(event.pos()).y() index = round((x - self.min_x) / self.step_x) pos_x = self._chart.mapToPosition( QPointF(index * self.step_x + self.min_x, self.min_y)) # print(x, pos_x, index, index * self.step_x + self.min_x) # 得到在坐标系中的所有series的类型和点 points = [(serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if points: # 永远在轴上的黑线条 self.lineItem.setLine(pos_x.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.lineItem.show() self.toolTipWidget.show("", points, event.pos() + QPoint(20, 20)) else: self.toolTipWidget.hide() self.lineItem.hide() def onSeriesHoverd(self, point, state): if state: try: name = self.sender().name() except: name = "" QToolTip.showText(QCursor.pos(), "%s\nx: %s\ny: %s" % (name, point.x(), point.y())) def initChart(self): self._chart = QChart(title="Line Chart") self._chart.setAcceptHoverEvents(True) dataTable = [ [120, 132, 101, 134, 90, 230, 210], [220, 182, 191, 234, 290, 330, 310], [150, 232, 201, 154, 190, 330, 410], [320, 332, 301, 334, 390, 330, 320], [820, 932, 901, 934, 1290, 1330, 1320] ] for i, data_list in enumerate(dataTable): series = QLineSeries(self._chart) for j, v in enumerate(data_list): series.append(j, v) series.setName("Series " + str(i)) series.setPointsVisible(True) # 显示原点 series.hovered.connect(self.onSeriesHoverd) self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 self._chart.axisX().setTickCount(7) # x轴设置7个刻度 self._chart.axisY().setTickCount(7) # y轴设置7个刻度 self._chart.axisY().setRange(0, 1500) # 设置y轴范围 self.setChart(self._chart)
class ChartView(QChartView): def __init__(self, *args, **kwargs): super(ChartView, self).__init__(*args, **kwargs) self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 # 自定义x轴label self.category = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] self.initChart() # 提示widget self.toolTipWidget = GraphicsProxyWidget(self._chart) # line self.lineItem = QGraphicsLineItem(self._chart) pen = QPen(Qt.gray) pen.setWidth(1) self.lineItem.setPen(pen) self.lineItem.setZValue(996) self.lineItem.hide() self.lineItemX = QGraphicsLineItem(self._chart) penx = QPen(Qt.gray) penx.setWidth(1) self.lineItemX.setPen(penx) self.lineItemX.setZValue(997) self.lineItemX.hide() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() def resizeEvent(self, event): super(ChartView, self).resizeEvent(event) # 当窗口大小改变时需要重新计算 # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # 坐标右上点 self.point_right_top = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) # 坐标右下点 self.point_right_bottom = self._chart.mapToPosition( QPointF(self.max_x, self.min_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / \ (self._chart.axisX().tickCount() - 1) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) pos = event.pos() # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(pos).x() y = self._chart.mapToValue(pos).y() # 根据间隔来确定鼠标当前所在的索引 index = round((x - self.min_x) / self.step_x) # 得到在坐标系中的所有正常显示的series的类型和点 points = [ (serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y ] if points: # 根据鼠标的点获取对应曲线的坐标点, # pos_x = self._chart.mapToPosition( # QPointF(index * self.step_x + self.min_x, self.min_y)) # 设置鼠标的垂直线 self.lineItem.setLine(pos.x(), self.point_top.y(), pos.x(), self.point_bottom.y()) self.lineItem.show() # 设置鼠标的水平线 self.lineItemX.setLine(self.point_top.x(), pos.y(), self.point_right_top.x(), pos.y()) self.lineItemX.show() try: title = self.category[index] except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show(title, points, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide() self.lineItemX.hide() def handleMarkerClicked(self): marker = self.sender() # 信号发送者 if not marker: return visible = not marker.series().isVisible() # 隐藏或显示series marker.series().setVisible(visible) marker.setVisible(True) # 要保证marker一直显示 # 透明度 alpha = 1.0 if visible else 0.4 # 设置label的透明度 brush = marker.labelBrush() color = brush.color() color.setAlphaF(alpha) brush.setColor(color) marker.setLabelBrush(brush) # 设置marker的透明度 brush = marker.brush() color = brush.color() color.setAlphaF(alpha) brush.setColor(color) marker.setBrush(brush) # 设置画笔透明度 pen = marker.pen() color = pen.color() color.setAlphaF(alpha) pen.setColor(color) marker.setPen(pen) def handleMarkerHovered(self, status): # 设置series的画笔宽度 marker = self.sender() # 信号发送者 if not marker: return series = marker.series() if not series: return pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) series.setPen(pen) def handleSeriesHoverd(self, point, state): # 设置series的画笔宽度 series = self.sender() # 信号发送者 pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if state else -1)) series.setPen(pen) def initChart(self): self._chart = QChart(title="折线图堆叠") self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) dataTable = [["邮件营销", [120, 132, 101, 134, 90, 230, 210]], ["联盟广告", [220, 182, 191, 234, 290, 330, 310]], ["视频广告", [150, 232, 201, 154, 190, 330, 410]], ["直接访问", [320, 332, 301, 334, 390, 330, 320]], ["搜索引擎", [820, 932, 901, 934, 1290, 1330, 1320]]] for series_name, data_list in dataTable: series = QLineSeries(self._chart) for j, v in enumerate(data_list): series.append(j, v) series.setName(series_name) series.setPointsVisible(True) # 显示圆点 series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 axisX = self._chart.axisX() # x轴 axisX.setTickCount(7) # x轴设置7个刻度 axisX.setGridLineVisible(False) # 隐藏从x轴往上的线条 axisY = self._chart.axisY() axisY.setTickCount(7) # y轴设置7个刻度 axisY.setRange(0, 1500) # 设置y轴范围 # 自定义x轴 axis_x = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) axis_x.setTickCount(7) axis_x.setGridLineVisible(False) min_x = axisX.min() max_x = axisX.max() step = (max_x - min_x) / (7 - 1) # 7个tick for i in range(0, 7): axis_x.append(self.category[i], min_x + i * step) self._chart.setAxisX(axis_x, self._chart.series()[-1]) # self._chart.addAxis(axis_x, Qt.AlignBottom) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart)
class ImageViewer(QGraphicsView, QObject): points_selection_sgn = pyqtSignal(list) key_press_sgn = pyqtSignal(QtGui.QKeyEvent) def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setDragMode(QGraphicsView.ScrollHandDrag) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._image = None self._image_original = None self._pixmap = None self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._create_grid() self._channels = [] self._current_tool = SELECTION_TOOL.POINTER self._dataset = None # create grid lines pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) self._current_label = None # rectangle selection tool self._rectangle_tool_origin = QPoint() self._rectangle_tool_picker = QRubberBand(QRubberBand.Rectangle, self) # polygon selection tool app = QApplication.instance() color = app.palette().color(QPalette.Highlight) self._polygon_guide_line_pen = QPen(color) self._polygon_guide_line_pen.setWidth(3) self._polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # circle self._current_ellipse = None # free selection tool self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._last_click_point = None self._free_Path_pen = QPen(color) self._free_Path_pen.setWidth(10) self._extreme_points = Queue(maxsize=4) @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value if self._current_label: color = QColor(self._current_label.color) self._free_Path_pen.setColor(color) self._polygon_guide_line_pen.setColor(color) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) @property def dataset(self): return self._dataset @dataset.setter def dataset(self, value): self._dataset = value @property def img_contrast(self): return self._img_contrast @img_contrast.setter def img_contrast(self, value): self._img_contrast = value @property def img_gamma(self): return self._img_gamma @img_gamma.setter def img_gamma(self, value): self._img_gamma = value @property def img_brightness(self): return self._img_brightness @img_brightness.setter def img_brightness(self, value): self._img_brightness = value @property def image(self): return self._image @image.setter def image(self, value): self._image = value self._image_original = value.copy() self.update_viewer() @property def pixmap(self) -> ImagePixmap: return self._pixmap @gui_exception def update_viewer(self, fit_image=True): rgb = cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB) rgb = ImageUtilities.adjust_image(rgb, self._img_contrast, self._img_brightness) rgb = ImageUtilities.adjust_gamma(rgb, self._img_gamma) pil_image = Image.fromarray(rgb) qppixmap_image = pil_image.toqpixmap() x, y = -qppixmap_image.width() / 2, -qppixmap_image.height() / 2 if self._pixmap: self._pixmap.resetTransform() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) else: self._pixmap = ImagePixmap() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) self._scene.addItem(self._pixmap) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._hide_guide_lines() if fit_image: self.fit_to_window() @gui_exception def reset_viewer(self): self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._image = self._image_original.copy() @gui_exception def equalize_histogram(self): self._image = ImageUtilities.histogram_equalization(self._image) @gui_exception def correct_lightness(self): self._image = ImageUtilities.correct_lightness(self._image) def clusterize(self, k): self._image = ImageUtilities.kmeans(self._image.copy(), k) @property def current_tool(self): return self._current_tool @current_tool.setter def current_tool(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._current_ellipse = None self._is_drawing = value == SELECTION_TOOL.FREE self._current_tool = value self.clear_extreme_points() if value == SELECTION_TOOL.POINTER: self.enable_items(True) else: self.enable_items(False) def fit_to_window(self): if not self._pixmap or not self._pixmap.pixmap(): return self.resetTransform() self.setTransform(QtGui.QTransform()) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def _create_grid(self, gridSize=15): app: QApplication = QApplication.instance() curr_theme = "dark" if app: curr_theme = app.property("theme") if curr_theme == "light": color1 = QtGui.QColor("white") color2 = QtGui.QColor(237, 237, 237) else: color1 = QtGui.QColor(20, 20, 20) color2 = QtGui.QColor(0, 0, 0) backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) backgroundPixmap.fill(color1) painter = QtGui.QPainter(backgroundPixmap) painter.fillRect(0, 0, gridSize, gridSize, color2) painter.fillRect(gridSize, gridSize, gridSize, gridSize, color2) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) @gui_exception def keyPressEvent(self, event: QKeyEvent): if event.key() == QtCore.Qt.Key_Space: image_rect: QRectF = self._pixmap.sceneBoundingRect() if self.current_tool == SELECTION_TOOL.POLYGON and self._current_polygon: points = self._current_polygon.points self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) if len(points) <= 2: self._current_polygon.delete_item() self.current_tool = SELECTION_TOOL.POINTER elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS and \ self._extreme_points.full(): points = [] image_offset = QPointF(image_rect.width() / 2, image_rect.height() / 2) for pt in self._extreme_points.queue: pt: EditablePolygonPoint center = pt.sceneBoundingRect().center() x = math.floor(center.x() + image_offset.x()) y = math.floor(center.y() + image_offset.y()) points.append([x, y]) self.points_selection_sgn.emit(points) self.current_tool = SELECTION_TOOL.POINTER else: event.ignore() # guide lines events def _show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def _hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def _update_guide_lines(self, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): self._update_guide_lines(x, y) def pixmap_hoverEnterEvent_slot(self): self._show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self._hide_guide_lines() def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.current_tool = SELECTION_TOOL.POINTER self._polygon_guide_line.hide() @gui_exception def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() mouse_pos = self.mapToScene(evt.pos()) if evt.buttons() == QtCore.Qt.LeftButton: if self.current_tool == SELECTION_TOOL.BOX: # create rectangle self.setDragMode(QGraphicsView.NoDrag) self._rectangle_tool_origin = evt.pos() geometry = QRect(self._rectangle_tool_origin, QSize()) self._rectangle_tool_picker.setGeometry(geometry) self._rectangle_tool_picker.show() elif self.current_tool == SELECTION_TOOL.POLYGON: if image_rect.contains(mouse_pos): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.label = self._current_label self._current_polygon.tag = self._dataset self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(mouse_pos) else: self._current_polygon.addPoint(mouse_pos) elif self.current_tool == SELECTION_TOOL.ELLIPSE: if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) ellipse_rec = QtCore.QRectF(mouse_pos.x(), mouse_pos.y(), 0, 0) self._current_ellipse = EditableEllipse() self._current_ellipse.tag = self.dataset self._current_ellipse.label = self._current_label self._current_ellipse.setRect(ellipse_rec) self._scene.addItem(self._current_ellipse) elif self.current_tool == SELECTION_TOOL.FREE: # consider only the points into the image if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) self._last_point_drawn = mouse_pos self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(self._free_Path_pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS: if image_rect.contains(mouse_pos): if not self._extreme_points.full(): def delete_point(idx): del self._extreme_points.queue[idx] idx = self._extreme_points.qsize() editable_pt = EditablePolygonPoint(idx) editable_pt.signals.deleted.connect(delete_point) editable_pt.setPos(mouse_pos) self._scene.addItem(editable_pt) self._extreme_points.put(editable_pt) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) @gui_exception def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: mouse_pos = self.mapToScene(evt.pos()) image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: if not self._rectangle_tool_origin.isNull(): geometry = QRect(self._rectangle_tool_origin, evt.pos()).normalized() self._rectangle_tool_picker.setGeometry(geometry) elif self.current_tool == SELECTION_TOOL.POLYGON: if self._current_polygon and image_rect.contains(mouse_pos): if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.current_tool == SELECTION_TOOL.ELLIPSE: if self._current_ellipse and image_rect.contains(mouse_pos): ellipse_rect = self._current_ellipse.rect() ellipse_pos = QPointF(ellipse_rect.x(), ellipse_rect.y()) distance = math.hypot(mouse_pos.x() - ellipse_pos.x(), mouse_pos.y() - ellipse_pos.y()) ellipse_rect.setWidth(distance) ellipse_rect.setHeight(distance) self._current_ellipse.setRect(ellipse_rect) elif self.current_tool == SELECTION_TOOL.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path and image_rect.contains(mouse_pos): painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) @gui_exception def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: roi: QRect = self._rectangle_tool_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() self._rectangle_tool_picker.hide() if image_rect == roi.united(image_rect): rect = EditableBox(roi) rect.label = self.current_label rect.tag = self._dataset self._scene.addItem(rect) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.current_tool == SELECTION_TOOL.ELLIPSE and self._current_ellipse: roi: QRect = self._current_ellipse.boundingRect() if image_rect == roi.united(image_rect): self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) else: self._current_ellipse.delete_item() elif self.current_tool == SELECTION_TOOL.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() if image_rect == path_rect.united(image_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.tag = self.dataset path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableItem): item.delete_item() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableItem): if item.label and item.label.name == label_name: item.delete_item() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditableItem): item.setEnabled(value) def clear_extreme_points(self): if self._extreme_points.qsize() > 0: for pt in self._extreme_points.queue: self._scene.removeItem(pt) self._extreme_points.queue.clear()
class ImageViewer(QGraphicsView, QObject): def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) #self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setDragMode(QGraphicsView.ScrollHandDrag) #self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._create_grid() self._create_grid_lines() self._pixmap = None self._selection_mode = SELECTION_MODE.NONE # polygon selection _polygon_guide_line_pen = QPen(QtGui.QColor(235, 72, 40)) _polygon_guide_line_pen.setWidth(2) _polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(_polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # rectangle selection self._box_origin = QPoint() self._box_picker = QRubberBand(QRubberBand.Rectangle, self) # free selection self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._current_label = None @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value @property def pixmap(self) -> ImagePixmap: return self._pixmap @pixmap.setter def pixmap(self, value: QPixmap): self.selection_mode = SELECTION_MODE.NONE self.resetTransform() if self.pixmap: self._scene.removeItem(self._pixmap) self.remove_annotations() self._pixmap = ImagePixmap() self._pixmap.setPixmap(value) self._pixmap.setOffset(-value.width() / 2, -value.height() / 2) self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._scene.addItem(self._pixmap) # rect=self._scene.addRect(QtCore.QRectF(0,0,100,100), QtGui.QPen(QtGui.QColor("red"))) # rect.setZValue(1.0) self.fit_to_window() @property def selection_mode(self): return self._selection_mode @selection_mode.setter def selection_mode(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._is_drawing = value == SELECTION_MODE.FREE if value == SELECTION_MODE.NONE: self.enable_items(True) else: self.enable_items(False) self._selection_mode = value def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableBox): self._scene.removeItem(item) elif isinstance(item, EditablePolygon): item.delete_polygon() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableBox): if item.label and item.label.name == label_name: self._scene.removeItem(item) elif isinstance(item, EditablePolygon): if item.label and item.label.name == label_name: item.delete_polygon() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditablePolygon) or isinstance( item, EditableBox): item.setEnabled(value) def _create_grid(self): gridSize = 15 backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) #backgroundPixmap.fill(QtGui.QColor("white")) backgroundPixmap.fill(QtGui.QColor(20, 20, 20)) #backgroundPixmap.fill(QtGui.QColor("powderblue")) painter = QtGui.QPainter(backgroundPixmap) #backgroundColor=QtGui.QColor("palegoldenrod") #backgroundColor=QtGui.QColor(237,237,237) backgroundColor = QtGui.QColor(0, 0, 0) painter.fillRect(0, 0, gridSize, gridSize, backgroundColor) painter.fillRect(gridSize, gridSize, gridSize, gridSize, backgroundColor) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def _create_grid_lines(self): pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) def fit_to_window(self): """Fit image within view.""" if not self.pixmap or not self._pixmap.pixmap(): return #self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def pixmap_hoverEnterEvent_slot(self): self.show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self.hide_guide_lines() def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: if self.selection_mode == SELECTION_MODE.BOX: if not self._box_origin.isNull(): self._box_picker.setGeometry( QRect(self._box_origin, evt.pos()).normalized()) elif self.selection_mode == SELECTION_MODE.POLYGON: if self._current_polygon: if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.selection_mode == SELECTION_MODE.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path: painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.buttons() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: self.setDragMode(QGraphicsView.NoDrag) self._box_origin = evt.pos() self._box_picker.setGeometry(QRect(self._box_origin, QSize())) self._box_picker.show() elif self._selection_mode == SELECTION_MODE.POLYGON: pixmap_rect: QRectF = self._pixmap.boundingRect() new_point = self.mapToScene(evt.pos()) # consider only the points intothe image if pixmap_rect.contains(new_point): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(new_point) else: self._current_polygon.addPoint(new_point) elif self._selection_mode == SELECTION_MODE.FREE: # start drawing new_point = self.mapToScene(evt.pos()) pixmap_rect: QRectF = self._pixmap.boundingRect() # consider only the points intothe image if pixmap_rect.contains(new_point): self.setDragMode(QGraphicsView.NoDrag) pen = QPen(QtGui.QColor(235, 72, 40)) pen.setWidth(10) self._last_point_drawn = new_point self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: roi: QRect = self._box_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() pixmap_rect = self._pixmap.boundingRect() self._box_picker.hide() if pixmap_rect == roi.united(pixmap_rect): rect = EditableBox(roi) rect.label = self.current_label self._scene.addItem(rect) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.selection_mode == SELECTION_MODE.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() pixmap_rect = self._pixmap.boundingRect() if pixmap_rect == path_rect.united(pixmap_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if self._current_polygon and event.key() == QtCore.Qt.Key_Space: points = self._current_polygon.points self._current_polygon.label = self.current_label self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).keyPressEvent(event) def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide()
class RotaryDialHoverRegion(QGraphicsEllipseItem): def __init__(self, rect, parent=None): # setup DNA line super(QGraphicsEllipseItem, self).__init__(rect, parent) self._parent = parent self.setPen(QPen(Qt.NoPen)) self.setBrush(_HOVER_BRUSH) self.setAcceptHoverEvents(True) # hover marker self._hoverLine = QGraphicsLineItem(-_ROTARY_DELTA_WIDTH/2, 0, _ROTARY_DELTA_WIDTH/2, 0, self) self._hoverLine.setPen(QPen(QColor(204, 0, 0), .5)) self._hoverLine.hide() self._startPos = None self._startAngle = None # save selection start self._clockwise = None self.dummy = RotaryDialDeltaItem(0, 0, parent) self.dummy.hide() def updateRect(self, rect): self.setRect(rect) def hoverEnterEvent(self, event): self.updateHoverLine(event) self._hoverLine.show() # end def def hoverMoveEvent(self, event): self.updateHoverLine(event) # end def def hoverLeaveEvent(self, event): self._hoverLine.hide() # end def def mousePressEvent(self, event): r = _RADIUS self.updateHoverLine(event) pos = self._hoverLine.pos() aX, aY, angle = self.snapPosToCircle(pos, r) if angle != None: self._startPos = QPointF(aX, aY) self._startAngle = self.updateHoverLine(event) self.dummy.updateAngle(self._startAngle, 0) self.dummy.show() # mark the start # f = QGraphicsEllipseItem(pX, pY, 2, 2, self) # f.setPen(QPen(Qt.NoPen)) # f.setBrush(QBrush(QColor(204, 0, 0))) # end def def mouseMoveEvent(self, event): eventAngle = self.updateHoverLine(event) # Record initial direction before calling getSpanAngle if self._clockwise is None: self._clockwise = False if eventAngle > self._startAngle else True spanAngle = self.getSpanAngle(eventAngle) self.dummy.updateAngle(self._startAngle, spanAngle) # end def def mouseReleaseEvent(self, event): self.dummy.hide() endAngle = self.updateHoverLine(event) spanAngle = self.getSpanAngle(endAngle) old_angle = self._parent.virtualHelix().getProperty('eulerZ') new_angle = round((old_angle - spanAngle) % 360,0) self._parent.virtualHelix().setProperty('eulerZ', new_angle) # mark the end # x = self._hoverLine.x() # y = self._hoverLine.y() # f = QGraphicsEllipseItem(x, y, 6, 6, self) # f.setPen(QPen(Qt.NoPen)) # f.setBrush(QBrush(QColor(204, 0, 0, 128))) # end def def updateHoverLine(self, event): """ Moves red line to point (aX,aY) on RotaryDialLine closest to event.pos. Returns the angle of aX, aY, using the Qt arc coordinate system (0 = east, 90 = north, 180 = west, 270 = south). """ r = _RADIUS aX, aY, angle = self.snapPosToCircle(event.pos(), r) if angle != None: self._hoverLine.setPos(aX, aY) self._hoverLine.setRotation(-angle) return angle # end def def snapPosToCircle(self, pos, radius): """Given x, y and radius, return x,y of nearest point on circle, and its angle""" pX = pos.x() pY = pos.y() cX = cY = radius vX = pX - cX vY = pY - cY magV = sqrt(vX*vX + vY*vY) if magV == 0: return (None, None, None) aX = cX + vX / magV * radius aY = cY + vY / magV * radius angle = (atan2(aY-cY, aX-cX)) deg = -degrees(angle) if angle < 0 else 180+(180-degrees(angle)) return (aX, aY, deg) # end def def getSpanAngle(self, angle): """ Return the spanAngle angle by checking the initial direction of the selection. Selections that cross 0° must be handed as an edge case. """ if self._clockwise: # spanAngle is negative if angle < self._startAngle: spanAngle = angle - self._startAngle else: spanAngle = -(self._startAngle + (360-angle)) else: # counterclockwise, spanAngle is positive if angle > self._startAngle: spanAngle = angle - self._startAngle else: spanAngle = (360-self._startAngle) + angle return spanAngle
class barChartView(QChartView): def __init__(self, xAxis=[], *args, **kwargs): super(barChartView, self).__init__(*args, **kwargs) self.initChart(xAxis) # line 宽度需要调整 self.lineItem = QGraphicsLineItem(self._chart) pen = QPen(Qt.gray) self.lineItem.setPen(pen) self.lineItem.setZValue(998) self.lineItem.hide() self.cal() # 一些固定计算,减少mouseMoveEvent中的计算量 def cal(self): # 提示widget self.toolTipWidget = GraphicsProxyWidget(self._chart) # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.category_len = len(axisX.categories()) self.min_x, self.max_x = -0.5, self.category_len - 0.5 self.min_y, self.max_y = axisY.min(), axisY.max() # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) def setCat(self, data): self.categories = data #初始化 def initChart(self, xAxis): self._chart = QChart() # 调整边距 self._chart.layout().setContentsMargins(0, 0, 0, 0) # 外界 self._chart.setMargins(QMargins(3, 0, 3, 0)) # 内界 self._chart.setBackgroundRoundness(0) self._chart.setBackgroundVisible(False) # 设置主题 self._chart.setTheme(QChart.ChartThemeBlueIcy) # 抗锯齿 self.setRenderHint(QPainter.Antialiasing) # 开启动画效果 self._chart.setAnimationOptions(QChart.SeriesAnimations) self.categories = xAxis self._series = QBarSeries(self._chart) self._chart.addSeries(self._series) self._chart.createDefaultAxes() # 创建默认的轴 self._axis_x = QBarCategoryAxis(self._chart) self._axis_x.append(self.categories) self._axis_y = QValueAxis(self._chart) self._axis_y.setTitleText("任务数") self._axis_y.setRange(0, 10) self._chart.setAxisX(self._axis_x, self._series) self._chart.setAxisY(self._axis_y, self._series) # chart的图例 legend = self._chart.legend() legend.setVisible(True) self.setChart(self._chart) def mouseMoveEvent(self, event): super(barChartView, self).mouseMoveEvent(event) pos = event.pos() # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(pos).x() y = self._chart.mapToValue(pos).y() index = round(x) # 得到在坐标系中的所有bar的类型和点 serie = self._chart.series()[0] bars = [ (bar, bar.at(index)) for bar in serie.barSets() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y ] # print(bars) if bars: right_top = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) # 等分距离比例 step_x = round( (right_top.x() - self.point_top.x()) / self.category_len) posx = self._chart.mapToPosition(QPointF(x, self.min_y)) self.lineItem.setLine(posx.x(), self.point_top.y(), posx.x(), posx.y()) self.lineItem.show() try: title = self.categories[index] except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show(title, bars, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide()
class IMUChartView(QChartView, BaseGraph): def __init__(self, parent=None): super().__init__(parent=parent) # Render on OpenGL self.setViewport(QOpenGLWidget()) self.xvalues = {} self.chart = QChart() self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) # Cursor (with chart as parent) self.cursor = QGraphicsLineItem(self.chart) self.cursor.setZValue(100.0) # self.scene().addItem(self.cursor) # Selection features # self.setRubberBand(QChartView.HorizontalRubberBand) self.selectionBand = QRubberBand(QRubberBand.Rectangle, self) self.selecting = False self.initialClick = QPoint() # Track mouse self.setMouseTracking(True) self.labelValue = QLabel(self) self.labelValue.setStyleSheet( "background-color: rgba(255,255,255,75%); color: black;") self.labelValue.setAlignment(Qt.AlignCenter) self.labelValue.setMargin(5) self.labelValue.setVisible(False) self.build_style() self.selection_start_time = None self.selection_stop_time = None self.cursor_time = None def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.chart.setTheme(QChart.ChartThemeBlueCerulean) self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') # def closeEvent(self, event): # self.aboutToClose.emit(self) @staticmethod def decimate(xdata, ydata): # assert(len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 # decimate_factor = 1.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) # print('decimate factor', decimate_factor) x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) # for i in range(len(x)): for i, _ in enumerate(x): index = i * decimate_factor # assert(index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min_value, max_value): # print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X axisx = QDateTimeAxis() axisx.setTickCount(5) axisx.setFormat("dd MMM yyyy hh:mm:ss") axisx.setTitleText("Date") self.chart.addAxis(axisx, Qt.AlignBottom) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisx) series.attachAxis(axisY) vect = series.pointsVector() # for i in range(len(vect)): for i, _ in enumerate(vect): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable # axisx.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) # curve.setPointsVisible(True) # curve.setUseOpenGL(True) self.total_samples = max(self.total_samples, len(xdata)) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) # self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) # if len(xdecimated) > 0: # xdecimated = xdecimated - xdecimated[0] xdecimated *= 1000 # No decimal expected points = [] for i, _ in enumerate(xdecimated): # TODO hack # curve.append(QPointF(xdecimated[i], ydecimated[i])) points.append(QPointF(xdecimated[i], ydecimated[i])) curve.replace(points) points.clear() if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) self.xvalues[self.ncurves] = np.array(xdecimated) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def update_data(self, xdata, ydata, series_id): if series_id < len(self.chart.series()): # Find start time to replace data current_series = self.chart.series()[series_id] current_points = current_series.pointsVector() # Find start and end indexes start_index = -1 try: start_index = current_points.index( QPointF(xdata[0] * 1000, ydata[0])) # Right on the value! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x() == xdata[0] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[0] * 1000 < current_points[i].x()): start_index = i # print("update_data: start_index found approximative match.") break end_index = -1 try: end_index = current_points.index( QPointF(xdata[len(xdata) - 1] * 1000, ydata[len(ydata) - 1])) # Right on! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x( ) == xdata[len(xdata) - 1] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[len(xdata) - 1] * 1000 < current_points[i].x()): end_index = i # print("update_data: start_index found approximative match.") break if start_index < 0 or end_index < 0: return # Decimate, if needed xdata, ydata = self.decimate(xdata, ydata) # Check if we have the same number of points for that range. If not, remove and replace! target_points = current_points[start_index:end_index] if len(target_points) != len(xdata): points = [] for i, value in enumerate(xdata): # TODO improve points.append(QPointF(value * 1000, ydata[i])) new_points = current_points[0:start_index] + points[0:len(points)-1] + \ current_points[end_index:len(current_points)-1] current_series.replace(new_points) new_points.clear() # self.xvalues[series_id] = np.array(xdata) return @classmethod def set_title(cls, title): # print('Setting title: ', title) # self.chart.setTitle(title) return @staticmethod def series_to_polyline(xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) # for i in range(0, len(xdata)): # polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) for i, data in enumerate(xdata): polyline[i] = QPointF(data - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title( "Simple example with %d curves of %d points (OpenGL Accelerated Series)" % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): if self.selecting: clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) clicked_y = max( self.chart.plotArea().y(), min( self.mapToScene(e.pos()).y(), self.chart.plotArea().y() + self.chart.plotArea().height())) current_pos = QPoint(clicked_x, clicked_y) # e.pos() self.selection_stop_time = self.chart.mapToValue( QPointF(clicked_x, 0)).x() self.setCursorPosition(clicked_x, True) if self.interaction_mode == GraphInteractionMode.SELECT: if current_pos.x() < self.initialClick.x(): start_x = current_pos.x() width = self.initialClick.x() - start_x else: start_x = self.initialClick.x() width = current_pos.x() - self.initialClick.x() self.selectionBand.setGeometry( QRect(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.selection_rec: self.selection_rec.setRect( QRectF(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.interaction_mode == GraphInteractionMode.MOVE: new_pos = current_pos - self.initialClick self.chart.scroll(-new_pos.x(), new_pos.y()) self.initialClick = current_pos def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands # super().mousePressEvent(e) # Verify if click is inside plot area # if self.chart.plotArea().contains(e.pos()): self.selecting = True clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) clicked_y = max( self.chart.plotArea().y(), min( self.mapToScene(e.pos()).y(), self.chart.plotArea().y() + self.chart.plotArea().height())) self.initialClick = QPoint(clicked_x, clicked_y) # e.pos() self.selection_start_time = self.chart.mapToValue(QPointF( clicked_x, 0)).x() self.setCursorPosition(clicked_x, True) if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.setGeometry( QRect(self.initialClick.x(), self.chart.plotArea().y(), 1, self.chart.plotArea().height())) self.selectionBand.show() if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.setOverrideCursor(Qt.ClosedHandCursor) self.labelValue.setVisible(False) def mouseReleaseEvent(self, e: QMouseEvent): # Assure if click is inside plot area # Handling rubberbands (with min / max x) clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.hide() if clicked_x != self.mapToScene(self.initialClick).x(): mapped_x = self.mapToScene(self.initialClick).x() if self.initialClick.x() < clicked_x: self.setSelectionArea(mapped_x, clicked_x, True) else: self.setSelectionArea(clicked_x, mapped_x, True) if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.restoreOverrideCursor() self.selecting = False def clearSelectionArea(self, emit_signal=False): if self.selection_rec: self.scene().removeItem(self.selection_rec) self.selection_rec = None if emit_signal: self.clearedSelectionArea.emit() def setSelectionArea(self, start_pos, end_pos, emit_signal=False): selection_brush = QBrush(QColor(153, 204, 255, 128)) selection_pen = QPen(Qt.transparent) if self.selection_rec: self.scene().removeItem(self.selection_rec) self.selection_rec = self.scene().addRect( start_pos, self.chart.plotArea().y(), end_pos - start_pos, self.chart.plotArea().height(), selection_pen, selection_brush) if emit_signal: self.selectedAreaChanged.emit( self.chart.mapToValue(QPointF(start_pos, 0)).x(), self.chart.mapToValue(QPointF(end_pos, 0)).x()) def setSelectionAreaFromTime(self, start_time, end_time, emit_signal=False): # Convert times to x values if isinstance(start_time, datetime.datetime): start_time = start_time.timestamp() * 1000 if isinstance(end_time, datetime.datetime): end_time = end_time.timestamp() * 1000 start_pos = self.chart.mapToPosition(QPointF(start_time, 0)).x() end_pos = self.chart.mapToPosition(QPointF(end_time, 0)).x() self.setSelectionArea(start_pos, end_pos) def setCursorPosition(self, pos, emit_signal=False): self.cursor_time = self.chart.mapToValue(QPointF(pos, 0)).x() # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap_initial = self.chart.mapToValue(QPointF(pos, 0)).x() display = '' # '<i>' + (datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())).strftime('%d-%m-%Y %H:%M:%S') + # '</i><br />' ypos = 10 last_val = None for i in range(self.ncurves): # Find nearest point idx = (np.abs(self.xvalues[i] - xmap_initial)).argmin() ymap = self.chart.series()[i].at(idx).y() xmap = self.chart.series()[i].at(idx).x() if i == 0: display += "<i>" + (datetime.datetime.fromtimestamp(xmap_initial/1000)).strftime('%d-%m-%Y %H:%M:%S:%f') + \ "</i>" # Compute where to display label if last_val is None or ymap > last_val: last_val = ymap ypos = self.chart.mapToPosition(QPointF(xmap, ymap)).y() if display != '': display += '<br />' display += self.chart.series()[i].name( ) + ': <b>' + '%.3f' % ymap + '</b>' self.labelValue.setText(display) self.labelValue.setGeometry(pos, ypos, 100, 100) self.labelValue.adjustSize() self.labelValue.setVisible(True) if emit_signal: self.cursorMoved.emit(xmap_initial) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Find nearest point if isinstance(timestamp, datetime.datetime): timestamp = timestamp.timestamp() pos = self.get_pos_from_time(timestamp) self.setCursorPosition(pos, emit_signal) def get_pos_from_time(self, timestamp): px = timestamp idx1 = (np.abs(self.xvalues[0] - px)).argmin() x1 = self.chart.series()[0].at(idx1).x() pos1 = self.chart.mapToPosition(QPointF(x1, 0)).x() idx2 = idx1 + 1 if idx2 < len(self.chart.series()[0]): x2 = self.chart.series()[0].at(idx2).x() if x2 != x1: pos2 = self.chart.mapToPosition(QPointF(x2, 0)).x() x2 /= 1000 x1 /= 1000 pos = (((px - x1) / (x2 - x1)) * (pos2 - pos1)) + pos1 else: pos = pos1 else: pos = pos1 return pos def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # oldSize = e.oldSize() # newSize = e.size() # Update cursor from time if self.cursor_time: self.setCursorPositionFromTime(self.cursor_time / 1000.0) # Update selection if self.selection_rec: self.setSelectionAreaFromTime(self.selection_start_time, self.selection_stop_time) def zoom_in(self): self.chart.zoomIn() self.update_axes() def zoom_out(self): self.chart.zoomOut() self.update_axes() def zoom_area(self): if self.selection_rec: zoom_rec = self.selection_rec.rect() zoom_rec.setY(0) zoom_rec.setHeight(self.chart.plotArea().height()) self.chart.zoomIn(zoom_rec) self.clearSelectionArea(True) self.update_axes() def zoom_reset(self): self.chart.zoomReset() self.update_axes() def get_displayed_start_time(self): min_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() xmap = self.chart.mapToValue(QPointF(min_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) def get_displayed_end_time(self): max_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() max_x += self.chart.mapToScene( self.chart.plotArea()).boundingRect().width() xmap = self.chart.mapToValue(QPointF(max_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) @property def is_zoomed(self): return self.chart.isZoomed()
class IMUChartView(QChartView): aboutToClose = pyqtSignal(QObject) cursorMoved = pyqtSignal(datetime.datetime) def __init__(self, parent=None): super(QChartView, self).__init__(parent=parent) #self.setFixedHeight(400) #self.setMinimumHeight(500) """self.setMaximumHeight(700) self.setFixedHeight(700) self.setMinimumWidth(1500) self.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed)""" self.reftime = datetime.datetime.now() self.cursor = QGraphicsLineItem() self.scene().addItem(self.cursor) self.decim_factor = 1 # self.setScene(QGraphicsScene()) self.chart = QChart() # self.scene().addItem(self.chart) self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) self.setRubberBand(QChartView.HorizontalRubberBand) # X, Y label on bottom # self.xTextItem = QGraphicsSimpleTextItem(self.chart) # self.xTextItem.setText('X: ') # self.yTextItem = QGraphicsSimpleTextItem(self.chart) # self.yTextItem.setText('Y: ') # self.update_x_y_coords() # Track mouse self.setMouseTracking(True) # Top Widgets newWidget = QWidget(self) newLayout = QHBoxLayout() newLayout.setContentsMargins(0, 0, 0, 0) newWidget.setLayout(newLayout) #labelx = QLabel(self) #labelx.setText('X:') #self.labelXValue = QLabel(self) #labely = QLabel(self) #labely.setText('Y:') #self.labelYValue = QLabel(self) # Test buttons #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) # Spacer #newLayout.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) # Labels """newLayout.addWidget(labelx) newLayout.addWidget(self.labelXValue) self.labelXValue.setMinimumWidth(200) self.labelXValue.setMaximumWidth(200) newLayout.addWidget(labely) newLayout.addWidget(self.labelYValue) self.labelYValue.setMinimumWidth(200) self.labelYValue.setMaximumWidth(200) """ """if parent is not None: parent.layout().setMenuBar(newWidget) """ # self.layout() self.build_style() def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') def closeEvent(self, QCloseEvent): self.aboutToClose.emit(self) @pyqtSlot(QPointF) def lineseries_clicked(self, point): print('lineseries clicked', point) @pyqtSlot(QPointF) def lineseries_hovered(self, point): print('lineseries hovered', point) def update_x_y_coords(self): pass # self.xTextItem.setPos(self.chart.size().width() / 2 - 100, self.chart.size().height() - 40) # self.yTextItem.setPos(self.chart.size().width() / 2 + 100, self.chart.size().height() - 40) def decimate(self, xdata, ydata): assert (len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) #print('decimate factor', decimate_factor) # x = decimate(xdata, decimate_factor) # y = decimate(ydata, decimate_factor) self.decim_factor = decimate_factor x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) for i in range(len(x)): index = i * decimate_factor assert (index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) #print('return size', len(x), len(y), 'timestamp', x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min, max): #print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X # axisX = QDateTimeAxis() # axisX.setTickCount(5) # axisX.setFormat("dd MMM yyyy") # axisX.setTitleText("Date") # self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) axisX = QValueAxis() axisX.setTickCount(10) axisX.setLabelFormat("%li") axisX.setTitleText("Seconds") self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisX) series.attachAxis(axisY) vect = series.pointsVector() for i in range(len(vect)): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable axisX.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) #curve.setUseOpenGL(True) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) for i in range(len(xdecimated)): # TODO hack x = xdecimated[i] - xdecimated[0] curve.append(QPointF(x, ydecimated[i])) self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def set_title(self, title): # print('Setting title: ', title) #self.chart.setTitle(title) pass def series_to_polyline(self, xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) for i in range(0, len(xdata)): polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title("Simple example with %d curves of %d points " \ "(OpenGL Accelerated Series)" \ % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseMoveEvent(e) # Go back to seconds (instead of ms) """xmap = self.chart.mapToValue(e.pos()).x() ymap = self.chart.mapToValue(e.pos()).y() self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) self.labelYValue.setText(str(ymap))""" # self.xTextItem.setText('X: ' + str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) # self.yTextItem.setText('Y: ' + str(ymap)) def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands super().mousePressEvent(e) self.setCursorPosition(e.pos().x(), True) pass def setCursorPosition(self, pos, emit_signal=False): # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap = self.chart.mapToValue(QPointF(pos, 0)).x() ymap = self.chart.mapToValue(QPointF(pos, 0)).y() #self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) #self.labelYValue.setText(str(ymap)) if emit_signal: self.cursorMoved.emit( datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Converts timestamp to x value pos = self.chart.mapToPosition( QPointF((timestamp - self.reftime).total_seconds(), 0)).x() self.setCursorPosition(pos, emit_signal) def mouseReleaseEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseReleaseEvent(e) pass def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # Update cursor height area = self.chart.plotArea() line = self.cursor.line() self.cursor.setLine(line.x1(), area.y(), line.x2(), area.y() + area.height()) # self.scene().setSceneRect(0, 0, e.size().width(), e.size().height()) # Need to reposition X,Y labels self.update_x_y_coords()
class KVWidget(QWidget): """ 主页面 """ def __init__(self): super(KVWidget, self).__init__() self.setMouseTracking(True) # 获取数据 self.stocks = read_tick_data() self.k_view = KLineChartView(self.stocks[:100]) self.v_view = VLineChartView(self.stocks[:100]) self.k_view.candles_hovered.connect(self.on_series_hovered) self.v_view.bar_hovered.connect(self.on_series_hovered) btn_widget = QWidget() h_layout = QHBoxLayout() btn_clear = QPushButton('清除') btn_add = QPushButton('添加') h_layout.addStretch() h_layout.addWidget(btn_clear) h_layout.addWidget(btn_add) btn_widget.setLayout(h_layout) btn_clear.clicked.connect(self.on_clear_clicked) btn_add.clicked.connect(self.on_add_clicked) self.v_splitter = QSplitter(Qt.Vertical, self) self.v_splitter.addWidget(self.k_view) self.v_splitter.addWidget(self.v_view) self.v_splitter.addWidget(btn_widget) self.v_splitter.setStretchFactor(0, 3) self.v_splitter.setStretchFactor(1, 2) self.v_splitter.setStretchFactor(2, 1) layout = QVBoxLayout() # layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.v_splitter) self.setLayout(layout) # self.showMaximized() # 鼠标跟踪的十字线 pen = QPen() pen.setStyle(Qt.DotLine) pen.setColor(QColor(0x40, 0x99, 0xF5)) pen.setWidth(1) self.k_line_h = QGraphicsLineItem(self.k_view.chart()) self.k_line_v = QGraphicsLineItem(self.k_view.chart()) self.v_line_h = QGraphicsLineItem(self.v_view.chart()) self.v_line_v = QGraphicsLineItem(self.v_view.chart()) self.k_line_h.setPen(pen) self.k_line_v.setPen(pen) self.v_line_h.setPen(pen) self.v_line_v.setPen(pen) self.k_line_h.setZValue(100) self.k_line_v.setZValue(100) self.v_line_h.setZValue(100) self.v_line_v.setZValue(100) self.k_line_h.hide() self.k_line_v.hide() self.v_line_h.hide() self.v_line_v.hide() # 鼠标在图表的位置, 初始化在左上角 self.tool_tip_widget = GraphicsProxyWidget(self.k_view.chart()) self.k_zero_point = self.k_view.min_point() self.k_max_point = self.k_view.max_point() self.v_zero_point = self.v_view.min_point() self.v_max_point = self.v_view.max_point() self._hovered_pos_left = QPointF(self.k_zero_point.x(), self.k_max_point.y()) self._hovered_pos_right = QPointF( self.k_max_point.x() - self.tool_tip_widget.width(), self.k_max_point.y()) self._hovered_pos = self._hovered_pos_left # 鼠标在中心左边 self._is_left = True # 事件过滤 QApplication.instance().installEventFilter(self) # TODO: 向左右拖动图表能显示之前或之后的图表,且坐标跟着变化 # TODO: 能标准成本线,能计算指定两个点的涨幅度 def eventFilter(self, obj: QObject, event: QEvent): if event.type() == QEvent.MouseMove: pos = QMouseEvent(event).globalPos() k_chart_pos = self.k_view.chart().mapFromParent( self.k_view.mapFromGlobal(pos)) v_chart_pos = self.v_view.chart().mapFromParent( self.v_view.mapFromGlobal(pos)) self.k_zero_point = self.k_view.min_point() self.k_max_point = self.k_view.max_point() self.v_zero_point = self.v_view.min_point() self.v_max_point = self.v_view.max_point() self.update_lines(k_chart_pos, v_chart_pos) elif event.type() == QEvent.Resize and obj == self: self.update_margins() self.k_zero_point = self.k_view.min_point() self.k_max_point = self.k_view.max_point() return super(KVWidget, self).eventFilter(obj, event) def update_lines(self, k_chart_pos, v_chart_pos): """ 更新跟踪的十字线 """ if (self.k_zero_point.y() >= k_chart_pos.y() >= self.k_max_point.y()) \ and (self.k_zero_point.x() <= k_chart_pos.x() <= self.k_max_point.x()): self.k_line_h.setLine(self.k_zero_point.x(), k_chart_pos.y(), self.k_max_point.x(), k_chart_pos.y()) self.k_line_v.setLine(k_chart_pos.x(), self.k_max_point.y(), k_chart_pos.x(), self.k_zero_point.y()) self.v_line_v.setLine(v_chart_pos.x(), self.v_max_point.y(), v_chart_pos.x(), self.v_zero_point.y()) self.k_line_h.show() self.k_line_v.show() self.v_line_v.show() self.v_line_h.hide() self._is_left = k_chart_pos.x() < (self.k_max_point.x() + self.k_zero_point.x()) / 2 elif (self.v_zero_point.y() >= v_chart_pos.y() >= self.v_max_point.y()) \ and (self.v_zero_point.x() <= v_chart_pos.x() <= self.v_max_point.x()): self.k_line_v.setLine(k_chart_pos.x(), self.k_max_point.y(), k_chart_pos.x(), self.k_zero_point.y()) self.v_line_h.setLine(self.v_zero_point.x(), v_chart_pos.y(), self.k_max_point.x(), v_chart_pos.y()) self.v_line_v.setLine(v_chart_pos.x(), self.v_max_point.y(), v_chart_pos.x(), self.v_zero_point.y()) self.k_line_h.hide() self.k_line_v.show() self.v_line_v.show() self.v_line_h.show() self._is_left = v_chart_pos.x() < (self.v_max_point.x() + self.v_zero_point.x()) / 2 else: self.k_line_h.hide() self.k_line_v.hide() self.v_line_v.hide() self.v_line_h.hide() def update_margins(self): margin_k = self.k_view.chart().margins() margin_v = self.v_view.chart().margins() width_k = self.k_view.chart().plotArea().width() width_v = self.v_view.chart().plotArea().width() sub = width_k - width_v if sub > 0: self.k_view.chart().setMargins( QMargins(margin_k.left() + sub, margin_k.top(), margin_k.right(), margin_k.bottom())) else: self.v_view.chart().setMargins( QMargins(margin_v.left() - sub, margin_v.top(), margin_v.right(), margin_v.bottom())) self.update() def on_clear_clicked(self): """ 清空图表 """ self.k_view.clear_series_values() self.v_view.clear_series_value() self.k_view.set_name('') def on_add_clicked(self): """ 添加数据 """ self.k_view.add_series_values(self.stocks) self.v_view.add_series_values(self.stocks) self.k_view.set_name(self.stocks['name'].iloc[0]) self.update_margins() def on_series_hovered(self, status: bool, index_date: str): """ QCandlestickSeries的hovered的信号响应槽 """ if status: self._hovered_pos_left = QPointF(self.k_zero_point.x(), self.k_max_point.y()) self._hovered_pos_right = QPointF( self.k_max_point.x() - self.tool_tip_widget.width(), self.k_max_point.y()) self._hovered_pos = self._hovered_pos_right if self._is_left else self._hovered_pos_left tip_value_df = self.stocks[self.stocks['trade_date'] == index_date] if tip_value_df.empty: return self.tool_tip_widget.show(index_date, str(tip_value_df['open'].iloc[0]), str(tip_value_df['close'].iloc[0]), str(tip_value_df['high'].iloc[0]), str(tip_value_df['low'].iloc[0]), str(tip_value_df['vol'].iloc[0]), self._hovered_pos) else: self.tool_tip_widget.hide()
class DetailChartView(QChartView): def __init__(self, *args, **kwargs): super(DetailChartView, self).__init__(*args, **kwargs) self.setRenderHint(QPainter.Antialiasing) self.setMaximumHeight(320) self.c_chart = None self.moved = False def linesInstallHoverEvent(self): for series in self.chart().series(): series.hovered.connect(self.lines_hovered) # 鼠标悬停信号连接 if self.c_chart: # 线条对象 self.line_item = QGraphicsLineItem(self.c_chart) # # 提示块 self.tips_tool = GraphicsProxyWidget(self.c_chart) axis_X = self.c_chart.axisX() axis_Y = self.c_chart.axisY() self.min_x, self.max_x = axis_X.min(), axis_X.max() self.min_y, self.max_y = axis_Y.min(), axis_Y.max() self.moved = True # 开启鼠标移动事件 def lines_hovered(self, point, state): # 鼠标悬停信号槽函数:state表示鼠标是否在线上(布尔值) series = self.sender() # 获取获得鼠标信号的那条线 pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if state else -1)) series.setPen(pen) def barsInstallHoverEvent(self): self.moved = False # 关闭鼠标移动事件 for series in self.chart().series(): # print(series, type(series)) for bar in series.barSets(): bar.hovered.connect(self.bars_hovered) # 鼠标悬停信号连接 def bars_hovered(self, status, index): # status 是否在柱子上 # index 第几组柱形图 bar = self.sender() pen = bar.pen() if not pen: return pen.setColor(bar.color()) pen.setWidth(pen.width() + (1 if status else -1)) bar.setPen(pen) def setChart(self, chart): super(DetailChartView, self).setChart(chart) self.c_chart = chart def mouseMoveEvent(self, event): super(DetailChartView, self).mouseMoveEvent(event) # 原先的hover事件 if self.c_chart and self.moved: pos = event.pos() # 鼠标位置转为坐标点 x, y = self.c_chart.mapToValue(pos).x(), self.c_chart.mapToValue( pos).y() index = round((x - self.min_x) / self.step_x) points = [(series, series.at(index)) for series in self.c_chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if points: pos_x = self.c_chart.mapToPosition( QPointF(index * self.step_x + self.min_x, self.min_y)) # 算出当前鼠标所在的x位置 # 自定义指示线 self.line_item.setLine(pos_x.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.line_item.show() try: title = self.c_chart.x_labels[index] except Exception: title = '' tips_width = self.tips_tool.width() tips_height = self.tips_tool.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - tips_width if self.width() - pos.x( ) - 20 < tips_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - tips_height if self.height() - pos.y( ) - 20 < tips_height else pos.y() # print(title, points, QPoint(x, y)) self.tips_tool.show(title, points, QPoint(x, y)) else: self.tips_tool.hide() self.line_item.hide() # pos = event.pos() # # 鼠标位置转为坐标点 # x, y = self.c_chart.mapToValue(pos).x(), self.c_chart.mapToValue(pos).y() # if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y: # points = [] # for series in self.c_chart.series(): # for point in series.pointsVector(): # if QDateTime.fromMSecsSinceEpoch(point.x()).date() == QDateTime.fromMSecsSinceEpoch(x).date(): # points.append((series, point)) # break # if points: # pos_x = self.c_chart.mapToPosition(QPointF(x, self.min_y)) # 算出当前鼠标所在的x位置 # # 自定义指示线 # self.line_item.setLine(pos_x.x(), self.point_top.y(), # pos_x.x(), self.point_bottom.y()) # self.line_item.show() # try: # title = QDateTime.fromMSecsSinceEpoch(x).toString("yyyy-MM-dd") # except Exception: # title = '' # tips_width = self.tips_tool.width() # tips_height = self.tips_tool.height() # # 如果鼠标位置离右侧的距离小于tip宽度 # x = pos.x() - tips_width if self.width() - \ # pos.x() - 20 < tips_width else pos.x() # # 如果鼠标位置离底部的高度小于tip高度 # y = pos.y() - tips_height if self.height() - \ # pos.y() - 20 < tips_height else pos.y() # # print(title, points, QPoint(x, y)) # self.tips_tool.show( # title, points, QPoint(x, y)) # else: # self.tips_tool.hide() # self.line_item.hide() def resizeEvent(self, event): super(DetailChartView, self).resizeEvent(event) if self.c_chart and self.moved: # 当窗口大小改变时需要重新计算 # 坐标系中左上角顶点 self.point_top = self.c_chart.mapToPosition( QPointF(self.min_x, self.max_y)) # 坐标原点坐标 self.point_bottom = self.c_chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = 1 # 步长取1 每个数据都能显示
class KLineChartView(QChartView): def __init__(self): super(KLineChartView, self).__init__() self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 self._chart = QChart(title='蜡烛图悬浮提示') self.stocks = read_tick_data() self.category = [ trade_date[4:] for trade_date in self.stocks['trade_date'] ] self._count = len(self.category) self.resize(800, 300) self.init_chart() self.toolTipWidget = GraphicsProxyWidget(self._chart) # 鼠标跟踪的十字线 self.lineItem_h = QGraphicsLineItem(self._chart) self.lineItem_v = QGraphicsLineItem(self._chart) pen = QPen() pen.setStyle(Qt.DotLine) pen.setColor(QColor(Qt.gray)) pen.setWidth(2) self.lineItem_h.setPen(pen) self.lineItem_v.setPen(pen) self.lineItem_h.setZValue(100) self.lineItem_v.setZValue(100) self.lineItem_h.hide() self.lineItem_v.hide() # 坐标轴上最大最小的值 # x 轴是 self.min_x, self.max_x = 0, len(self._chart.axisX().categories()) self.min_y, self.max_y = self._chart.axisY().min(), self._chart.axisY( ).max() # y 轴最高点坐标 self.point_y_max = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # x 轴最高点坐标 self.point_x_max = self._chart.mapToPosition( QPointF(self.max_x, self.min_y)) # self.point_x_min = self._chart.mapToPosition(QPointF(self.min_x, self.min_y)) # 计算x轴单个cate的宽度,用来处理横线不能画到边界 self.x_width = (self.point_x_max.x() - self.point_y_max.x()) / len( self.category) self.x_x_min = self.point_y_max.x() - self.x_width / 2 self.x_x_max = self.point_x_max.x() - self.x_width / 2 # 中间位置,用来判断TipWidget放在哪里 mid_date = self.stocks['trade_date'].iloc[len( self.stocks['trade_date']) // 2] self.mid_x = float( time.mktime( datetime.datetime.strptime(str(mid_date), '%Y%m%d').timetuple())) self.left_pos = self.point_y_max self.right_pos = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) def mouseMoveEvent(self, event): super(KLineChartView, self).mouseMoveEvent(event) pos = event.pos() if self.x_x_min < pos.x() < self.x_x_max \ and self.point_x_max.y() > pos.y() > self.point_y_max.y(): self.lineItem_h.setLine(self.x_x_min, pos.y(), self.x_x_max, pos.y()) self.lineItem_v.setLine(pos.x(), self.point_y_max.y(), pos.x(), self.point_x_max.y()) self.lineItem_h.show() self.lineItem_v.show() else: self.lineItem_h.hide() self.lineItem_v.hide() def resizeEvent(self, event): super(KLineChartView, self).resizeEvent(event) # y 轴最高点坐标 self.point_y_max = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # x 轴最高点坐标 self.point_x_max = self._chart.mapToPosition( QPointF(self.max_x, self.min_y)) # 计算x轴单个cate的宽度,用来处理横线不能画到边界 self.x_width = (self.point_x_max.x() - self.point_y_max.x()) / len( self.category) self.x_x_min = self.point_y_max.x() - self.x_width / 2 self.x_x_max = self.point_x_max.x() - self.x_width / 2 self.left_pos = self.point_y_max self.right_pos = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) def init_chart(self): self._chart.setAnimationOptions(QChart.SeriesAnimations) series = QCandlestickSeries() series.setIncreasingColor(QColor(Qt.red)) series.setDecreasingColor(QColor(Qt.green)) series.setName(self.stocks['name'].iloc[0]) for _, stock in self.stocks.iterrows(): time_p = datetime.datetime.strptime(stock['trade_date'], '%Y%m%d') time_p = float(time.mktime(time_p.timetuple())) _set = QCandlestickSet(float(stock['open']), float(stock['high']), float(stock['low']), float(stock['close']), time_p, series) _set.hovered.connect(self.handleBarHoverd) # 鼠标悬停 series.append(_set) self._chart.addSeries(series) self._chart.createDefaultAxes() self._chart.setLocalizeNumbers(True) axis_x = self._chart.axisX() axis_y = self._chart.axisY() axis_x.setGridLineVisible(False) axis_y.setGridLineVisible(False) axis_x.setCategories(self.category) max_p = self.stocks[['high', 'low']].stack().max() + 10 min_p = self.stocks[['high', 'low']].stack().min() - 10 axis_y.setRange(min_p, max_p) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) self.setChart(self._chart) # 设置外边界全部为0 self._chart.layout().setContentsMargins(0, 0, 0, 0) # 设置内边界都为0 # self._chart.setMargins(QMargins(0, 0, 0, 0)) # 设置背景区域无圆角 self._chart.setBackgroundRoundness(0) def handleBarHoverd(self, status): """ 改变画笔的风格 """ bar = self.sender() # 信号发送者 pen = bar.pen() if not pen: return pen.setStyle(Qt.DotLine if status else Qt.SolidLine) bar.setPen(pen) if status: # 通过 bar 可以获取横轴坐标(timestamp)和纵轴坐标(high) # 然后将坐标值转换为位置,显示 TipWidget 的位置 right_pos = QPointF( self.right_pos.x() - self.toolTipWidget.width() - self.x_width, self.right_pos.y()) pos = self.left_pos if bar.timestamp() > self.mid_x else right_pos trade_date = time.strftime('%Y%m%d', time.localtime(bar.timestamp())) self.toolTipWidget.show(str(trade_date), str(bar.open()), str(bar.close()), str(bar.high()), str(bar.low()), pos) else: self.toolTipWidget.hide()
class LineChartView(QChartView): def __init__(self): super(LineChartView, self).__init__() self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 # 自定义x轴label def CreatePlot(self, NumSection, Alltime, PlotData, type, name='列车调度'): if type == 1: self.initChart(NumSection, Alltime, PlotData, name) else: self.initProgressChart(NumSection, Alltime, PlotData, name) # 提示widget self.toolTipWidget = GraphicsProxyWidget(self._chart) # line self.lineItem = QGraphicsLineItem(self._chart) pen = QPen(Qt.gray) pen.setWidth(1) self.lineItem.setPen(pen) self.lineItem.setZValue(998) #self.lineItem.hide() self.lineItem.show() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() def resizeEvent(self, event): super(LineChartView, self).resizeEvent(event) # 当窗口大小改变时需要重新计算 # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / (self._chart.axisX().tickCount() - 1) def mouseMoveEvent(self, event): super(LineChartView, self).mouseMoveEvent(event) pos = event.pos() # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(pos).x() y = self._chart.mapToValue(pos).y() index = round(int(x / self.step_x) * self.step_x) + round( x % self.step_x) - 1 #index = round((x - self.min_x) / self.step_x) # 得到在坐标系中的所有正常显示的series的类型和点 points = [ (serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y ] if points: pos_x = self._chart.mapToPosition( QPointF(index * self.step_x + self.min_x, self.min_y)) self.lineItem.setLine(pos_x.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.lineItem.show() try: title = "" except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show(title, points, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide() def handleMarkerClicked(self): marker = self.sender() # 信号发送者 if not marker: return visible = not marker.series().isVisible() # # 隐藏或显示series marker.series().setVisible(visible) marker.setVisible(True) # 要保证marker一直显示 # 透明度 alpha = 1.0 if visible else 0.4 # 设置label的透明度 brush = marker.labelBrush() color = brush.color() color.setAlphaF(alpha) brush.setColor(color) marker.setLabelBrush(brush) # 设置marker的透明度 brush = marker.brush() color = brush.color() color.setAlphaF(alpha) brush.setColor(color) marker.setBrush(brush) # 设置画笔透明度 pen = marker.pen() color = pen.color() color.setAlphaF(alpha) pen.setColor(color) marker.setPen(pen) def handleMarkerHovered(self, status): # 设置series的画笔宽度 marker = self.sender() # 信号发送者 if not marker: return series = marker.series() if not series: return pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) series.setPen(pen) def handleSeriesHoverd(self, point, state): # 设置series的画笔宽度 series = self.sender() # 信号发送者 pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if state else -1)) series.setPen(pen) def initProgressChart(self, MaxTime, Allgen, PlotData, name): MinTime = 100000 for k in PlotData: if min(k[1]) < MinTime: MinTime = min(k[1]) self._chart = QChart(title=name) self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) # dataTable = [ # ["邮件营销", [120, 132, 101, 134, 90, 230]], # ["联盟广告", [220, 182, 191, 234, 290, 330, 310]], # ["视频广告", [150, 232, 201, 154, 190, 330, 410]], # ["直接访问", [320, 332, 301, 334, 390, 330, 320]], # ["搜索引擎", [820, 932, 901, 934, 1290, 1330, 1320]] # ] for series_name, data_list, label_list in PlotData: series = QLineSeries(self._chart) for j, v in zip(label_list, data_list): series.append(j, v) series.setName(series_name) series.setPointsVisible(True) # 显示圆点 series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 axisX = self._chart.axisX() # x轴 axisX.setTickCount(11) # x轴设置7个刻度 axisX.setGridLineVisible(False) # 隐藏从x轴往上的线条 axisY = self._chart.axisY() axisY.setTickCount(10) # y轴设置7个刻度 #axisY.setRange(0, MaxTime) # 设置y轴范围 # 自定义x轴 # axis_x = QCategoryAxis( # self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) # axis_x.setTickCount(Allgen) # axis_x.setGridLineVisible(False) # min_x = axisX.min() # for i in range(0, Allgen + 1): # axis_x.append(str(i), min_x + i) # self._chart.setAxisX(axis_x, self._chart.series()[-1]) # 自定义y轴 # axis_y = QCategoryAxis( # self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) # axis_y.setTickCount(round(MaxTime)-max(round(MinTime)-10,0)) # axis_y.setRange(max(round(MinTime)-10,0), round(MaxTime)) # for i in range(max(round(MinTime)-10,0), round(MaxTime) + 1): # axis_y.append('%i' % i, i) # self._chart.setAxisY(axis_y, self._chart.series()[-1]) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart) def initChart(self, NumSection, Alltime, PlotData, name): self._chart = QChart(title=name) self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) # dataTable = [ # ["邮件营销", [120, 132, 101, 134, 90, 230]], # ["联盟广告", [220, 182, 191, 234, 290, 330, 310]], # ["视频广告", [150, 232, 201, 154, 190, 330, 410]], # ["直接访问", [320, 332, 301, 334, 390, 330, 320]], # ["搜索引擎", [820, 932, 901, 934, 1290, 1330, 1320]] # ] for series_name, data_list, label_list in PlotData: series = QLineSeries(self._chart) for j, v in zip(label_list, data_list): series.append(j, v) series.setName(series_name) series.setPointsVisible(True) # 显示圆点 series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 axisX = self._chart.axisX() # x轴 axisX.setTickCount(Alltime) # x轴设置7个刻度 axisX.setGridLineVisible(False) # 隐藏从x轴往上的线条 axisY = self._chart.axisY() axisY.setTickCount(NumSection) # y轴设置7个刻度 axisY.setRange(0, NumSection) # 设置y轴范围 # 自定义x轴 axis_x = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) axis_x.setTickCount(Alltime + 1) axis_x.setGridLineVisible(False) min_x = axisX.min() for i in range(0, Alltime + 1): axis_x.append(str(i), min_x + i) self._chart.setAxisX(axis_x, self._chart.series()[-1]) # 自定义y轴 axis_y = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionCenter) axis_y.setTickCount(NumSection) axis_y.setRange(0, NumSection) for i in range(0, NumSection + 1): axis_y.append('section%i' % (NumSection - i + 1), i) self._chart.setAxisY(axis_y, self._chart.series()[-1]) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart)