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) # 抗锯齿 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 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 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 AbstractSliceTool(QGraphicsObject): """Summary Attributes: angles (TYPE): Description FILTER_NAME (str): Description is_started (bool): Description manager (TYPE): Description part_item (TYPE): Description sgv (TYPE): Description vectors (TYPE): Description """ _RADIUS = styles.SLICE_HELIX_RADIUS _CENTER_OF_HELIX = QPointF(_RADIUS, _RADIUS) FILTER_NAME = 'virtual_helix' # _CENTER_OF_HELIX = QPointF(0. 0.) """Abstract base class to be subclassed by all other pathview tools.""" def __init__(self, manager): """Summary Args: manager (TYPE): Description """ super(AbstractSliceTool, self).__init__(parent=manager.viewroot) """ Pareting to viewroot to prevent orphan _line_item from occuring """ self.sgv = None self.manager = manager self._active = False self._last_location = None self._line_item = QGraphicsLineItem(self) self._line_item.hide() self._vhi = None self.hide() self.is_started = False self.angles = [math.radians(x) for x in range(0, 360, 30)] self.vectors = self.setVectors() self.part_item = None self.vhi_hint_item = QGraphicsEllipseItem(_DEFAULT_RECT, self) self.vhi_hint_item.setPen(_MOD_PEN) self.vhi_hint_item.setZValue(styles.ZPARTITEM) # end def ######################## Drawing ####################################### def setVectors(self): """Summary Returns: TYPE: Description """ rad = self._RADIUS return [QLineF(rad, rad, rad*(1. + 2.*math.cos(x)), rad*(1. + 2.*math.sin(x)) ) for x in self.angles] # end def def setVirtualHelixItem(self, virtual_helix_item): """Summary Args: virtual_helix_item (cadnano.gui.views.sliceview.virtualhelixitem.VirtualHelixItem): Description Returns: TYPE: Description """ rad = self._RADIUS self._vhi = virtual_helix_item li = self._line_item li.setParentItem(virtual_helix_item) li.setLine(rad, rad, rad, rad) # li.setLine(0., 0., 0., 0.) # end def def setSelectionFilter(self, filter_name_list): if 'virtual_helix' in filter_name_list: self.vhi_hint_item.setPen(_MOD_PEN) else: self.vhi_hint_item.setPen(_INACTIVE_PEN) # end def def resetTool(self): """Summary Returns: TYPE: Description """ self._line_item.setParentItem(self) def idNum(self): """Summary Returns: TYPE: Description """ if self._vhi is not None: return self._vhi.idNum() def setPartItem(self, part_item): """Summary Args: part_item (TYPE): Description Returns: TYPE: Description """ self.vhi_hint_item.setParentItem(part_item) self.part_item = part_item # end def def boundingRect(self): """Required to prevent NotImplementedError() """ return QRectF() def eventToPosition(self, part_item, event): """take an event and return a position as a QPointF update widget as well Args: part_item (TYPE): Description event (TYPE): Description """ if self.is_started: pos = self.findNearestPoint(part_item, event.scenePos()) else: pos = event.pos() self.vhi_hint_item.setPos( pos - QPointF(_RADIUS - DELTA, _RADIUS - DELTA)) return pos # end def def setHintPos(self, pos): self.vhi_hint_item.setPos( pos - QPointF(_RADIUS - DELTA, _RADIUS - DELTA)) # end def def findNearestPoint(self, part_item, target_scenepos): """ Args: part_item (TYPE): Description target_scenepos (TYPE): Description """ li = self._line_item pos = li.mapFromScene(target_scenepos) line = li.line() mouse_point_vec = QLineF(self._CENTER_OF_HELIX, pos) # Check if the click happened on the origin VH if mouse_point_vec.length() < self._RADIUS: # return part_item.mapFromScene(target_scenepos) return None angle_min = 9999 direction_min = None for vector in self.vectors: angle_new = mouse_point_vec.angleTo(vector) if angle_new < angle_min: direction_min = vector angle_min = angle_new if direction_min is not None: li.setLine(direction_min) return part_item.mapFromItem(li, direction_min.p2()) else: print("default point") line.setP2(pos) li.setLine(line) return part_item.mapFromItem(li, pos) # end def def findNextPoint(self, part_item, target_part_pos): """ Args: part_item (TYPE): Description target_part_pos (TYPE): Description """ li = self._line_item pos = li.mapFromItem(part_item, target_part_pos) for i, vector in enumerate(self.vectors): if vector.p2() == pos: return part_item.mapFromItem(li, self.vectors[i - 1].p2()) # origin VirtualHelixItem is overlapping destination VirtualHelixItem return part_item.mapFromItem(li, self.vectors[0].p2()) # end def def hideLineItem(self): """Summary Returns: TYPE: Description """ self.vhi_hint_item.hide() li = self._line_item li.hide() li.setParentItem(self) line = li.line() line.setP2(self._CENTER_OF_HELIX) li.setLine(line) # li.hide() self.is_started = False # end def # def hoverEnterEvent(self, event): # self.vhi_hint_item.show() # #print("Slice VHI hoverEnterEvent") # # def hoverMoveEvent(self, event): # # print("Slice VHI hoverMoveEvent") # def hoverLeaveEvent(self, event): # # self.vhi_hint_item.hide() # #print("Slice VHI hoverLeaveEvent") def hoverMoveEvent(self, part_item, event): """Summary Args: part_item (TYPE): Description event (TYPE): Description Returns: TYPE: Description """ # self.vhi_hint_item.setPos( event.pos()- # QPointF(_RADIUS - DELTA, _RADIUS - DELTA)) pos = self.eventToPosition(part_item, event) return pos # end def def setActive(self, will_be_active, old_tool=None): """ Called by SliceToolManager.setActiveTool when the tool becomes active. Used, for example, to show/hide tool-specific ui elements. Args: will_be_active (TYPE): Description old_tool (None, optional): Description """ if self._active and not will_be_active: self.deactivate() self._active = will_be_active self.sgv = self.manager.window.slice_graphics_view if hasattr(self, 'getCustomContextMenu'): # print("connecting ccm") try: # Hack to prevent multiple connections self.sgv.customContextMenuRequested.disconnect() except: pass self.sgv.customContextMenuRequested.connect(self.getCustomContextMenu) # end def def deactivate(self): """Summary Returns: TYPE: Description """ if hasattr(self, 'getCustomContextMenu'): # print("disconnecting ccm") self.sgv.customContextMenuRequested.disconnect(self.getCustomContextMenu) self.sgv = None self.is_started = False self.hideLineItem() self._vhi = None self.part_item = None self.hide() self._active = False # end def def isActive(self): """Returns isActive """ return self._active
class AbstractSliceTool(QGraphicsObject): """Summary Attributes: angles (TYPE): Description FILTER_NAME (str): Description is_started (bool): Description manager (TYPE): Description part_item (TYPE): Description sgv (TYPE): Description vectors (TYPE): Description """ _RADIUS = styles.SLICE_HELIX_RADIUS _CENTER_OF_HELIX = QPointF(_RADIUS, _RADIUS) FILTER_NAME = 'virtual_helix' # _CENTER_OF_HELIX = QPointF(0. 0.) """Abstract base class to be subclassed by all other pathview tools.""" def __init__(self, manager): """Summary Args: manager (TYPE): Description """ super(AbstractSliceTool, self).__init__(parent=manager.viewroot) """ Pareting to viewroot to prevent orphan _line_item from occuring """ self.sgv = None self.manager = manager self._active = False self._last_location = None self._line_item = QGraphicsLineItem(self) self._line_item.hide() self._vhi = None self.hide() self.is_started = False self.angles = [math.radians(x) for x in range(0, 360, 30)] self.vectors = self.setVectors() self.part_item = None # end def ######################## Drawing ####################################### def setVectors(self): """Summary Returns: TYPE: Description """ rad = self._RADIUS return [ QLineF(rad, rad, rad * (1. + 2. * math.cos(x)), rad * (1. + 2. * math.sin(x))) for x in self.angles ] # end def def setVirtualHelixItem(self, virtual_helix_item): """Summary Args: virtual_helix_item (cadnano.gui.views.sliceview.virtualhelixitem.VirtualHelixItem): Description Returns: TYPE: Description """ rad = self._RADIUS self._vhi = virtual_helix_item li = self._line_item li.setParentItem(virtual_helix_item) li.setLine(rad, rad, rad, rad) # li.setLine(0., 0., 0., 0.) # end def def resetTool(self): """Summary Returns: TYPE: Description """ self._line_item.setParentItem(self) def idNum(self): """Summary Returns: TYPE: Description """ if self._vhi is not None: return self._vhi.idNum() def setPartItem(self, part_item): """Summary Args: part_item (TYPE): Description Returns: TYPE: Description """ self.part_item = part_item # end def def boundingRect(self): """Required to prevent NotImplementedError() """ return QRectF() def eventToPosition(self, part_item, event): """take an event and return a position as a QPointF update widget as well Args: part_item (TYPE): Description event (TYPE): Description """ if self.is_started: return self.findNearestPoint(part_item, event.scenePos()) else: return event.pos() # end def def findNearestPoint(self, part_item, target_scenepos): """ Args: part_item (TYPE): Description target_scenepos (TYPE): Description """ li = self._line_item pos = li.mapFromScene(target_scenepos) line = li.line() mouse_point_vec = QLineF(self._CENTER_OF_HELIX, pos) # Check if the click happened on the origin VH if mouse_point_vec.length() < self._RADIUS: # return part_item.mapFromScene(target_scenepos) return None angle_min = 9999 direction_min = None for vector in self.vectors: angle_new = mouse_point_vec.angleTo(vector) if angle_new < angle_min: direction_min = vector angle_min = angle_new if direction_min is not None: li.setLine(direction_min) return part_item.mapFromItem(li, direction_min.p2()) else: print("default point") line.setP2(pos) li.setLine(line) return part_item.mapFromItem(li, pos) # end def def findNextPoint(self, part_item, target_part_pos): """ Args: part_item (TYPE): Description target_part_pos (TYPE): Description """ li = self._line_item pos = li.mapFromItem(part_item, target_part_pos) for i, vector in enumerate(self.vectors): if vector.p2() == pos: return part_item.mapFromItem(li, self.vectors[i - 1].p2()) # origin VirtualHelixItem is overlapping destination VirtualHelixItem return part_item.mapFromItem(li, self.vectors[0].p2()) # end def def hideLineItem(self): """Summary Returns: TYPE: Description """ li = self._line_item li.setParentItem(self) line = li.line() line.setP2(self._CENTER_OF_HELIX) li.setLine(line) li.hide() self.is_started = False # end def def hoverMoveEvent(self, part_item, event): """Summary Args: part_item (TYPE): Description event (TYPE): Description Returns: TYPE: Description """ return self.eventToPosition(part_item, event) # end def def setActive(self, will_be_active, old_tool=None): """ Called by SliceToolManager.setActiveTool when the tool becomes active. Used, for example, to show/hide tool-specific ui elements. Args: will_be_active (TYPE): Description old_tool (None, optional): Description """ if self._active and not will_be_active: self.deactivate() self._active = will_be_active self.sgv = self.manager.window.slice_graphics_view if hasattr(self, 'getCustomContextMenu'): # print("connecting ccm") try: # Hack to prevent multiple connections self.sgv.customContextMenuRequested.disconnect() except: pass self.sgv.customContextMenuRequested.connect( self.getCustomContextMenu) # end def def deactivate(self): """Summary Returns: TYPE: Description """ if hasattr(self, 'getCustomContextMenu'): # print("disconnecting ccm") self.sgv.customContextMenuRequested.disconnect( self.getCustomContextMenu) self.sgv = None self.is_started = False self.hideLineItem() self._vhi = None self.part_item = None self.hide() self._active = False # end def def isActive(self): """Returns isActive """ return self._active
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 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 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) pen.setWidth(1) self.lineItem.setPen(pen) 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) 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.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.lineItem.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() ''' 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, status, index, barset): return print(status, index, barset) # # 设置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) categories = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] names = ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"] series = QBarSeries(self._chart) for name in names: bar = QBarSet(name) #随机数据 for _ in range(7): bar.append(randint(0, 10)) series.append(bar) # series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 # x轴 axis_x = QBarCategoryAxis(self._chart) axis_x.append(categories) self._chart.setAxisX(axis_x, series) # 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 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 AbstractGridTool(AbstractTool): _RADIUS = styles.GRID_HELIX_RADIUS _CENTER_OF_HELIX = QPointF(_RADIUS, _RADIUS) FILTER_NAME = 'virtual_helix' # _CENTER_OF_HELIX = QPointF(0. 0.) """Abstract base class to be subclassed by all other pathview tools.""" def __init__(self, manager: AbstractToolManager): """Summary Args: manager (TYPE): Description """ # Setting parent to viewroot to prevent orphan _line_item from occurring super(AbstractGridTool, self).__init__(parent=manager.viewroot) self.slice_graphics_view = manager.window.grid_graphics_view self.manager = manager self._active = False self._last_location = None self._line_item = QGraphicsLineItem(self) self._line_item.hide() self._vhi = None self.hide() self.is_started = False self.angles = [math.radians(x) for x in range(0, 360, 30)] self.vectors = self.setVectors() self.part_item = None self.vhi_hint_item = QGraphicsEllipseItem(_DEFAULT_RECT, self) self.vhi_hint_item.setPen(_MOD_PEN) self.vhi_hint_item.setZValue(styles.ZPARTITEM) # end def ######################## Drawing ####################################### def setVectors(self): """Summary Returns: TYPE: Description """ rad = self._RADIUS return [ QLineF(rad, rad, rad * (1. + 2. * math.cos(x)), rad * (1. + 2. * math.sin(x))) for x in self.angles ] # end def def setVirtualHelixItem(self, virtual_helix_item): """Summary Args: virtual_helix_item (cadnano.views.gridview.virtualhelixitem.VirtualHelixItem): Description Returns: TYPE: Description """ rad = self._RADIUS self._vhi = virtual_helix_item li = self._line_item li.setParentItem(virtual_helix_item) li.setLine(rad, rad, rad, rad) # li.setLine(0., 0., 0., 0.) # end def def setSelectionFilter(self, filter_name_list): if 'virtual_helix' in filter_name_list: self.vhi_hint_item.setPen(_MOD_PEN) else: self.vhi_hint_item.setPen(_INACTIVE_PEN) # end def def resetTool(self): """Summary Returns: TYPE: Description """ self._line_item.setParentItem(self) def idNum(self): """Summary Returns: TYPE: Description """ if self._vhi is not None: return self._vhi.idNum() def setPartItem(self, part_item): """Summary Args: part_item (TYPE): Description Returns: TYPE: Description """ self.vhi_hint_item.setParentItem(part_item) self.part_item = part_item # end def def boundingRect(self): """Required to prevent NotImplementedError() """ return QRectF() def eventToPosition(self, part_item, event): """take an event and return a position as a QPointF update widget as well Args: part_item (TYPE): Description event (TYPE): Description """ if self.is_started: pos = self.findNearestPoint(part_item, event.scenePos()) else: pos = event.pos() self.vhi_hint_item.setPos(pos - QPointF(_RADIUS - DELTA, _RADIUS - DELTA)) return pos # end def def setHintPos(self, pos): self.vhi_hint_item.setPos(pos - QPointF(_RADIUS - DELTA, _RADIUS - DELTA)) # end def def findNearestPoint(self, part_item: QAbstractPartItem, target_scenepos: QPointF) -> QPointF: """ Args: part_item: Description target_scenepos: position in the Scene """ li = self._line_item pos = li.mapFromScene(target_scenepos) line = li.line() mouse_point_vec = QLineF(self._CENTER_OF_HELIX, pos) # Check if the click happened on the origin VH if mouse_point_vec.length() < self._RADIUS: # return part_item.mapFromScene(target_scenepos) return None angle_min = 9999 direction_min = None for vector in self.vectors: angle_new = mouse_point_vec.angleTo(vector) if angle_new < angle_min: direction_min = vector angle_min = angle_new if direction_min is not None: li.setLine(direction_min) return part_item.mapFromItem(li, direction_min.p2()) else: print("default point") line.setP2(pos) li.setLine(line) return part_item.mapFromItem(li, pos) # end def def findNextPoint(self, part_item: QAbstractPartItem, target_part_pos: QPointF) -> QPointF: """ Args: part_item: Description target_part_pos: Position in the Part """ li = self._line_item pos = li.mapFromItem(part_item, target_part_pos) for i, vector in enumerate(self.vectors): if vector.p2() == pos: return part_item.mapFromItem(li, self.vectors[i - 1].p2()) # origin VirtualHelixItem is overlapping destination VirtualHelixItem return part_item.mapFromItem(li, self.vectors[0].p2()) # end def def hideLineItem(self): """ Hide the ``_line_item`` and the ``vhi_hint_item`` set ``is_started`` to :bool:`False` """ # print("hideLineItem") self.vhi_hint_item.hide() li = self._line_item li.hide() li.setParentItem(self) line = li.line() line.setP2(self._CENTER_OF_HELIX) li.setLine(line) self.is_started = False # end def # def hoverEnterEvent(self, event): # self.vhi_hint_item.show() # #print("Grid VHI hoverEnterEvent") # # def hoverMoveEvent(self, event): # # print("Grid VHI hoverMoveEvent") # def hoverLeaveEvent(self, event): # # self.vhi_hint_item.hide() # #print("Grid VHI hoverLeaveEvent") def hoverMoveEvent(self, part_item, event): """Summary Args: part_item (TYPE): Description event (TYPE): Description Returns: TYPE: Description """ # self.vhi_hint_item.setPos( event.pos()- # QPointF(_RADIUS - DELTA, _RADIUS - DELTA)) pos = self.eventToPosition(part_item, event) return pos # end def def setActive(self, will_be_active, old_tool=None): """ Called by GridToolManager.setActiveTool when the tool becomes active. Used, for example, to show/hide tool-specific ui elements. Args: will_be_active (TYPE): Description old_tool (None, optional): Description """ if self._active and not will_be_active: self.deactivate() self._active = will_be_active self.slice_graphics_view = self.manager.window.grid_graphics_view if hasattr(self, 'getCustomContextMenu'): # print("connecting ccm") try: # Hack to prevent multiple connections self.slice_graphics_view.customContextMenuRequested.disconnect( ) except (AttributeError, TypeError): pass self.slice_graphics_view.customContextMenuRequested.connect( self.getCustomContextMenu) # end def def deactivate(self): """Summary Returns: TYPE: Description """ if hasattr(self, 'getCustomContextMenu'): # print("disconnecting ccm") self.slice_graphics_view.customContextMenuRequested.disconnect( self.getCustomContextMenu) self.slice_graphics_view = None self.is_started = False self.hideLineItem() self._vhi = None self.part_item = None self.hide() self._active = False # end def def isActive(self) -> bool: """Returns isActive """ return self._active
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)