def drawGrid(self): black_notes = [2,4,6,9,11] scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, 0) scale_bar.setBrush(QColor(100,100,100)) clearpen = QPen(QColor(0,0,0,0)) for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1): for j in range(self.notes_in_octave, 0, -1): scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1)) scale_bar.setPen(clearpen) if j not in black_notes: scale_bar.setBrush(QColor(120,120,120)) else: scale_bar.setBrush(QColor(100,100,100)) measure_pen = QPen(QColor(0, 0, 0, 120), 3) half_measure_pen = QPen(QColor(0, 0, 0, 40), 2) line_pen = QPen(QColor(0, 0, 0, 40)) for i in range(0, int(self.num_measures) + 1): measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header) measure.setPos(self.measure_width * i, 0.5 * measure_pen.width()) measure.setPen(measure_pen) if i < self.num_measures: number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header) number.setPos(self.measure_width * i + 5, 2) number.setBrush(Qt.white) for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.): line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header) line.setZValue(1.0) line.setPos(self.measure_width * i + self.value_width * j, self.header_height) if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0: line.setPen(half_measure_pen) else: line.setPen(line_pen)
class WayPoint: def __init__(self, **kwargs): super().__init__() self.location = MapPoint() self.__dict__.update(kwargs) self.pixmap = QGraphicsPixmapItem( QPixmap('HOME_DIR + /nparse/data/maps/waypoint.png')) self.pixmap.setOffset(-10, -20) self.line = QGraphicsLineItem(0.0, 0.0, self.location.x, self.location.y) self.line.setPen(QPen(Qt.green, 1, Qt.DashLine)) self.line.setVisible(False) self.pixmap.setZValue(5) self.line.setZValue(4) self.pixmap.setPos(self.location.x, self.location.y) def update_(self, scale, location=None): self.pixmap.setScale(scale) if location: line = self.line.line() line.setP1(QPointF(location.x, location.y)) self.line.setLine(line) pen = self.line.pen() pen.setWidth(1 / scale) self.line.setPen(pen) self.line.setVisible(True)
def _constructTree(self, node, x, y, par_x, par_y, text, squared=True, depth=1, h_index=0): if squared: node_item = BinTreeSquareNodeGraphicsItem(text, depth=depth) else: node_item = BinTreeNodeGraphicsItem(text, depth=depth) self.nodeCount += 1 node_item.setBrush(Qt.white) if node.data in self.regionPoints: node_item.setPen(Qt.green) self.addItem(node_item) node_item.moveBy(x, y) dx, dy = self.width() / (2 ** (depth + 2)), 2 * self.nodeWidth text_item = QGraphicsTextItem(text) text_item.moveBy(x-self.nodeWidth/4, y-self.nodeWidth/4) self.addItem(text_item) edge = QGraphicsLineItem(x, y, par_x, par_y) edge.setZValue(-1) self.addItem(edge) if node.left: self._constructTree(node.left, x-dx, y+dy, x, y, str(node.left.data), not squared, depth+1, 2*h_index) if node.right: self._constructTree(node.right, x+dx, y+dy, x, y, str(node.right.data), not squared, depth+1, 2*h_index+1)
def drawGrid(self): black_notes = [2,4,6,9,11] scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, 0) scale_bar.setBrush(QColor(100,100,100)) clearpen = QPen(QColor(0,0,0,0)) for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1): for j in range(self.notes_in_octave, 0, -1): scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1)) scale_bar.setPen(clearpen) if j not in black_notes: scale_bar.setBrush(QColor(120,120,120)) else: scale_bar.setBrush(QColor(100,100,100)) measure_pen = QPen(QColor(0, 0, 0, 120), 3) half_measure_pen = QPen(QColor(0, 0, 0, 40), 2) line_pen = QPen(QColor(0, 0, 0, 40)) for i in range(0, int(self.num_measures) + 1): measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header) measure.setPos(self.measure_width * i, 0.5 * measure_pen.width()) measure.setPen(measure_pen) if i < self.num_measures: number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header) number.setPos(self.measure_width * i + 5, 2) number.setBrush(Qt.white) for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.): line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header) line.setZValue(1.0) line.setPos(self.measure_width * i + self.value_width * j, self.header_height) if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0: line.setPen(half_measure_pen) else: line.setPen(line_pen)
def create_axis(self): bounding_end = abs(self.dimension_analysis.bounding_rect[3]) bounding_start = abs(self.dimension_analysis.bounding_rect[2]) pen = QPen() pen.setWidthF(0.5) # horizontal line self.graph_zero[0] = self.position[0] + self.margin - self.line_extend self.graph_zero[1] = self.position[1] + bounding_start * self.height_multiplier + self.margin self.graph_end[0] = self.graph_zero[0] + self.content_width + self.line_extend self.graph_end[1] = self.graph_zero[1] line_item_horizontal = QGraphicsLineItem(self.graph_zero[0], self.graph_zero[1], self.graph_end[0], self.graph_end[1]) line_item_horizontal.setPen(pen) self.addToGroup(line_item_horizontal) center = (self.graph_zero[0] + self.line_extend), self.graph_zero[1] y_top = center[1] - (bounding_start*self.height_multiplier) y_bottom = center[1]+(bounding_end*self.height_multiplier) line_item_vertical = QGraphicsLineItem(center[0], y_top, center[0], y_bottom) line_item_vertical.setPen(pen) self.addToGroup(line_item_vertical) pen_thin = QPen() pen_thin.setWidthF(0.2) start_graph = center[1] - 10 while start_graph > center[1] - bounding_start * self.height_multiplier: line_item_horizontal = QGraphicsLineItem(self.graph_zero[0], start_graph, self.graph_end[0], start_graph) line_item_horizontal.setPen(pen_thin) line_item_horizontal.setZValue(-0.5) self.addToGroup(line_item_horizontal) start_graph -= 10 start_graph = center[1] + 10 while start_graph < center[1] + bounding_end * self.height_multiplier: line_item_horizontal = QGraphicsLineItem(self.graph_zero[0], start_graph, self.graph_end[0], start_graph) line_item_horizontal.setPen(pen_thin) line_item_horizontal.setZValue(-0.5) self.addToGroup(line_item_horizontal) start_graph += 10
def refresh(self): if not self.items(): return super().refresh() for i in self.items(): if isinstance(i, QGraphicsLineItem): self.removeItem(i) hull = self.hull_method(self.point_model.points) pts = hull + [hull[0]] point_items = list(filter(lambda i: isinstance(i, PointGraphicsItem), self.items())) for p in point_items: p.setBrush(Qt.blue) min_point = min(point_items, key=lambda p:p.x()) min_point.setBrush(Qt.green) for i in range(len(pts)-1): dx, dy = pts[i+1].x - pts[i].x, pts[i+1].y - pts[i].y line = QGraphicsLineItem(pts[i+1].x, pts[i+1].y, pts[i+1].x+dx, pts[i+1].y+dy) pen = QPen() pen.setStyle(Qt.DashLine) line.setPen(pen) line.setZValue(-2) self.addItem(line) h_line = QGraphicsLineItem(pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y) h_line.setPen(Qt.red) h_line.setZValue(-1) self.enumeratePoints()
def add_graphics_line_item(self, line): line_item = QGraphicsLineItem(line) line_item.setPen(self.pen) line_item.setZValue(-1) scene = self.model.gui.scene scene.addItem(line_item) pair = (line, line_item) self.line_pairs.append(pair) return pair
def drawLrLine(self): leftmost = list(filter(lambda i: i.point == self.algorithm.stageResults[0][0], self.items()))[0] rightmost = list(filter(lambda i: i.point == self.algorithm.stageResults[0][1], self.items()))[0] leftmost.setBrush(Qt.red) rightmost.setBrush(Qt.red) lr_line = QGraphicsLineItem(leftmost.x(), leftmost.y(), rightmost.x(), rightmost.y()) lr_line.setPen(Qt.red) lr_line.setZValue(-1) self.addItem(lr_line)
def drawLinesFromOriginToPoints(self): points = self.algorithm.stageResults[1] for p in points: line = QGraphicsLineItem(self.originItem.x(), self.originItem.y(), p.x, p.y) pen = QPen() pen.setStyle(Qt.DashLine) line.setPen(pen) line.setZValue(-1) self.addItem(line)
def updateCurve(self, name, color=Qt.black): # if name in self.curveObjs: # curveitem = self.curveObjs[name] # else: # curveitem = QGraphicsPathItem() # self.scene.addItem(curveitem) # # path=curveitem.path() # path = QPainterPath() # # pointItems = self.curvePointObjs[name] # if len(pointItems) > 0: # path.moveTo(pointItems[0].pos()) # for pointitem in pointItems[1:]: # path.lineTo(pointitem.pos()) # curveitem.setPath(path) # curveitem.update(curveitem.boundingRect()) # curveitem.prepareGeometryChange() # self.scene.update() # self.viewport().repaint() # self.viewport().update() if not isinstance(name, str): return if name not in self.pointObjs: return lastitems = [] if name in self.curveObjs: lastitems = self.curveObjs[name] if not isinstance(lastitems, list): lastitems = [] if name in self.pointObjs: pointItems = self.pointObjs[name] else: pointItems = [] points = [] for ptitem in pointItems: points.append(ptitem.pos()) self.curveObjs[name] = [] if len(points) > 1: for i in range(1, len(points)): l = QGraphicsLineItem(points[i - 1].x(), points[i - 1].y(), points[i].x(), points[i].y()) l.setPen(color) l.setZValue(10) # l.setFlag(QGraphicsItem.ItemIsSelectable) self.curveObjs[name].append(l) self.scene.addItem(l) for line in lastitems: self.scene.removeItem(line) self.updateCurvePoints(name)
def drawHlrLines(self): hlr = self.algorithm.stageResults[1] for h, l, r in hlr: pen = QPen() pen.setStyle(Qt.PenStyle.DashLine) lh_line = QGraphicsLineItem(l.x, l.y, h.x, h.y) lh_line.setPen(pen) lh_line.setZValue(-1) rh_line = QGraphicsLineItem(r.x, r.y, h.x, h.y) rh_line.setPen(pen) rh_line.setZValue(-1) self.addItem(lh_line) self.addItem(rh_line)
def createArrow(self): rad = self._RADIUS pen = QPen() pen.setWidth(3) color = QColor(Qt.blue) color.setAlphaF(0.25) pen.setBrush(color) if self._virtual_helix.isEvenParity(): arrow = QGraphicsLineItem(rad, rad, 2*rad, rad, self) else: arrow = QGraphicsLineItem(0, rad, rad, rad, self) arrow.setTransformOriginPoint(rad, rad) arrow.setZValue(400) arrow.setPen(pen) self.arrow = arrow self.arrow.hide()
def on_actItem_Line_triggered(self): # 添加直线 item = QGraphicsLineItem(-100, 0, 100, 0) # x,y 为左上角的图元局部坐标,图元中心点为0,0 item.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable) pen = QPen(Qt.red) pen.setWidth(3) item.setPen(pen) self.view.frontZ = self.view.frontZ + 1 item.setZValue(self.view.frontZ) item.setPos(-50 + (QtCore.qrand() % 100), -50 + (QtCore.qrand() % 100)) self.view.seqNum = self.view.seqNum + 1 item.setData(self.view.ItemId, self.view.seqNum) # //自定义数据,ItemId键 item.setData(self.view.ItemDesciption, "直线") self.scene.addItem(item) self.scene.clearSelection() item.setSelected(True)
def drawEdges(self): points = self.algorithm.stageResults[1] table = self.algorithm.stageResults[2] for row in table: p1, p2, p3 = points[row[0][0] - 1], points[row[0][1] - 1], points[row[0][2] - 1] line1 = QGraphicsLineItem(*p1.coords, *p2.coords) line2 = QGraphicsLineItem(*p2.coords, *p3.coords) line1.setZValue(-2) line2.setZValue(-2) if row[1]: line1.setPen(Qt.green) line2.setPen(Qt.green) else: line1.setPen(Qt.red) line2.setPen(Qt.red) self.addItem(line1) self.addItem(line2)
def _constructTree(self, node, x, y, par_x, par_y, text, depth=1, h_index=0): node_item = BinTreeNodeGraphicsItem(text, node.data, depth) node_item.setBrush(Qt.white) self.addItem(node_item) node_item.moveBy(x, y) dx, dy = self.width() / (2 ** (depth + 2)), 4 * self.rad text_item = QGraphicsTextItem() text_item.setHtml(text+"</sub>") text_item.moveBy(x-self.rad/2, y-self.rad/2) self.addItem(text_item) edge = QGraphicsLineItem(x, y, par_x, par_y) edge.setZValue(-1) self.addItem(edge) if node.left: new_text = text + "1" self._constructTree(node.left, x-dx, y+dy, x, y, new_text, depth+1, 2*h_index) if node.right: new_text = text + "2" self._constructTree(node.right, x+dx, y+dy, x, y, new_text, depth+1, 2*h_index+1)
def updateGrid(self): for line in self.gridObjs: self.scene.removeItem(line) if self.gridxpos and self.gridypos: clr = QColor(self.proj.gridColor) clr.setAlphaF(self.proj.gridOpacity) for x in self.gridxpos: line = QGraphicsLineItem(x, self.gridypos[0], x, self.gridypos[-1]) line.setZValue(5) line.setPen( QPen(clr, self.proj.gridLineWidth, self.proj.gridLineType)) self.gridObjs.append(line) self.scene.addItem(line) for y in self.gridypos: line = QGraphicsLineItem(self.gridxpos[0], y, self.gridxpos[-1], y) line.setZValue(5) line.setPen( QPen(clr, self.proj.gridLineWidth, self.proj.gridLineType)) self.gridObjs.append(line) self.scene.addItem(line)
def makePartition(self): medians = self.algorithm.stageResults[1].to_list() directions = self.algorithm.stageResults[2] for i in range(len(medians)): point = medians[i] vertical = directions[i] line = None if vertical: lowermostY, uppermostY = self._boundingHorizontalLinesYCoords(*point.data.coords) x = point.data.x line = QGraphicsLineItem( QLineF( QPointF(x, lowermostY), QPointF(x, uppermostY) ) ) line.setPen(Qt.red) line.setZValue(-1) self.vLinesXCoords.append(x) self.vLinesYRanges.append([lowermostY, uppermostY]) else: leftmostX, rightmostX = self._boundingVerticalLinesXCoords(*point.data.coords) y = point.data.y line = QGraphicsLineItem( QLineF( QPointF(leftmostX, y), QPointF(rightmostX, y) ) ) line.setPen(Qt.green) line.setZValue(-2) self.hLinesYCoords.append(y) self.hLinesXRanges.append([leftmostX, rightmostX]) self.addItem(line)
def createArrows(self): rad = self._RADIUS pen1 = self._pen1 pen2 = self._pen2 pen1.setWidth(3) pen2.setWidth(3) pen1.setBrush(Qt.gray) pen2.setBrush(Qt.lightGray) if self._virtual_helix.isEvenParity(): arrow1 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow2 = QGraphicsLineItem(0, rad, rad, rad, self) else: arrow1 = QGraphicsLineItem(0, rad, rad, rad, self) arrow2 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow1.setTransformOriginPoint(rad, rad) arrow2.setTransformOriginPoint(rad, rad) arrow1.setZValue(400) arrow2.setZValue(400) arrow1.setPen(pen1) arrow2.setPen(pen2) self.arrow1 = arrow1 self.arrow2 = arrow2 self.arrow1.hide() self.arrow2.hide()
def createArrows(self): rad = _RADIUS pen1 = self._pen1 pen2 = self._pen2 pen1.setWidth(3) pen2.setWidth(3) pen1.setBrush(Qt.gray) pen2.setBrush(Qt.lightGray) if self._virtual_helix.isEvenParity(): arrow1 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow2 = QGraphicsLineItem(0, rad, rad, rad, self) else: arrow1 = QGraphicsLineItem(0, rad, rad, rad, self) arrow2 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow1.setTransformOriginPoint(rad, rad) arrow2.setTransformOriginPoint(rad, rad) arrow1.setZValue(400) arrow2.setZValue(400) arrow1.setPen(pen1) arrow2.setPen(pen2) self.arrow1 = arrow1 self.arrow2 = arrow2 self.arrow1.hide() self.arrow2.hide()
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 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)
def _AddBackLineItem(self, x1, y1, x2, y2, pen): line = QGraphicsLineItem(x1, y1, x2, y2) line.setPen(pen) line.setZValue(self.m_LineZValue) self.addItem(line)
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 PianoRoll(QGraphicsScene): '''the piano roll''' midievent = pyqtSignal(list) measureupdate = pyqtSignal(int) modeupdate = pyqtSignal(str) def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'): QGraphicsScene.__init__(self) self.setBackgroundBrush(QColor(50, 50, 50)) self.mousePos = QPointF() self.notes = [] self.selected_notes = [] self.piano_keys = [] self.marquee_select = False self.insert_mode = False self.velocity_mode = False self.place_ghost = False self.ghost_note = None self.default_ghost_vel = 100 self.ghost_vel = self.default_ghost_vel ## dimensions self.padding = 2 ## piano dimensions self.note_height = 10 self.start_octave = -2 self.end_octave = 8 self.notes_in_octave = 12 self.total_notes = (self.end_octave - self.start_octave) \ * self.notes_in_octave + 1 self.piano_height = self.note_height * self.total_notes self.octave_height = self.notes_in_octave * self.note_height self.piano_width = 34 ## height self.header_height = 20 self.total_height = self.piano_height - self.note_height + self.header_height #not sure why note_height is subtracted ## width self.full_note_width = 250 # i.e. a 4/4 note self.snap_value = None self.quantize_val = quantize_val ### dummy vars that will be changed self.time_sig = 0 self.measure_width = 0 self.num_measures = 0 self.max_note_length = 0 self.grid_width = 0 self.value_width = 0 self.grid_div = 0 self.piano = None self.header = None self.play_head = None self.setTimeSig(time_sig) self.setMeasures(num_measures) self.setGridDiv() self.default_length = 1. / self.grid_div # ------------------------------------------------------------------------- # Callbacks def movePlayHead(self, transport_info): # TODO: need conversion between frames and PPQ x = 105. # works for 120bpm total_duration = self.time_sig[0] * self.num_measures * x pos = transport_info['frame'] / x frac = (pos % total_duration) / total_duration self.play_head.setPos(QPointF(frac * self.grid_width, 0)) def setTimeSig(self, time_sig): try: new_time_sig = list(map(float, time_sig.split('/'))) if len(new_time_sig)==2: self.time_sig = new_time_sig self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1] self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] self.grid_width = self.measure_width * self.num_measures self.setGridDiv() except ValueError: pass def setMeasures(self, measures): try: self.num_measures = float(measures) self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] self.grid_width = self.measure_width * self.num_measures self.refreshScene() except: pass def setDefaultLength(self, length): try: v = list(map(float, length.split('/'))) if len(v) < 3: self.default_length = \ v[0] if len(v)==1 else \ v[0] / v[1] pos = self.enforce_bounds(self.mousePos) if self.insert_mode: self.makeGhostNote(pos.x(), pos.y()) except ValueError: pass def setGridDiv(self, div=None): if not div: div = self.quantize_val try: val = list(map(int, div.split('/'))) if len(val) < 3: self.quantize_val = div self.grid_div = val[0] if len(val)==1 else val[1] self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None self.setQuantize(div) self.refreshScene() except ValueError: pass def setQuantize(self, value): try: val = list(map(float, value.split('/'))) if len(val) == 1: self.quantize(val[0]) self.quantize_val = value elif len(val) == 2: self.quantize(val[0] / val[1]) self.quantize_val = value except ValueError: pass # ------------------------------------------------------------------------- # Event Callbacks def keyPressEvent(self, event): QGraphicsScene.keyPressEvent(self, event) if event.key() == Qt.Key_F: if not self.insert_mode: self.velocity_mode = False self.insert_mode = True self.makeGhostNote(self.mousePos.x(), self.mousePos.y()) self.modeupdate.emit('insert_mode') elif self.insert_mode: self.insert_mode = False if self.place_ghost: self.place_ghost = False self.removeItem(self.ghost_note) self.ghost_note = None self.modeupdate.emit('') elif event.key() == Qt.Key_D: if self.velocity_mode: self.velocity_mode = False self.modeupdate.emit('') else: if self.insert_mode: self.removeItem(self.ghost_note) self.ghost_note = None self.insert_mode = False self.place_ghost = False self.velocity_mode = True self.modeupdate.emit('velocity_mode') elif event.key() == Qt.Key_A: if all((note.isSelected() for note in self.notes)): for note in self.notes: note.setSelected(False) self.selected_notes = [] else: for note in self.notes: note.setSelected(True) self.selected_notes = self.notes[:] elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace): self.notes = [note for note in self.notes if note not in self.selected_notes] for note in self.selected_notes: self.removeItem(note) self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]]) del note self.selected_notes = [] def mousePressEvent(self, event): QGraphicsScene.mousePressEvent(self, event) if not (any(key.pressed for key in self.piano_keys) or any(note.pressed for note in self.notes)): for note in self.selected_notes: note.setSelected(False) self.selected_notes = [] if event.button() == Qt.LeftButton: if self.insert_mode: self.place_ghost = True else: self.marquee_select = True self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1) self.marquee = QGraphicsRectItem(self.marquee_rect) self.marquee.setBrush(QColor(255, 255, 255, 100)) self.addItem(self.marquee) else: for s_note in self.notes: if s_note.pressed and s_note in self.selected_notes: break elif s_note.pressed and s_note not in self.selected_notes: for note in self.selected_notes: note.setSelected(False) self.selected_notes = [s_note] break for note in self.selected_notes: if not self.velocity_mode: note.mousePressEvent(event) def mouseMoveEvent(self, event): QGraphicsScene.mouseMoveEvent(self, event) self.mousePos = event.scenePos() if not (any((key.pressed for key in self.piano_keys))): m_pos = event.scenePos() if self.insert_mode and self.place_ghost: #placing a note m_width = self.ghost_rect.x() + self.ghost_rect_orig_width if m_pos.x() > m_width: m_new_x = self.snap(m_pos.x()) self.ghost_rect.setRight(m_new_x) self.ghost_note.setRect(self.ghost_rect) #self.adjust_note_vel(event) else: m_pos = self.enforce_bounds(m_pos) if self.insert_mode: #ghostnote follows mouse around (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y()) self.ghost_rect.moveTo(m_new_x, m_new_y) try: self.ghost_note.setRect(self.ghost_rect) except RuntimeError: self.ghost_note = None self.makeGhostNote(m_new_x, m_new_y) elif self.marquee_select: marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton) if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y(): self.marquee_rect.setBottomRight(m_pos) elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y(): self.marquee_rect.setTopRight(m_pos) elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y(): self.marquee_rect.setBottomLeft(m_pos) elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y(): self.marquee_rect.setTopLeft(m_pos) self.marquee.setRect(self.marquee_rect) self.selected_notes = [] for item in self.collidingItems(self.marquee): if item in self.notes: self.selected_notes.append(item) for note in self.notes: if note in self.selected_notes: note.setSelected(True) else: note.setSelected(False) elif self.velocity_mode: if Qt.LeftButton == event.buttons(): for note in self.selected_notes: note.updateVelocity(event) elif not self.marquee_select: #move selected if Qt.LeftButton == event.buttons(): x = y = False if any(note.back.stretch for note in self.selected_notes): x = True elif any(note.front.stretch for note in self.selected_notes): y = True for note in self.selected_notes: note.back.stretch = x note.front.stretch = y note.moveEvent(event) def mouseReleaseEvent(self, event): if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))): if event.button() == Qt.LeftButton: if self.place_ghost and self.insert_mode: self.place_ghost = False note_start = self.get_note_start_from_x(self.ghost_rect.x()) note_num = self.get_note_num_from_y(self.ghost_rect.y()) note_length = self.get_note_length_from_x(self.ghost_rect.width()) self.drawNote(note_num, note_start, note_length, self.ghost_vel) self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel]) self.makeGhostNote(self.mousePos.x(), self.mousePos.y()) elif self.marquee_select: self.marquee_select = False self.removeItem(self.marquee) elif not self.marquee_select: for note in self.selected_notes: old_info = note.note[:] note.mouseReleaseEvent(event) if self.velocity_mode: note.setSelected(True) if not old_info == note.note: self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]]) self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]]) # ------------------------------------------------------------------------- # Internal Functions def drawHeader(self): self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height) #self.header.setZValue(1.0) self.header.setPos(self.piano_width, 0) self.addItem(self.header) def drawPiano(self): piano_keys_width = self.piano_width - self.padding labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C') black_notes = (2,4,6,9,11) piano_label = QFont() piano_label.setPointSize(6) self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height) self.piano.setPos(0, self.header_height) self.addItem(self.piano) key = PianoKeyItem(piano_keys_width, self.note_height, self.piano) label = QGraphicsSimpleTextItem('C8', key) label.setPos(18, 1) label.setFont(piano_label) key.setBrush(QColor(255, 255, 255)) for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1): for j in range(self.notes_in_octave, 0, -1): if j in black_notes: key = PianoKeyItem(piano_keys_width/1.4, self.note_height, self.piano) key.setBrush(QColor(0, 0, 0)) key.setZValue(1.0) key.setPos(0, self.note_height * j + self.octave_height * (i - 1)) elif (j - 1) and (j + 1) in black_notes: key = PianoKeyItem(piano_keys_width, self.note_height * 2, self.piano) key.setBrush(QColor(255, 255, 255)) key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.) elif (j - 1) in black_notes: key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano) key.setBrush(QColor(255, 255, 255)) key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.) elif (j + 1) in black_notes: key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano) key.setBrush(QColor(255, 255, 255)) key.setPos(0, self.note_height * j + self.octave_height * (i - 1)) if j == 12: label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i), key ) label.setPos(18, 6) label.setFont(piano_label) self.piano_keys.append(key) def drawGrid(self): black_notes = [2,4,6,9,11] scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, 0) scale_bar.setBrush(QColor(100,100,100)) clearpen = QPen(QColor(0,0,0,0)) for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1): for j in range(self.notes_in_octave, 0, -1): scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1)) scale_bar.setPen(clearpen) if j not in black_notes: scale_bar.setBrush(QColor(120,120,120)) else: scale_bar.setBrush(QColor(100,100,100)) measure_pen = QPen(QColor(0, 0, 0, 120), 3) half_measure_pen = QPen(QColor(0, 0, 0, 40), 2) line_pen = QPen(QColor(0, 0, 0, 40)) for i in range(0, int(self.num_measures) + 1): measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header) measure.setPos(self.measure_width * i, 0.5 * measure_pen.width()) measure.setPen(measure_pen) if i < self.num_measures: number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header) number.setPos(self.measure_width * i + 5, 2) number.setBrush(Qt.white) for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.): line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header) line.setZValue(1.0) line.setPos(self.measure_width * i + self.value_width * j, self.header_height) if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0: line.setPen(half_measure_pen) else: line.setPen(line_pen) def drawPlayHead(self): self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height) self.play_head.setPen(QPen(QColor(255,255,255,50), 2)) self.play_head.setZValue(1.) self.addItem(self.play_head) def refreshScene(self): list(map(self.removeItem, self.notes)) self.selected_notes = [] self.piano_keys = [] self.clear() self.drawPiano() self.drawHeader() self.drawGrid() self.drawPlayHead() for note in self.notes[:]: if note.note[1] >= (self.num_measures * self.time_sig[0]): self.notes.remove(note) self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]]) elif note.note[2] > self.max_note_length: new_note = note.note[:] new_note[2] = self.max_note_length self.notes.remove(note) self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False) self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]]) self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]]) list(map(self.addItem, self.notes)) if self.views(): self.views()[0].setSceneRect(self.itemsBoundingRect()) def clearNotes(self): self.clear() self.notes = [] self.selected_notes = [] self.drawPiano() self.drawHeader() self.drawGrid() def makeGhostNote(self, pos_x, pos_y): """creates the ghostnote that is placed on the scene before the real one is.""" if self.ghost_note: self.removeItem(self.ghost_note) length = self.full_note_width * self.default_length (start, note) = self.snap(pos_x, pos_y) self.ghost_vel = self.default_ghost_vel self.ghost_rect = QRectF(start, note, length, self.note_height) self.ghost_rect_orig_width = self.ghost_rect.width() self.ghost_note = QGraphicsRectItem(self.ghost_rect) self.ghost_note.setBrush(QColor(230, 221, 45, 100)) self.addItem(self.ghost_note) def drawNote(self, note_num, note_start=None, note_length=None, note_velocity=None, add=True): """ note_num: midi number, 0 - 127 note_start: 0 - (num_measures * time_sig[0]) so this is in beats note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures note_velocity: 0 - 127 """ info = [note_num, note_start, note_length, note_velocity] if not note_start % (self.num_measures * self.time_sig[0]) == note_start: #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity]) while not note_start % (self.num_measures * self.time_sig[0]) == note_start: self.setMeasures(self.num_measures+1) self.measureupdate.emit(self.num_measures) self.refreshScene() x_start = self.get_note_x_start(note_start) if note_length > self.max_note_length: note_length = self.max_note_length + 0.25 x_length = self.get_note_x_length(note_length) y_pos = self.get_note_y_pos(note_num) note = NoteItem(self.note_height, x_length, info) note.setPos(x_start, y_pos) self.notes.append(note) if add: self.addItem(note) # ------------------------------------------------------------------------- # Helper Functions def frange(self, x, y, t): while x < y: yield x x += t def quantize(self, value): self.snap_value = float(self.full_note_width) * value if value else None def snap(self, pos_x, pos_y = None): if self.snap_value: pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) \ * self.snap_value + self.piano_width if pos_y: pos_y = int((pos_y - self.header_height) / self.note_height) \ * self.note_height + self.header_height return (pos_x, pos_y) if pos_y else pos_x def adjust_note_vel(self, event): m_pos = event.scenePos() #bind velocity to vertical mouse movement self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10 if self.ghost_vel < 0: self.ghost_vel = 0 elif self.ghost_vel > 127: self.ghost_vel = 127 m_width = self.ghost_rect.x() + self.ghost_rect_orig_width if m_pos.x() < m_width: m_pos.setX(m_width) m_new_x = self.snap(m_pos.x()) self.ghost_rect.setRight(m_new_x) self.ghost_note.setRect(self.ghost_rect) def enforce_bounds(self, pos): if pos.x() < self.piano_width: pos.setX(self.piano_width) elif pos.x() > self.grid_width + self.piano_width: pos.setX(self.grid_width + self.piano_width) if pos.y() < self.header_height + self.padding: pos.setY(self.header_height + self.padding) return pos def get_note_start_from_x(self, note_x): return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0]) def get_note_x_start(self, note_start): return self.piano_width + \ (self.grid_width / self.num_measures / self.time_sig[0]) * note_start def get_note_x_length(self, note_length): return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures def get_note_length_from_x(self, note_x): return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width \ * note_x def get_note_y_pos(self, note_num): return self.header_height + self.note_height * (self.total_notes - note_num - 1) def get_note_num_from_y(self, note_y_pos): return -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
class PianoRoll(QGraphicsScene): '''the piano roll''' noteclicked = pyqtSignal(int,bool) midievent = pyqtSignal(list) measureupdate = pyqtSignal(int) modeupdate = pyqtSignal(str) default_ghost_vel = 100 def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'): QGraphicsScene.__init__(self) self.setBackgroundBrush(QColor(50, 50, 50)) self.notes = [] self.removed_notes = [] self.selected_notes = [] self.piano_keys = [] self.marquee_select = False self.marquee_rect = None self.marquee = None self.ghost_note = None self.ghost_rect = None self.ghost_rect_orig_width = None self.ghost_vel = self.default_ghost_vel self.ignore_mouse_events = False self.insert_mode = False self.velocity_mode = False self.place_ghost = False self.last_mouse_pos = QPointF() ## dimensions self.padding = 2 ## piano dimensions self.note_height = 10 self.start_octave = -2 self.end_octave = 8 self.notes_in_octave = 12 self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1 self.piano_height = self.note_height * self.total_notes self.octave_height = self.notes_in_octave * self.note_height self.piano_width = 34 ## height self.header_height = 20 self.total_height = self.piano_height - self.note_height + self.header_height #not sure why note_height is subtracted ## width self.full_note_width = 250 # i.e. a 4/4 note self.snap_value = None self.quantize_val = quantize_val ### dummy vars that will be changed self.time_sig = (0,0) self.measure_width = 0 self.num_measures = 0 self.max_note_length = 0 self.grid_width = 0 self.value_width = 0 self.grid_div = 0 self.piano = None self.header = None self.play_head = None self.setGridDiv() self.default_length = 1. / self.grid_div # ------------------------------------------------------------------------- # Callbacks def movePlayHead(self, transportInfo): ticksPerBeat = transportInfo['ticksPerBeat'] max_ticks = ticksPerBeat * self.time_sig[0] * self.num_measures cur_tick = ticksPerBeat * self.time_sig[0] * transportInfo['bar'] + ticksPerBeat * transportInfo['beat'] + transportInfo['tick'] frac = (cur_tick % max_ticks) / max_ticks self.play_head.setPos(QPointF(frac * self.grid_width, 0)) def setTimeSig(self, time_sig): self.time_sig = time_sig self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1] self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] self.grid_width = self.measure_width * self.num_measures self.setGridDiv() def setMeasures(self, measures): #try: self.num_measures = float(measures) self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] self.grid_width = self.measure_width * self.num_measures self.refreshScene() #except: #pass def setDefaultLength(self, length): v = list(map(float, length.split('/'))) if len(v) < 3: self.default_length = v[0] if len(v) == 1 else v[0] / v[1] pos = self.enforce_bounds(self.last_mouse_pos) if self.insert_mode: self.makeGhostNote(pos.x(), pos.y()) def setGridDiv(self, div=None): if not div: div = self.quantize_val try: val = list(map(int, div.split('/'))) if len(val) < 3: self.quantize_val = div self.grid_div = val[0] if len(val)==1 else val[1] self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None self.setQuantize(div) self.refreshScene() except ValueError: pass def setQuantize(self, value): val = list(map(float, value.split('/'))) if len(val) == 1: self.quantize(val[0]) self.quantize_val = value elif len(val) == 2: self.quantize(val[0] / val[1]) self.quantize_val = value # ------------------------------------------------------------------------- # Event Callbacks def keyPressEvent(self, event): QGraphicsScene.keyPressEvent(self, event) if event.key() == Qt.Key_F: if not self.insert_mode: # turn off velocity mode self.velocity_mode = False # enable insert mode self.insert_mode = True self.place_ghost = False self.makeGhostNote(self.last_mouse_pos.x(), self.last_mouse_pos.y()) self.modeupdate.emit('insert_mode') else: # turn off insert mode self.insert_mode = False self.place_ghost = False if self.ghost_note is not None: self.removeItem(self.ghost_note) self.ghost_note = None self.modeupdate.emit('') elif event.key() == Qt.Key_D: if not self.velocity_mode: # turn off insert mode self.insert_mode = False self.place_ghost = False if self.ghost_note is not None: self.removeItem(self.ghost_note) self.ghost_note = None # enable velocity mode self.velocity_mode = True self.modeupdate.emit('velocity_mode') else: # turn off velocity mode self.velocity_mode = False self.modeupdate.emit('') elif event.key() == Qt.Key_A: for note in self.notes: if not note.isSelected(): has_unselected = True break else: has_unselected = False # select all notes if has_unselected: for note in self.notes: note.setSelected(True) self.selected_notes = self.notes[:] # unselect all else: for note in self.notes: note.setSelected(False) self.selected_notes = [] elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace): # remove selected notes from our notes list self.notes = [note for note in self.notes if note not in self.selected_notes] # delete the selected notes for note in self.selected_notes: self.removeItem(note) self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]]) del note self.selected_notes = [] def mousePressEvent(self, event): QGraphicsScene.mousePressEvent(self, event) # mouse click on left-side piano area if self.piano.contains(event.scenePos()): self.ignore_mouse_events = True return clicked_notes = [] for note in self.notes: if note.pressed or note.back.stretch or note.front.stretch: clicked_notes.append(note) print("clicked_notes", clicked_notes) # mouse click on existing notes if clicked_notes: keep_selection = all(note in self.selected_notes for note in clicked_notes) if keep_selection: for note in self.selected_notes: note.setSelected(True) return for note in self.selected_notes: if note not in clicked_notes: note.setSelected(False) for note in clicked_notes: if note not in self.selected_notes: note.setSelected(True) self.selected_notes = clicked_notes return # mouse click on empty area (no note selected) for note in self.selected_notes: note.setSelected(False) self.selected_notes = [] if event.button() != Qt.LeftButton: return if self.insert_mode: self.place_ghost = True else: self.marquee_select = True self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1) self.marquee = QGraphicsRectItem(self.marquee_rect) self.marquee.setBrush(QColor(255, 255, 255, 100)) self.addItem(self.marquee) def mouseMoveEvent(self, event): QGraphicsScene.mouseMoveEvent(self, event) self.last_mouse_pos = event.scenePos() if self.ignore_mouse_events: return pos = self.enforce_bounds(self.last_mouse_pos) if self.insert_mode: if self.ghost_note is None: self.makeGhostNote(pos.x(), pos.y()) max_x = self.grid_width + self.piano_width # placing note, only width needs updating if self.place_ghost: pos_x = pos.x() min_x = self.ghost_rect.x() + self.ghost_rect_orig_width if pos_x < min_x: pos_x = min_x new_x = self.snap(pos_x) self.ghost_rect.setRight(new_x) self.ghost_note.setRect(self.ghost_rect) #self.adjust_note_vel(event) # ghostnote following mouse around else: pos_x = pos.x() if pos_x + self.ghost_rect.width() >= max_x: pos_x = max_x - self.ghost_rect.width() elif pos_x > self.piano_width + self.ghost_rect.width()*3/4: pos_x -= self.ghost_rect.width()/2 new_x, new_y = self.snap(pos_x, pos.y()) self.ghost_rect.moveTo(new_x, new_y) self.ghost_note.setRect(self.ghost_rect) return if self.marquee_select: marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton) if marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() < pos.y(): self.marquee_rect.setBottomRight(pos) elif marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() > pos.y(): self.marquee_rect.setTopRight(pos) elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() < pos.y(): self.marquee_rect.setBottomLeft(pos) elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() > pos.y(): self.marquee_rect.setTopLeft(pos) self.marquee.setRect(self.marquee_rect) for note in self.selected_notes: note.setSelected(False) self.selected_notes = [] for item in self.collidingItems(self.marquee): if item in self.notes: item.setSelected(True) self.selected_notes.append(item) return if event.buttons() != Qt.LeftButton: return if self.velocity_mode: for note in self.selected_notes: note.updateVelocity(event) return x = y = False for note in self.selected_notes: if note.back.stretch: x = True break for note in self.selected_notes: if note.front.stretch: y = True break for note in self.selected_notes: note.back.stretch = x note.front.stretch = y note.moveEvent(event) def mouseReleaseEvent(self, event): QGraphicsScene.mouseReleaseEvent(self, event) if self.ignore_mouse_events: self.ignore_mouse_events = False return if self.marquee_select: self.marquee_select = False self.removeItem(self.marquee) self.marquee = None if self.insert_mode and self.place_ghost: self.place_ghost = False note_start = self.get_note_start_from_x(self.ghost_rect.x()) note_num = self.get_note_num_from_y(self.ghost_rect.y()) note_length = self.get_note_length_from_x(self.ghost_rect.width()) note = self.drawNote(note_num, note_start, note_length, self.ghost_vel) note.setSelected(True) self.selected_notes.append(note) self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel]) pos = self.enforce_bounds(self.last_mouse_pos) pos_x = pos.x() if pos_x > self.piano_width + self.ghost_rect.width()*3/4: pos_x -= self.ghost_rect.width()/2 self.makeGhostNote(pos_x, pos.y()) for note in self.selected_notes: note.back.stretch = False note.front.stretch = False # ------------------------------------------------------------------------- # Internal Functions def drawHeader(self): self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height) #self.header.setZValue(1.0) self.header.setPos(self.piano_width, 0) self.addItem(self.header) def drawPiano(self): piano_keys_width = self.piano_width - self.padding labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C') black_notes = (2,4,6,9,11) piano_label = QFont() piano_label.setPointSize(6) self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height) self.piano.setPos(0, self.header_height) self.addItem(self.piano) key = PianoKeyItem(piano_keys_width, self.note_height, 78, self.piano) label = QGraphicsSimpleTextItem('C9', key) label.setPos(18, 1) label.setFont(piano_label) key.setBrush(QColor(255, 255, 255)) for i in range(self.end_octave - self.start_octave, 0, -1): for j in range(self.notes_in_octave, 0, -1): note = (self.end_octave - i + 3) * 12 - j if j in black_notes: key = PianoKeyItem(piano_keys_width/1.4, self.note_height, note, self.piano) key.setBrush(QColor(0, 0, 0)) key.setZValue(1.0) key.setPos(0, self.note_height * j + self.octave_height * (i - 1)) elif (j - 1) and (j + 1) in black_notes: key = PianoKeyItem(piano_keys_width, self.note_height * 2, note, self.piano) key.setBrush(QColor(255, 255, 255)) key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.) elif (j - 1) in black_notes: key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano) key.setBrush(QColor(255, 255, 255)) key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.) elif (j + 1) in black_notes: key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano) key.setBrush(QColor(255, 255, 255)) key.setPos(0, self.note_height * j + self.octave_height * (i - 1)) if j == 12: label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i + 1), key) label.setPos(18, 6) label.setFont(piano_label) self.piano_keys.append(key) def drawGrid(self): black_notes = [2,4,6,9,11] scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, 0) scale_bar.setBrush(QColor(100,100,100)) clearpen = QPen(QColor(0,0,0,0)) for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1): for j in range(self.notes_in_octave, 0, -1): scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano) scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1)) scale_bar.setPen(clearpen) if j not in black_notes: scale_bar.setBrush(QColor(120,120,120)) else: scale_bar.setBrush(QColor(100,100,100)) measure_pen = QPen(QColor(0, 0, 0, 120), 3) half_measure_pen = QPen(QColor(0, 0, 0, 40), 2) line_pen = QPen(QColor(0, 0, 0, 40)) for i in range(0, int(self.num_measures) + 1): measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header) measure.setPos(self.measure_width * i, 0.5 * measure_pen.width()) measure.setPen(measure_pen) if i < self.num_measures: number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header) number.setPos(self.measure_width * i + 5, 2) number.setBrush(Qt.white) for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.): line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header) line.setZValue(1.0) line.setPos(self.measure_width * i + self.value_width * j, self.header_height) if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0: line.setPen(half_measure_pen) else: line.setPen(line_pen) def drawPlayHead(self): self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height) self.play_head.setPen(QPen(QColor(255,255,255,50), 2)) self.play_head.setZValue(1.) self.addItem(self.play_head) def refreshScene(self): list(map(self.removeItem, self.notes)) self.selected_notes = [] self.piano_keys = [] self.place_ghost = False if self.ghost_note is not None: self.removeItem(self.ghost_note) self.ghost_note = None self.clear() self.drawPiano() self.drawHeader() self.drawGrid() self.drawPlayHead() for note in self.notes[:]: if note.note[1] >= (self.num_measures * self.time_sig[0]): self.notes.remove(note) self.removed_notes.append(note) #self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]]) elif note.note[2] > self.max_note_length: new_note = note.note[:] new_note[2] = self.max_note_length self.notes.remove(note) self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False) self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]]) self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]]) for note in self.removed_notes[:]: if note.note[1] < (self.num_measures * self.time_sig[0]): self.removed_notes.remove(note) self.notes.append(note) list(map(self.addItem, self.notes)) if self.views(): self.views()[0].setSceneRect(self.itemsBoundingRect()) def clearNotes(self): self.clear() self.notes = [] self.removed_notes = [] self.selected_notes = [] self.drawPiano() self.drawHeader() self.drawGrid() def makeGhostNote(self, pos_x, pos_y): """creates the ghostnote that is placed on the scene before the real one is.""" if self.ghost_note is not None: self.removeItem(self.ghost_note) length = self.full_note_width * self.default_length pos_x, pos_y = self.snap(pos_x, pos_y) self.ghost_vel = self.default_ghost_vel self.ghost_rect = QRectF(pos_x, pos_y, length, self.note_height) self.ghost_rect_orig_width = self.ghost_rect.width() self.ghost_note = QGraphicsRectItem(self.ghost_rect) self.ghost_note.setBrush(QColor(230, 221, 45, 100)) self.addItem(self.ghost_note) def drawNote(self, note_num, note_start, note_length, note_velocity, add=True): """ note_num: midi number, 0 - 127 note_start: 0 - (num_measures * time_sig[0]) so this is in beats note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures note_velocity: 0 - 127 """ info = [note_num, note_start, note_length, note_velocity] if not note_start % (self.num_measures * self.time_sig[0]) == note_start: #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity]) while not note_start % (self.num_measures * self.time_sig[0]) == note_start: self.setMeasures(self.num_measures+1) self.measureupdate.emit(self.num_measures) self.refreshScene() x_start = self.get_note_x_start(note_start) if note_length > self.max_note_length: note_length = self.max_note_length + 0.25 x_length = self.get_note_x_length(note_length) y_pos = self.get_note_y_pos(note_num) note = NoteItem(self.note_height, x_length, info) note.setPos(x_start, y_pos) self.notes.append(note) if add: self.addItem(note) return note # ------------------------------------------------------------------------- # Helper Functions def frange(self, x, y, t): while x < y: yield x x += t def quantize(self, value): self.snap_value = float(self.full_note_width) * value if value else None def snap(self, pos_x, pos_y = None): if self.snap_value: pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) * self.snap_value + self.piano_width if pos_y is not None: pos_y = int((pos_y - self.header_height) / self.note_height) * self.note_height + self.header_height return (pos_x, pos_y) if pos_y is not None else pos_x def adjust_note_vel(self, event): m_pos = event.scenePos() #bind velocity to vertical mouse movement self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10 if self.ghost_vel < 0: self.ghost_vel = 0 elif self.ghost_vel > 127: self.ghost_vel = 127 m_width = self.ghost_rect.x() + self.ghost_rect_orig_width if m_pos.x() < m_width: m_pos.setX(m_width) m_new_x = self.snap(m_pos.x()) self.ghost_rect.setRight(m_new_x) self.ghost_note.setRect(self.ghost_rect) def enforce_bounds(self, pos): pos = QPointF(pos) if pos.x() < self.piano_width: pos.setX(self.piano_width) elif pos.x() >= self.grid_width + self.piano_width: pos.setX(self.grid_width + self.piano_width - 1) if pos.y() < self.header_height + self.padding: pos.setY(self.header_height + self.padding) return pos def get_note_start_from_x(self, note_x): return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0]) def get_note_x_start(self, note_start): return self.piano_width + (self.grid_width / self.num_measures / self.time_sig[0]) * note_start def get_note_x_length(self, note_length): return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures def get_note_length_from_x(self, note_x): return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width * note_x def get_note_y_pos(self, note_num): return self.header_height + self.note_height * (self.total_notes - note_num - 1) def get_note_num_from_y(self, note_y_pos): return -(int((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1) def move_note(self, old_note, new_note): self.midievent.emit(["midievent-remove", old_note[0], old_note[1], old_note[2], old_note[3]]) self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
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 painter(QGraphicsView): pixelSize = int(15 / narrowRatio) width = int(480 / narrowRatio) height = int(360 / narrowRatio) fontSize = int(15 / narrowRatio) anchorLineSize = int(100 / narrowRatio) ellipseRadius = int(8 / narrowRatio) textInterval = int(90 / narrowRatio) col = width / pixelSize line = height / pixelSize centerIndex = int(round(((line / 2 - 1) * col) + col / 2)) frameCount = 0 failedCount = 0 baseZValue = 0 mode = 1 body = 1 open_status = 0 textLineHeight = fontSize + 10 blurRaduis = 50 # Smoother improvement def __init__(self, dataThread): super(painter, self).__init__() self.dataThread = dataThread self.setFixedSize(self.width + 200, self.height + self.textLineHeight) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scene = QGraphicsScene() self.setScene(self.scene) # center het text item self.centerTextItem = QGraphicsTextItem() self.centerTextItem.setPos((self.width + 200) / 2 - self.fontSize, 0) self.centerTextItem.setZValue(self.baseZValue + 1) self.scene.addItem(self.centerTextItem) # version text item self.versionTextItem = QGraphicsTextItem() self.versionTextItem.setPos(self.width * 0.8 - self.fontSize, self.height * 0.8 - self.fontSize) self.versionTextItem.setZValue(self.baseZValue + 1) self.scene.addItem(self.versionTextItem) self.userCom = None # center anchor item centerX = self.width / 2 centerY = self.height / 2 self.ellipseItem = QGraphicsEllipseItem(0, 0, self.ellipseRadius * 2, self.ellipseRadius * 2) self.horLineItem = QGraphicsLineItem(0, 0, self.anchorLineSize, 0) self.verLineItem = QGraphicsLineItem(0, 0, 0, self.anchorLineSize) self.ellipseItem.setPos(centerX - self.ellipseRadius, centerY - self.ellipseRadius) self.horLineItem.setPos(centerX - self.anchorLineSize / 2, centerY) self.verLineItem.setPos(centerX, centerY - self.anchorLineSize / 2) self.ellipseItem.setPen(QColor(Qt.white)) self.horLineItem.setPen(QColor(Qt.white)) self.verLineItem.setPen(QColor(Qt.white)) self.ellipseItem.setZValue(self.baseZValue + 1) self.horLineItem.setZValue(self.baseZValue + 1) self.verLineItem.setZValue(self.baseZValue + 1) self.scene.addItem(self.ellipseItem) self.scene.addItem(self.horLineItem) self.scene.addItem(self.verLineItem) # camera item self.cameraBuffer = QPixmap(self.width, self.height + self.textLineHeight) self.cameraItem = QGraphicsPixmapItem() if useBlur: self.gusBlurEffect = QGraphicsBlurEffect() self.gusBlurEffect.setBlurRadius(self.blurRaduis) self.cameraItem.setGraphicsEffect(self.gusBlurEffect) self.cameraItem.setPos(100, 0) self.cameraItem.setZValue(self.baseZValue) self.scene.addItem(self.cameraItem) # het text item self.hetTextBuffer = QPixmap(self.width + 200, self.textLineHeight) self.hetTextItem = QGraphicsPixmapItem() self.hetTextItem.setPos(0, self.height) self.hetTextItem.setZValue(self.baseZValue) self.scene.addItem(self.hetTextItem) # button item self.ctrlOpenButton = QPushButton('Open', self) self.ctrlOpenButton.clicked.connect(self.ctrlOpen) self.ctrlOpenButton.setGeometry(10, 30, 100, 40) self.EvaluateButton = QPushButton('None', self) self.EvaluateButton.clicked.connect(self.evaluate) self.EvaluateButton.setGeometry(10, 80, 100, 40) self.getOnePicButton = QPushButton('Get a frame', self) self.getOnePicButton.clicked.connect(self.ctrlSendone) self.getOnePicButton.setGeometry(10, 130, 100, 40) self.getOnePicButton.setEnabled(False) self.modeManualButton = QPushButton('Auto', self) self.modeManualButton.clicked.connect(self.modeManual) self.modeManualButton.setGeometry(10, 180, 100, 40) self.modeObjButton = QPushButton('Body', self) self.modeObjButton.clicked.connect(self.objBody) self.modeObjButton.setGeometry(10, 230, 100, 40) self.modeFpsButton = QPushButton('4FPS', self) self.modeFpsButton.clicked.connect(self.rate) self.modeFpsButton.setGeometry(10, 280, 100, 40) self.modeAutoButton = QPushButton('Get offset', self) self.modeAutoButton.clicked.connect(self.commonOffset) self.modeAutoButton.setGeometry(570, 30, 100, 40) self.modeAutoButton = QPushButton('Get version', self) self.modeAutoButton.clicked.connect(self.sysVer) self.modeAutoButton.setGeometry(570, 80, 100, 40) self.evaluate = 1 self.evaluateButton = QPushButton('Evaluate', self) self.evaluateButton.clicked.connect(self.setEvaluate) self.evaluateButton.setGeometry(570, 120, 100, 40) self.serial_l = QLabel(self) self.serial_l.move(580, 250) self.serial_l.resize(60, 30) self.serial_l.setStyleSheet( "QLabel{color:rgb(0,0,0,255);background-color: rgb(255,255,255);font-size:16px;font-weight:normal;font-family:Arial;}" ) self.serial_l.setText("Serial:") self.serialList = SerialComboBox(self) self.serialList.setCurrentIndex(0) self.serialList.setStyleSheet( "border-width: 1px;border-style: solid;border-color: rgb(255, 170, 0);" ) self.serialList.currentIndexChanged.connect(self.serialChange) # self.evaluateButton.setGeometry(570,140,100,40) self.serialList.move(580, 280) self.serialList.resize(120, 30) self.serialList.addItem("Please select serial device") def checkSerial(self): reply = QMessageBox.information(self, 'No serial', "Please select serial device", QMessageBox.Yes) # portlist = SerialComboBox().get_port_list(self) # self.userCom = None # if portlist is not None: # for port in portlist: # print("port ",port) # self.userCom = port # if self.userCom is not None : # self.dataThread.openSerial(self.userCom) # else: # reply = QMessageBox.information(self, '没有设置串口', "请选择串口" , QMessageBox.Yes) def serialChange(self, i): print("serialChange", i, self.serialList.currentText()) if i > 0: self.userCom = self.serialList.currentText() self.dataThread.initSerialPort(self.userCom) def ctrlOpen(self): print("self.userCom", self.userCom) if self.userCom is None: self.checkSerial() return if self.open_status == 1: print('start send C command 0') self.open_status = 0 self.ctrlOpenButton.setText("Open") self.dataThread.sendData('CMDC\0') else: print('start send C command 1') self.open_status = 1 self.ctrlOpenButton.setText("Close") self.dataThread.sendData('CMDC\1') def evaluate(self): if self.userCom is None: self.checkSerial() return global evaluate_mode if evaluate_mode == "operate": print('start send E command 0') evaluate_mode = "evaluate" self.EvaluateButton.setText("Evaluate") self.dataThread.sendData('CMDE\1') else: print('start send E command 1') evaluate_mode = "operate" self.EvaluateButton.setText("Operate") self.dataThread.sendData('CMDE\0') def ctrlSendone(self): if self.userCom is None: self.checkSerial() return print('send a frame') self.dataThread.sendData('CMDC\2') def modeManual(self): if self.userCom is None: self.checkSerial() return if self.mode == 1: self.getOnePicButton.setEnabled(True) self.modeManualButton.setText("Manual") self.dataThread.sendData('CMDM\0') self.mode = 0 print('mode: manual') else: self.getOnePicButton.setEnabled(False) self.modeManualButton.setText("Auto") self.dataThread.sendData('CMDM\1') print('mode: auto') self.mode = 1 def modeAuto(self): if self.userCom is None: self.checkSerial() return print('mode: auto') self.dataThread.sendData('CMDM\1') def rate(self): if self.userCom is None: self.checkSerial() return global btn_index btn_index = (btn_index + 1) % 5 print('FPS:', strButton[btn_index]) self.modeFpsButton.setText(strButton[btn_index]) self.dataThread.sendData(strFpsCmd[btn_index]) #'CMDF\0') def objBody(self): if self.userCom is None: self.checkSerial() return if self.body == 1: self.body = 0 print('obj: Object') self.modeObjButton.setText("Object") self.dataThread.sendData('CMDO\0') else: self.body = 1 print('obj: Human Body') self.modeObjButton.setText("Body") self.dataThread.sendData('CMDO\1') def uiUpdate(self): global evaluate_mode print("update UI ", evaluate_mode) if evaluate_mode == "operate": self.EvaluateButton.setText("Operate") elif evaluate_mode == "evaluate": self.EvaluateButton.setText("Evaluate") else: self.EvaluateButton.setText("None") def cmdUpdate(self): if self.userCom is None: self.checkSerial() return print("get evaluate status and version\n") time.sleep(0.1) self.dataThread.sendData('CMDE\2') time.sleep(0.1) self.dataThread.sendData('CMDV\0') time.sleep(0.1) self.dataThread.sendData('CMDT\1') def commonOffset(self): if self.userCom is None: self.checkSerial() return self.dataThread.sendData('CMDT\1') print("Get commonOffset") def sysVer(self): if self.userCom is None: self.checkSerial() return self.dataThread.sendData('CMDV\1') print("Get firmware version and calibration version.") def setEvaluate(self): if self.userCom is None: self.checkSerial() return if self.evaluate == 1: self.evaluate = 0 self.evaluateButton.setText("Operate") self.dataThread.sendData('CMDE\0') else: self.evaluate = 1 self.evaluateButton.setText("Evaluate") self.dataThread.sendData('CMDE\1') def draw(self): global minHue global maxHue global maxHet global minHet global device_commonOffset global evaluate_mode if len(displayData) == 0: return font = QFont() color = QColor() font.setPointSize(self.fontSize) #font.setFamily("Microsoft YaHei") font.setLetterSpacing(QFont.AbsoluteSpacing, 0) index = 0 lock.acquire() frame = displayData.pop(0) lock.release() p = QPainter(self.cameraBuffer) p.fillRect(0, 0, self.width, self.height + self.textLineHeight, QBrush(QColor(Qt.black))) # draw camera color = QColor() cneter = 0.0 if chip == "90640": if self.centerIndex == 0: centerIndex = 12 * 32 + 16 for yIndex in range(int(self.height / self.pixelSize)): for xIndex in range(int(self.width / self.pixelSize)): color.setHsvF(frame[index] / 360, 1.0, 1.0) p.fillRect(xIndex * self.pixelSize, yIndex * self.pixelSize, self.pixelSize, self.pixelSize, QBrush(color)) index = index + 1 cneter = round( mapValue(frame[self.centerIndex], minHue, maxHue, minHet, maxHet), 1) elif chip == "90621": if self.centerIndex == 0 or self.centerIndex >= 64: self.centerIndex = 2 * 16 + 8 cneter = round( mapValue(frame[self.centerIndex], minHue, maxHue, minHet, maxHet), 1) for yIndex in range(int(self.height / self.pixelSize / 6)): for xIndex in range(int(self.width / self.pixelSize / 2)): color.setHsvF(frame[index] / 360, 1.0, 1.0) p.fillRect(xIndex * self.pixelSize * 2, yIndex * self.pixelSize * 2 + 160, self.pixelSize * 2, self.pixelSize * 2, QBrush(color)) index = index + 1 elif chip == "90641": if self.centerIndex == 0 or self.centerIndex >= 192: self.centerIndex = 6 * 16 + 8 cneter = round( mapValue(frame[self.centerIndex], minHue, maxHue, minHet, maxHet), 1) for yIndex in range(int(self.height / self.pixelSize / 2)): for xIndex in range(int(self.width / self.pixelSize / 2)): color.setHsvF(frame[index] / 360, 1.0, 1.0) p.fillRect(xIndex * self.pixelSize * 2, yIndex * self.pixelSize * 2, self.pixelSize * 2, self.pixelSize * 2, QBrush(color)) index = index + 1 else: print("Dsiplay Error: can't detect any chip type!") self.cameraItem.setPixmap(self.cameraBuffer) self.frameCount = self.frameCount + 1 # draw text p = QPainter(self.hetTextBuffer) p.fillRect(0, 0, self.width + 200, self.height + self.textLineHeight, QBrush(QColor(Qt.black))) version = self.dataThread.getID() p.setPen(QColor(Qt.white)) p.setFont(font) p.drawText(0, self.fontSize, " max:" + '{:.2f}'.format(maxHet)) p.drawText(self.textInterval, self.fontSize, " min:" + '{:.2f}'.format(minHet)) p.drawText(self.textInterval * 2, self.fontSize, "offset:" + '{:.2f}'.format(device_commonOffset)) p.drawText(self.textInterval * 3, self.fontSize, "mode:" + detect_status) p.drawText(self.textInterval * 4, self.fontSize, evaluate_mode) p.drawText(self.textInterval * 5, self.fontSize, "ID:" + str(version[0])) p.drawText(self.textInterval * 6, self.fontSize, "ver:" + str(version[1] & 0xff)) p.drawText(self.textInterval * 7, self.fontSize, chip) self.hetTextItem.setPixmap(self.hetTextBuffer) cneter = round( mapValue(frame[self.centerIndex], minHue, maxHue, minHet, maxHet), 1) centerText = "<font color=white>%s</font>" self.centerTextItem.setFont(font) self.centerTextItem.setHtml(centerText % (str(cneter) + "°")) # draw version text self.versionTextItem.setFont(font)
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 painter(QGraphicsView): narrowRatio = int(sys.argv[4]) if len(sys.argv) >= 5 else 1 useBlur = sys.argv[5] != "False" if len(sys.argv) >= 6 else True pixelSize = int(15 / narrowRatio) width = int(480 / narrowRatio) height = int(360 / narrowRatio) fontSize = int(30 / narrowRatio) anchorLineSize = int(100 / narrowRatio) ellipseRadius = int(8 / narrowRatio) textInterval = int(90 / narrowRatio) col = width / pixelSize line = height / pixelSize centerIndex = int(round(((line / 2 - 1) * col) + col / 2)) frameCount = 0 baseZValue = 0 textLineHeight = fontSize + 10 blurRaduis = 50 # Smoother improvement def __init__(self): super(painter, self).__init__() self.setFixedSize(self.width, self.height + self.textLineHeight) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scene = QGraphicsScene() self.setScene(self.scene) # center het text item self.centerTextItem = QGraphicsTextItem() self.centerTextItem.setPos(self.width / 2 - self.fontSize, 0) self.centerTextItem.setZValue(self.baseZValue + 1) self.scene.addItem(self.centerTextItem) # center anchor item centerX = self.width / 2 centerY = self.height / 2 self.ellipseItem = QGraphicsEllipseItem(0, 0, self.ellipseRadius * 2, self.ellipseRadius * 2) self.horLineItem = QGraphicsLineItem(0, 0, self.anchorLineSize, 0) self.verLineItem = QGraphicsLineItem(0, 0, 0, self.anchorLineSize) self.ellipseItem.setPos(centerX - self.ellipseRadius, centerY - self.ellipseRadius) self.horLineItem.setPos(centerX - self.anchorLineSize / 2, centerY) self.verLineItem.setPos(centerX, centerY - self.anchorLineSize / 2) self.ellipseItem.setPen(QColor(Qt.white)) self.horLineItem.setPen(QColor(Qt.white)) self.verLineItem.setPen(QColor(Qt.white)) self.ellipseItem.setZValue(self.baseZValue + 1) self.horLineItem.setZValue(self.baseZValue + 1) self.verLineItem.setZValue(self.baseZValue + 1) self.scene.addItem(self.ellipseItem) self.scene.addItem(self.horLineItem) self.scene.addItem(self.verLineItem) # camera item self.cameraBuffer = QPixmap(self.width, self.height + self.textLineHeight) self.cameraItem = QGraphicsPixmapItem() if self.useBlur: self.gusBlurEffect = QGraphicsBlurEffect() self.gusBlurEffect.setBlurRadius(self.blurRaduis) self.cameraItem.setGraphicsEffect(self.gusBlurEffect) self.cameraItem.setPos(0, 0) self.cameraItem.setZValue(self.baseZValue) self.scene.addItem(self.cameraItem) # het text item self.hetTextBuffer = QPixmap(self.width, self.textLineHeight) self.hetTextItem = QGraphicsPixmapItem() self.hetTextItem.setPos(0, self.height) self.hetTextItem.setZValue(self.baseZValue) self.scene.addItem(self.hetTextItem) def draw(self): if len(hetaData) == 0: return font = QFont() color = QColor() font.setPointSize(self.fontSize) font.setFamily("Microsoft YaHei") font.setLetterSpacing(QFont.AbsoluteSpacing, 0) index = 0 lock.acquire() frame = hetaData.pop(0) lock.release() maxHet = frame["maxHet"] minHet = frame["minHet"] frame = frame["frame"] p = QPainter(self.cameraBuffer) p.fillRect(0, 0, self.width, self.height + self.textLineHeight, QBrush(QColor(Qt.black))) # draw camera color = QColor() for yIndex in range(int(self.height / self.pixelSize)): for xIndex in range(int(self.width / self.pixelSize)): tempData = constrain( mapValue(frame[index], minHet, maxHet, minHue, maxHue), minHue, maxHue) color.setHsvF(tempData / 360, 1.0, 1.0) p.fillRect(xIndex * self.pixelSize, yIndex * self.pixelSize, self.pixelSize, self.pixelSize, QBrush(color)) index = index + 1 self.cameraItem.setPixmap(self.cameraBuffer) # draw text p = QPainter(self.hetTextBuffer) p.fillRect(0, 0, self.width, self.height + self.textLineHeight, QBrush(QColor(Qt.black))) hetDiff = maxHet - minHet bastNum = round(minHet) interval = round(hetDiff / 5) for i in range(5): hue = constrain( mapValue((bastNum + (i * interval)), minHet, maxHet, minHue, maxHue), minHue, maxHue) color.setHsvF(hue / 360, 1.0, 1.0) p.setPen(color) p.setFont(font) p.drawText(i * self.textInterval, self.fontSize + 3, str(bastNum + (i * interval)) + "°") self.hetTextItem.setPixmap(self.hetTextBuffer) # draw center het text cneter = round(frame[self.centerIndex], 1) centerText = "<font color=white>%s</font>" self.centerTextItem.setFont(font) self.centerTextItem.setHtml(centerText % (str(cneter) + "°")) self.frameCount = self.frameCount + 1 print("picture->" + str(self.frameCount))
class BaseGraphic(object): def __init__(self, *args): super(BaseGraphic, self).__init__() self.label = args[3] self.parent = args[0] self.section_analyzer = self.parent.section_analyzer self.width = None self.height = None self.margin = None self.position = args[1], args[2] self.content_width = None self.content_height = None self.distance_pointer = None self.distance_label = None self.section_num = len(self.section_analyzer.section_list) self.section_distance = self.section_analyzer.section_distance self.length_multiplier = 100.0 self.height_multiplier = 100.0 self.line_extend = 20 self.margin = 50 self.material_legend = MaterialLegend(self) self._inited = False self.items = [] self.add_title() def add_title(self): if self.label: title = QGraphicsSimpleTextItem(self.label) title.setPos(self.position[0] + self.margin, self.position[1] + self.line_extend) self.addToGroup(title) def addToGroup(self, item): self.items.append(item) def create_distance_pointer(self): self.distance_pointer = QGraphicsLineItem() pen = QPen() pen.setWidthF(1.0) pen.setStyle(Qt.DashDotLine) color = Color.create_qcolor_from_rgb_tuple_f((1,0,0)) pen.setColor(color) self.distance_pointer.setPen(pen) self.distance_pointer.setZValue(1.0) self.addToGroup(self.distance_pointer) self.distance_label = QGraphicsSimpleTextItem() self.distance_label.setZValue(1.0) self.addToGroup(self.distance_label) def init_dimension(self): section_num = len(self.section_analyzer.section_list) section_distance = self.section_analyzer.section_distance self.content_width = section_num * section_distance * self.length_multiplier self.create_distance_pointer() self._inited = True def update_graph_size(self): if self.content_height and self.content_width: self.width = self.content_width + self.margin * 2 self.height = self.content_height + self.margin * 2 # bounding_rect.setWidth(self.width) # bounding_rect.setHeight(self.height) def set_distance_pointer(self, distance): if self._inited: x1 = self.position[0] + self.margin + distance * self.length_multiplier y1 = self.position[1] x2 = x1 y2 = y1 + self.height self.distance_pointer.setLine(x1, y1, x2, y2) self.distance_label.setText("%.2f" % distance) self.distance_label.setPos(x2,y2) pass @staticmethod def set_rect_fill(*args): if args[0] == 0: #surface color mode rect = args[1] color = args[2] qcolor = Color.create_qcolor_from_rgb_tuple_f(color) brush = QBrush(qcolor) rect.setBrush(brush) def create_legend(self): x = self.position[0] + self.width y = self.position[1] self.material_legend.create_material_legend(x, y) for item in self.material_legend.graphic_items: self.addToGroup(item)
class SliceNucleicAcidPartItem(QAbstractPartItem): """Parent should be either a SliceRootItem, or an AssemblyItem. Invariant: keys in _empty_helix_hash = range(_nrows) x range(_ncols) where x is the cartesian product. Attributes: active_virtual_helix_item (cadnano.views.sliceview.virtualhelixitem.SliceVirtualHelixItem): Description resize_handle_group (ResizeHandleGroup): handles for dragging and resizing griditem (GridItem): Description outline (QGraphicsRectItem): Description prexover_manager (PreXoverManager): Description scale_factor (float): Description """ _RADIUS = styles.SLICE_HELIX_RADIUS _BOUNDING_RECT_PADDING = 80 def __init__(self, model_part_instance, viewroot, parent=None): """Summary Args: model_part_instance (TYPE): Description viewroot (TYPE): Description parent (None, optional): Description """ super(SliceNucleicAcidPartItem, self).__init__(model_part_instance, viewroot, parent) self.shortest_path_start = None self.neighbor_map = dict() self.coordinates_to_vhid = dict() self.coordinates_to_xy = dict() self._last_hovered_item = None self._highlighted_path = [] self.spa_start_vhi = None self.last_mouse_position = None self._translated_x = 0.0 self._translated_y = 0.0 self._getActiveTool = viewroot.manager.activeToolGetter m_p = self._model_part self._controller = NucleicAcidPartItemController(self, m_p) self.scale_factor = self._RADIUS / m_p.radius() self.inverse_scale_factor = m_p.radius() / self._RADIUS self.active_virtual_helix_item = None self.prexover_manager = PreXoverManager(self) self.hide() # hide while until after attemptResize() to avoid flicker self._rect = QRectF(0., 0., 1000., 1000.) # set this to a token value self.boundRectToModel() self.setPen(getNoPen()) self.setRect(self._rect) self.setAcceptHoverEvents(True) self.shortest_path_add_mode = False # Cache of VHs that were active as of last call to activeSliceChanged # If None, all slices will be redrawn and the cache will be filled. # Connect destructor. This is for removing a part from scenes. # initialize the NucleicAcidPartItem with an empty set of old coords self.setZValue(styles.ZPARTITEM) self.outline = outline = QGraphicsRectItem(self) o_rect = self._configureOutline(outline) outline.setFlag(QGraphicsItem.ItemStacksBehindParent) outline.setZValue(styles.ZDESELECTOR) model_color = m_p.getColor() self.outline.setPen(getPenObj(model_color, _DEFAULT_WIDTH)) self.model_bounds_hint = QGraphicsRectItem(self) self.model_bounds_hint.setBrush(getBrushObj(model_color, alpha=12)) self.model_bounds_hint.setPen(getNoPen()) self.resize_handle_group = ResizeHandleGroup( o_rect, _HANDLE_SIZE, model_color, True, HandleType.TOP | HandleType.BOTTOM | HandleType.LEFT | HandleType.RIGHT | HandleType.TOP_LEFT | HandleType.TOP_RIGHT | HandleType.BOTTOM_LEFT | HandleType.BOTTOM_RIGHT, self, show_coords=True) self.griditem = GridItem(self, self._model_props['grid_type']) self.griditem.setZValue(1) self.resize_handle_group.setZValue(2) self.x_axis_line = QGraphicsLineItem(0, 0, self._RADIUS, 0, self) self.x_axis_line.setPen(getPenObj('#cc0000', _DEFAULT_WIDTH)) self.x_axis_line.setZValue(styles.ZAXIS) self.y_axis_line = QGraphicsLineItem(0, 0, 0, -self._RADIUS, self) self.y_axis_line.setPen(getPenObj('#007200', _DEFAULT_WIDTH)) self.y_axis_line.setZValue(styles.ZAXIS) # select upon creation for part in m_p.document().children(): if part is m_p: part.setSelected(True) else: part.setSelected(False) self.show() # end def ### SIGNALS ### ### SLOTS ### def partActiveVirtualHelixChangedSlot(self, part, id_num): """Summary Args: part (NucleicAcidPart): Description id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. """ vhi = self._virtual_helix_item_hash.get(id_num) self.setActiveVirtualHelixItem(vhi) self.setPreXoverItemsVisible(vhi) # end def def partActiveBaseInfoSlot(self, part, info): """Summary Args: part (TYPE): Description info (TYPE): Description Args: TYPE: Description """ pxom = self.prexover_manager pxom.deactivateNeighbors() if info and info is not None: id_num, is_fwd, idx, _ = info pxom.activateNeighbors(id_num, is_fwd, idx) # end def def partPropertyChangedSlot(self, model_part, property_key, new_value): """Summary Args: model_part (Part): The model part property_key (TYPE): Description new_value (TYPE): Description Args: TYPE: Description """ if self._model_part == model_part: self._model_props[property_key] = new_value if property_key == 'color': self.outline.setPen(getPenObj(new_value, _DEFAULT_WIDTH)) for vhi in self._virtual_helix_item_hash.values(): vhi.updateAppearance() self.resize_handle_group.setPens(getPenObj(new_value, 0)) elif property_key == 'is_visible': if new_value: self.show() else: self.hide() elif property_key == 'grid_type': self.griditem.setGridType(new_value) # end def def partRemovedSlot(self, sender): """docstring for partRemovedSlot Args: sender (obj): Model object that emitted the signal. """ self.parentItem().removePartItem(self) scene = self.scene() scene.removeItem(self) self._model_part = None self._mod_circ = None self._controller.disconnectSignals() self._controller = None self.resize_handle_group.removeHandles() self.griditem = None # end def def partVirtualHelicesTranslatedSlot(self, sender, vh_set, left_overs, do_deselect): """ left_overs are neighbors that need updating due to changes Args: sender (obj): Model object that emitted the signal. vh_set (TYPE): Description left_overs (TYPE): Description do_deselect (TYPE): Description """ if do_deselect: tool = self._getActiveTool() if tool.methodPrefix() == "selectTool": if tool.isSelectionActive(): # tool.deselectItems() tool.modelClear() # 1. move everything that moved for id_num in vh_set: vhi = self._virtual_helix_item_hash[id_num] vhi.updatePosition() # 2. now redraw what makes sense to be redrawn for id_num in vh_set: vhi = self._virtual_helix_item_hash[id_num] self._refreshVirtualHelixItemGizmos(id_num, vhi) for id_num in left_overs: vhi = self._virtual_helix_item_hash[id_num] self._refreshVirtualHelixItemGizmos(id_num, vhi) # 0. clear PreXovers: # self.prexover_manager.hideGroups() # if self.active_virtual_helix_item is not None: # self.active_virtual_helix_item.deactivate() # self.active_virtual_helix_item = None avhi = self.active_virtual_helix_item self.setPreXoverItemsVisible(avhi) self.enlargeRectToFit() # end def def _refreshVirtualHelixItemGizmos(self, id_num, vhi): """Update props and appearance of self & recent neighbors. Ultimately triggered by a partVirtualHelicesTranslatedSignal. Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. vhi (cadnano.views.sliceview.virtualhelixitem.SliceVirtualHelixItem): the item associated with id_num """ neighbors = vhi.cnModel().getProperty('neighbors') neighbors = literal_eval(neighbors) vhi.beginAddWedgeGizmos() for nvh in neighbors: nvhi = self._virtual_helix_item_hash.get(nvh, False) if nvhi: vhi.setWedgeGizmo(nvh, nvhi) # end for vhi.endAddWedgeGizmos() # end def def partVirtualHelixPropertyChangedSlot(self, sender, id_num, virtual_helix, keys, values): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. keys (tuple): keys that changed values (tuple): new values for each key that changed Args: TYPE: Description """ if self._model_part == sender: vh_i = self._virtual_helix_item_hash[id_num] vh_i.virtualHelixPropertyChangedSlot(keys, values) # end def def partVirtualHelixAddedSlot(self, sender, id_num, virtual_helix, neighbors): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. neighbors (TYPE): Description Args: TYPE: Description """ vhi = SliceVirtualHelixItem(virtual_helix, self) self._virtual_helix_item_hash[id_num] = vhi self._refreshVirtualHelixItemGizmos(id_num, vhi) for neighbor_id in neighbors: nvhi = self._virtual_helix_item_hash.get(neighbor_id, False) if nvhi: self._refreshVirtualHelixItemGizmos(neighbor_id, nvhi) self.enlargeRectToFit() position = sender.locationQt(id_num=id_num, scale_factor=self.scale_factor) coordinates = ShortestPathHelper.findClosestPoint( position=position, point_map=self.coordinates_to_xy) assert id_num not in self.coordinates_to_vhid.values() self.coordinates_to_vhid[coordinates] = id_num assert len(self.coordinates_to_vhid.keys()) == len( set(self.coordinates_to_vhid.keys())) assert len(self.coordinates_to_vhid.values()) == len( set(self.coordinates_to_vhid.values())) # end def def partVirtualHelixRemovingSlot(self, sender, id_num, virtual_helix, neighbors): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. neighbors (TYPE): Description Args: TYPE: Description """ tm = self._viewroot.manager tm.resetTools() self.removeVirtualHelixItem(id_num) for neighbor_id in neighbors: nvhi = self._virtual_helix_item_hash[neighbor_id] self._refreshVirtualHelixItemGizmos(neighbor_id, nvhi) for coordinates, current_id in self.coordinates_to_vhid.items(): if current_id == id_num: del self.coordinates_to_vhid[coordinates] break assert id_num not in self.coordinates_to_vhid.values() assert len(self.coordinates_to_vhid.keys()) == len( set(self.coordinates_to_vhid.keys())) assert len(self.coordinates_to_vhid.values()) == len( set(self.coordinates_to_vhid.values())) # end def def partSelectedChangedSlot(self, model_part, is_selected): """Set this Z to front, and return other Zs to default. Args: model_part (Part): The model part is_selected (TYPE): Description """ if is_selected: # self._drag_handle.resetAppearance(_SELECTED_COLOR, _SELECTED_WIDTH, _SELECTED_ALPHA) self.setZValue(styles.ZPARTITEM + 1) else: # self._drag_handle.resetAppearance(self.modelColor(), _DEFAULT_WIDTH, _DEFAULT_ALPHA) self.setZValue(styles.ZPARTITEM) # end def def partVirtualHelicesSelectedSlot(self, sender, vh_set, is_adding): """is_adding (bool): adding (True) virtual helices to a selection or removing (False) Args: sender (obj): Model object that emitted the signal. vh_set (TYPE): Description is_adding (TYPE): Description """ select_tool = self._viewroot.select_tool if is_adding: select_tool.selection_set.update(vh_set) select_tool.setPartItem(self) select_tool.getSelectionBoundingRect() else: select_tool.deselectSet(vh_set) # end def def partDocumentSettingChangedSlot(self, part, key, value): """Summary Args: part (TYPE): Description key (TYPE): Description value (TYPE): Description Args: TYPE: Description Raises: ValueError: Description """ if key == 'grid': if value == 'lines and points': self.griditem.setDrawlines(True) elif value == 'points': self.griditem.setDrawlines(False) elif value == 'circles': pass # self.griditem.setGridAppearance(False) else: raise ValueError("unknown grid styling") ### ACCESSORS ### def boundingRect(self): """Summary Args: TYPE: Description """ return self._rect # end def def modelColor(self): """Summary Args: TYPE: Description """ return self._model_props['color'] # end def def window(self): """Summary Args: TYPE: Description """ return self.parentItem().window() # end def def setActiveVirtualHelixItem(self, new_active_vhi): """Summary Args: new_active_vhi (TYPE): Description """ current_vhi = self.active_virtual_helix_item if new_active_vhi != current_vhi: if current_vhi is not None: current_vhi.deactivate() if new_active_vhi is not None: new_active_vhi.activate() self.active_virtual_helix_item = new_active_vhi # end def def setPreXoverItemsVisible(self, virtual_helix_item): """ self._pre_xover_items list references prexovers parented to other PathHelices such that only the activeHelix maintains the list of visible prexovers Args: virtual_helix_item (cadnano.views.sliceview.virtualhelixitem.SliceVirtualHelixItem): Description """ vhi = virtual_helix_item pxom = self.prexover_manager if vhi is None: pxom.hideGroups() return part = self.part() info = part.active_base_info if info: id_num, is_fwd, idx, to_vh_id_num = info per_neighbor_hits, pairs = part.potentialCrossoverMap(id_num, idx) pxom.activateVirtualHelix(virtual_helix_item, idx, per_neighbor_hits, pairs) # end def def removeVirtualHelixItem(self, id_num): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Args: TYPE: Description """ vhi = self._virtual_helix_item_hash[id_num] if vhi == self.active_virtual_helix_item: self.active_virtual_helix_item = None vhi.virtualHelixRemovedSlot() del self._virtual_helix_item_hash[id_num] # When any VH is removed, turn SPA mode off self.shortest_path_add_mode = False self.shortest_path_start = None # end def def reconfigureRect(self, top_left, bottom_right, finish=False, padding=80): """Reconfigures the rectangle that is the document. Args: top_left (tuple): A tuple corresponding to the x-y coordinates of top left corner of the document bottom_right (tuple): A tuple corresponding to the x-y coordinates of the bottom left corner of the document Returns: tuple: tuple of point tuples representing the top_left and bottom_right as reconfigured with padding """ rect = self._rect ptTL = QPointF( *self.padTL(padding, *top_left)) if top_left else rect.topLeft() ptBR = QPointF(*self.padBR( padding, *bottom_right)) if bottom_right else rect.bottomRight() self._rect = QRectF(ptTL, ptBR) self.setRect(self._rect) self._configureOutline(self.outline) self.griditem.updateGrid() return self.outline.rect() # end def def padTL(self, padding, xTL, yTL): return xTL + padding, yTL + padding # end def def padBR(self, padding, xBR, yBR): return xBR - padding, yBR - padding # end def def enlargeRectToFit(self): """Enlarges Part Rectangle to fit the model bounds. This should be called when adding a SliceVirtualHelixItem. This method enlarges the rectangle to ensure that it fits the design. This method needs to check the model size to do this, but also takes into account any expansions the user has made to the rectangle as to not shrink the rectangle after the user has expanded it. :rtype: None """ padding = self._BOUNDING_RECT_PADDING model_left, model_top, model_right, model_bottom = self.getModelMinBounds( ) rect_left, rect_right, rect_bottom, rect_top = self.bounds() xTL = min(rect_left, model_left) - padding xBR = max(rect_right, model_right) + padding yTL = min(rect_top, model_top) - padding yBR = max(rect_bottom, model_bottom) + padding new_outline_rect = self.reconfigureRect(top_left=(xTL, yTL), bottom_right=(xBR, yBR)) self.resize_handle_group.alignHandles(new_outline_rect) # self.grab_cornerTL.alignPos(*top_left) # self.grab_cornerBR.alignPos(*bottom_right) ### PRIVATE SUPPORT METHODS ### def _configureOutline(self, outline): """Adjusts `outline` size with default padding. Args: outline (TYPE): Description Returns: o_rect (QRect): `outline` rect adjusted by _BOUNDING_RECT_PADDING """ _p = self._BOUNDING_RECT_PADDING o_rect = self.rect().adjusted(-_p, -_p, _p, _p) outline.setRect(o_rect) return o_rect # end def def boundRectToModel(self): """Update the size of the rectangle corresponding to the grid to the size of the model or a minimum size (whichever is greater). :rtype: None """ xTL, yTL, xBR, yBR = self.getModelMinBounds() self._rect = QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR)) # end def def getModelMinBounds(self, handle_type=None): """Bounds in form of Qt scaled from model Args: Tuple (top_left, bottom_right) :rtype: Tuple where """ xLL, yLL, xUR, yUR = self.part().boundDimensions(self.scale_factor) # return xLL, -yUR, xUR, -yLL r = self._RADIUS return xLL - r, -yUR - r, xUR + r, -yLL + r # end def def bounds(self): """x_low, x_high, y_low, y_high """ rect = self._rect return (rect.left(), rect.right(), rect.bottom(), rect.top()) ### PUBLIC SUPPORT METHODS ### def setLastHoveredItem(self, gridpoint_item): """Stores the last self-reported griditem to be hovered. Args: griditem (GridItem): the hoveree """ self._last_hovered_item = gridpoint_item def setModifyState(self, bool_val): """Hides the mod_rect when modify state disabled. Args: bool_val (boolean): what the modifystate should be set to. """ self._can_show_mod_circ = bool_val if bool_val is False: self._mod_circ.hide() # end def def showModelMinBoundsHint(self, handle_type, show=True): """Shows QGraphicsRectItem reflecting current model bounds. ResizeHandleGroup should toggle this when resizing. Args: status_str (str): Description to display in status bar. """ m_b_h = self.model_bounds_hint if show: xTL, yTL, xBR, yBR = self.getModelMinBounds() m_b_h.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) m_b_h.show() else: m_b_h.hide() def updateStatusBar(self, status_str): """Shows status_str in the MainWindow's status bar. Args: status_str (str): Description to display in status bar. """ pass # disabled for now. # self.window().statusBar().showMessage(status_str, timeout) # end def def zoomToFit(self): """Ask the view to zoom to fit. """ thescene = self.scene() theview = thescene.views()[0] theview.zoomToFit() # end def ### EVENT HANDLERS ### def mousePressEvent(self, event): """Handler for user mouse press. Args: event (QGraphicsSceneMouseEvent): Contains item, scene, and screen coordinates of the the event, and previous event. Args: event (QMouseEvent): contains parameters that describe a mouse event. """ if event.button() == Qt.RightButton: return part = self._model_part part.setSelected(True) if self.isMovable(): return QGraphicsItem.mousePressEvent(self, event) tool = self._getActiveTool() if tool.FILTER_NAME not in part.document().filter_set: return tool_method_name = tool.methodPrefix() + "MousePress" if tool_method_name == 'createToolMousePress': return elif hasattr(self, tool_method_name): getattr(self, tool_method_name)(tool, event) else: event.setaccepted(False) QGraphicsItem.mousePressEvent(self, event) # end def def hoverMoveEvent(self, event): self.last_mouse_position = self.translateEventCoordinates(event) tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "HoverMove" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(tool, event) else: event.setAccepted(False) QGraphicsItem.hoverMoveEvent(self, event) # def hoverLeaveEvent(self, event): # pass # tool = self._getActiveTool() # tool.hideLineItem() def getModelPos(self, pos): """Y-axis is inverted in Qt +y === DOWN Args: pos (TYPE): Description """ sf = self.scale_factor x, y = pos.x() / sf, -1.0 * pos.y() / sf return x, y # end def def getVirtualHelixItem(self, id_num): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Returns: TYPE: Description """ return self._virtual_helix_item_hash.get(id_num) # end def def keyPressEvent(self, event): is_alt = bool(event.modifiers() & Qt.AltModifier) if event.key() == Qt.Key_Escape: self._setShortestPathStart(None) self.removeAllCreateHints() if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): self.highlightOneGridPoint(self.getLastHoveredCoordinates()) elif is_alt and self.shortest_path_add_mode is True: if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): x, y = self.coordinates_to_xy.get( self.getLastHoveredCoordinates()) self._preview_spa((x, y)) elif is_alt: if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): self.highlightOneGridPoint(self.getLastHoveredCoordinates(), styles.SPA_START_HINT_COLOR) # end def def keyReleaseEvent(self, event): is_alt = bool(event.modifiers() & Qt.AltModifier) if not is_alt: self.removeAllCreateHints() if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): self.highlightOneGridPoint(self.getLastHoveredCoordinates()) # end def def createToolMousePress(self, tool, event, alt_event=None): """Creates individual or groups of VHs in Part on user input. Shift modifier enables multi-helix addition. Args: event (TYPE): Description alt_event (None, optional): Description """ # Abort if a VH already exists here position = self.translateEventCoordinates(event) # 1. get point in model coordinates: part = self._model_part if alt_event is None: pt = tool.eventToPosition(self, event) else: pt = alt_event.pos() if pt is None: tool.deactivate() return QGraphicsItem.mousePressEvent(self, event) part_pt_tuple = self.getModelPos(pt) modifiers = event.modifiers() is_spa_mode = modifiers == Qt.AltModifier last_added_spa_vhi_id = self._handleShortestPathMousePress( tool=tool, position=position, is_spa_mode=is_spa_mode) if last_added_spa_vhi_id is not None: return row, column = self.getLastHoveredCoordinates() parity = self._getCoordinateParity(row, column) part.createVirtualHelix(x=part_pt_tuple[0], y=part_pt_tuple[1], parity=parity) id_num = part.getVirtualHelixAtPoint(part_pt_tuple) vhi = self._virtual_helix_item_hash[id_num] tool.setVirtualHelixItem(vhi) tool.startCreation() if is_spa_mode: self._highlightSpaVH(id_num) # end def def _getCoordinateParity(self, row, column): if self.griditem.grid_type is GridType.HONEYCOMB: return 0 if HoneycombDnaPart.isOddParity(row=row, column=column) else 1 elif self.griditem.grid_type is GridType.SQUARE: return 0 if SquareDnaPart.isEvenParity(row=row, column=column) else 1 else: return None def _handleShortestPathMousePress(self, tool, position, is_spa_mode): """ Handles logic for determining if SPA mode should be activated or continued. Args: tool (): position (tuple): the xy coordinates of the mouse press is_spa_mode (bool): whether or not this event is a SPA event Returns: True if nothing needs to be done by the caller (i.e. this method and its callees added VHs as necessary, False otherwise """ if is_spa_mode: # Complete the path if self.shortest_path_start is not None: last_vhi_id = self.createToolShortestPath( tool=tool, start=self.shortest_path_start, end=position) if last_vhi_id is not None: self._setShortestPathStart(position) self._highlightSpaVH(last_vhi_id) return last_vhi_id # Initialize SPA else: self._setShortestPathStart(position) else: self._setShortestPathStart(None) def _setShortestPathStart(self, position): # TODO[NF]: Docstring if position is not None: self.shortest_path_add_mode = True self.shortest_path_start = position else: self.shortest_path_add_mode = False self.shortest_path_start = None self._highlightSpaVH(None) def _highlightSpaVH(self, vh_id): # TODO[NF]: Docstring if self.spa_start_vhi: self.spa_start_vhi.setBrush(getNoBrush()) if vh_id is None: self.spa_start_vhi = None else: self.spa_start_vhi = self._virtual_helix_item_hash[vh_id] self.spa_start_vhi.setBrush( getBrushObj(styles.SPA_START_HINT_COLOR, alpha=32)) # end def def createToolShortestPath(self, tool, start, end): """ Handle the creation of VHIs for SPA mode. Args: tool (): start (tuple): the x-y coordinates of the start point end (tuple): the x-y coordinates of the end point Returns: The ID of the last VHI created """ path = ShortestPathHelper.shortestPathXY( start=start, end=end, neighbor_map=self.neighbor_map, vh_set=self.coordinates_to_vhid.keys(), point_map=self.coordinates_to_xy, grid_type=self.griditem.grid_type, scale_factor=self.inverse_scale_factor, radius=self._RADIUS) # Abort and exit SPA if there is no path from start to end if path == []: self.shortest_path_start = None self.shortest_path_add_mode = False return None else: x_list, y_list, parity_list = zip(*path) id_numbers = self._model_part.batchCreateVirtualHelices( x_list=x_list, y_list=y_list, parity=parity_list) for id_number in id_numbers: vhi = self._virtual_helix_item_hash[id_number] tool.setVirtualHelixItem(vhi) tool.startCreation() return id_number # end def def createToolHoverMove(self, tool, event): """Summary Args: tool (TYPE): Description event (TYPE): Description Returns: TYPE: Description """ event_xy = self.translateEventCoordinates(event) event_coord = ShortestPathHelper.findClosestPoint( event_xy, self.coordinates_to_xy) is_alt = True if event.modifiers() & Qt.AltModifier else False self.last_mouse_position = event_xy # Un-highlight GridItems if necessary by calling createToolHoverLeave if len(self._highlighted_path) > 1 or ( self._highlighted_path and self._highlighted_path[0] != event_coord): self.createToolHoverLeave(tool=tool, event=event) # Highlight GridItems if alt is being held down if is_alt and self.shortest_path_add_mode and self._inPointItem( event_xy, event_coord): self._preview_spa(event_xy) else: point_item = self.coordinates_to_xy.get(event_coord) if point_item is not None and self._inPointItem( event_xy, event_coord) and is_alt: self.highlightOneGridPoint(self.getLastHoveredCoordinates(), styles.SPA_START_HINT_COLOR) elif point_item is not None and self._inPointItem( event_xy, event_coord) and not is_alt: part = self._model_part next_idnums = (part._getNewIdNum(0), part._getNewIdNum(1)) self.griditem.showCreateHint(event_coord, next_idnums=next_idnums) self._highlighted_path.append(event_coord) tool.hoverMoveEvent(self, event) return QGraphicsItem.hoverMoveEvent(self, event) # end def def _inPointItem(self, event_xy, event_coord): """ Determine if x-y coordinates are inside the given GridPoint. Args: event_xy (tuple): the x-y coordinates corresponding to the position of the mouse event_coord (tuple): the i-j coordinates corresponding to the location of the GridPoint Returns: True if the mouse is in the given GridPoint, False otherwise """ if event_xy is None or event_coord is None: return False point_x, point_y = self.coordinates_to_xy.get(event_coord) event_x, event_y = event_xy return (point_x - event_x)**2 + (point_y - event_y)**2 <= self._RADIUS**2 # end def def _preview_spa(self, event_xy): """ Highlight and add VH ID numbers to the GridPoints that the SPA would use. Args: event_xy (tuple): the x-y coordinates corresponding to the position of the mouse Returns: None """ part = self._model_part start_coord = self.shortest_path_start end_coord = event_xy self._highlighted_path = ShortestPathHelper.shortestPathAStar( start=start_coord, end=end_coord, neighbor_map=self.neighbor_map, vh_set=self.coordinates_to_vhid.keys(), point_map=self.coordinates_to_xy) even_id = part._getNewIdNum(0) odd_id = part._getNewIdNum(1) for coord in self._highlighted_path: is_odd = self.griditem.showCreateHint(coord, next_idnums=(even_id, odd_id)) if is_odd is True: odd_id += 2 elif is_odd is False: even_id += 2 # end def def createToolHoverLeave(self, tool, event): self.removeAllCreateHints() def selectToolMousePress(self, tool, event): """ Args: tool (TYPE): Description event (TYPE): Description """ tool.setPartItem(self) pt = tool.eventToPosition(self, event) part_pt_tuple = self.getModelPos(pt) part = self._model_part if part.isVirtualHelixNearPoint(part_pt_tuple): id_num = part.getVirtualHelixAtPoint(part_pt_tuple) if id_num is not None: pass # loc = part.getCoordinate(id_num, 0) # print("VirtualHelix #{} at ({:.3f}, {:.3f})".format(id_num, loc[0], loc[1])) else: # tool.deselectItems() tool.modelClear() else: # tool.deselectItems() tool.modelClear() return QGraphicsItem.mousePressEvent(self, event) # end def def setNeighborMap(self, neighbor_map): """ Update the internal mapping of coordinates to their neighbors. Args: neighbor_map (dict): the new mapping of coordinates to their neighbors Returns: None """ assert isinstance(neighbor_map, dict) self.neighbor_map = neighbor_map # end def def setPointMap(self, point_map): """ Update the internal mapping of coordinates to x-y positions. Args: point_map (dict): the new mapping of coordinates to their x-y position Returns: None """ assert isinstance(point_map, dict) self.coordinates_to_xy = point_map # end def def updateTranslatedOffsets(self, delta_x, delta_y): """ Update the values used to calculate translational offsets. Args: delta_x (float): the new value for which we've translated in the x direction delta_y (float): the new value for which we've translated in the y direction Returns: None """ assert isinstance(delta_x, float) assert isinstance(delta_y, float) self._translated_x = delta_x self._translated_y = delta_y # end def def translateEventCoordinates(self, event): """ Given an event, return the x-y coordinates of the event accounting for any translations that may have happened Args: event (MousePressEvent): the event for which x-y coordinates should be returned Returns: A tuple of x-y coordinates of the event """ assert isinstance(event, QGraphicsSceneEvent) return event.scenePos().x() - self._translated_x, event.scenePos().y( ) - self._translated_y # end def def removeAllCreateHints(self): """ Remove the create hints from each currently hinted GridItem. Iterates over all coordinates in self._highlighted_path. Returns: None """ for coord in self._highlighted_path: self.griditem.showCreateHint(coord, show_hint=False) self._highlighted_path = [] # end def def highlightOneGridPoint(self, coordinates, color=None): """ Add a hint to one GridPoint. Args: coordinates (tuple): the row-column coordinates of the gridPoint to be highlighted color (): the color that the gridPoint should be changed to Returns: None """ if coordinates is None: return assert isinstance(coordinates, tuple) and len(coordinates) is 2 assert isinstance(coordinates[0], int) and isinstance( coordinates[1], int) if self.coordinates_to_xy.get(coordinates) is not None: part = self._model_part next_idnums = (part._getNewIdNum(0), part._getNewIdNum(1)) self.griditem.showCreateHint(coordinates, next_idnums=next_idnums, color=color) self._highlighted_path.append(coordinates) # end def def getLastHoveredCoordinates(self): """ Get the row and column corresponding to the GridPoint that was most recently hovered over. This accounts for the fact that the rows are inverted (i.e. the result returned by this method will match the coordinate system stored in this class' internal records of coordinates) Returns: A tuple corresponding to the row and column of the most recently hovered GridPoint. """ if self._last_hovered_item: row, column = self._last_hovered_item.coord() return -row, column
class ASCGraphicsView(QGraphicsView): scale_signal = pyqtSignal('float', 'float') set_focus_point_signal = pyqtSignal('int', 'int', 'int') set_focus_point_percent_signal = pyqtSignal('float', 'float', 'float') move_focus_point_signal = pyqtSignal('int', 'int', 'int') # x, y, z, BRUSH_TYPE, BRUSH_SIZE, ERASE paint_anno_on_point_signal = pyqtSignal('int', 'int', 'int', 'int', 'int', 'bool', 'bool') def __init__(self, parent=None): super(ASCGraphicsView, self).__init__(parent) self.scene = QGraphicsScene(self) self.raw_img_item = QGraphicsPixmapItem() self.raw_img_item.setZValue(0) self.anno_img_item = QGraphicsPixmapItem() self.anno_img_item.setZValue(1) self.cross_bar_v_line_item = QGraphicsLineItem() self.cross_bar_h_line_item = QGraphicsLineItem() self.cross_bar_v_line_item.setZValue(100) self.cross_bar_h_line_item.setZValue(100) self.cross_bar_v_line_item.setPen( QPen(Qt.blue, 0, Qt.DotLine, Qt.FlatCap, Qt.RoundJoin)) self.cross_bar_h_line_item.setPen( QPen(Qt.blue, 0, Qt.DotLine, Qt.FlatCap, Qt.RoundJoin)) self.paint_brush_circle_item = QGraphicsEllipseItem() self.paint_brush_rect_item = QGraphicsPolygonItem() self.paint_brush_circle_item.setZValue(10) self.paint_brush_rect_item.setZValue(11) self.paint_brush_circle_item.setVisible(False) self.paint_brush_rect_item.setVisible(False) self.paint_brush_circle_item.setPen( QPen(Qt.red, 0, Qt.DotLine, Qt.FlatCap, Qt.RoundJoin)) self.paint_brush_rect_item.setPen( QPen(Qt.red, 0, Qt.DotLine, Qt.FlatCap, Qt.RoundJoin)) self.scene.addItem(self.raw_img_item) self.scene.addItem(self.anno_img_item) self.scene.addItem(self.cross_bar_v_line_item) self.scene.addItem(self.cross_bar_h_line_item) self.scene.addItem(self.paint_brush_circle_item) self.scene.addItem(self.paint_brush_rect_item) self.setScene(self.scene) self.setViewport(QOpenGLWidget()) self._last_button_press = Qt.NoButton self._last_pos_middle_button = None self._last_pos_right_button = None self._brush_stats = {'type': BRUSH_TYPE_NO_BRUSH, 'size': 5} self.setResizeAnchor(QGraphicsView.AnchorViewCenter) self.slice_scroll_bar = None self.image_size = None self.is_valid = False def clear(self): """before loading new image""" self._last_pos_middle_button = None self._last_pos_right_button = None self._last_button_press = Qt.NoButton self.raw_img_item.setPixmap(QPixmap()) self.anno_img_item.setPixmap(QPixmap()) self.paint_brush_circle_item.setVisible(False) self.paint_brush_rect_item.setVisible(False) self.image_size = None self.is_valid = False def init_view(self, image_size): """after loading new image""" self.is_valid = True self.image_size = image_size self.slice_scroll_bar = self.parent().findChild( QScrollBar, self.objectName()[0] + 'SliceScrollBar') trans_mat = item2scene_transform[self.objectName()[0]] self.raw_img_item.setTransform(trans_mat) self.anno_img_item.setTransform(trans_mat) self.cross_bar_v_line_item.setTransform(trans_mat) self.cross_bar_h_line_item.setTransform(trans_mat) self.paint_brush_rect_item.setTransform(trans_mat) self.paint_brush_circle_item.setTransform(trans_mat) self.fitInView(self.raw_img_item, Qt.KeepAspectRatio) self.paint_brush_circle_item.setVisible(False) self.paint_brush_rect_item.setVisible(False) @property def brush_stats(self): return self._brush_stats @brush_stats.setter def brush_stats(self, stats_tuple): b_type, size = stats_tuple if b_type in [ BRUSH_TYPE_NO_BRUSH, BRUSH_TYPE_CIRCLE_BRUSH, BRUSH_TYPE_RECT_BRUSH ]: self._brush_stats['type'] = b_type self._brush_stats['size'] = size if b_type != BRUSH_TYPE_NO_BRUSH: self.setMouseTracking(True) else: self.setMouseTracking(False) def update_brush_preview(self, x, y, out_of_sight=False): if not self.is_valid or self.brush_stats[ 'type'] == BRUSH_TYPE_NO_BRUSH or out_of_sight: self.paint_brush_rect_item.setVisible(False) self.paint_brush_circle_item.setVisible(False) return center = self.anno_img_item.mapFromScene(self.mapToScene(x, y)) start_x = self.raw_img_item.boundingRect().topLeft().x() start_y = self.raw_img_item.boundingRect().topLeft().y() end_x = self.raw_img_item.boundingRect().bottomRight().x() end_y = self.raw_img_item.boundingRect().bottomRight().y() center.setX(min(max(start_x, center.x()), end_x) + 0.5) center.setY(min(max(start_y, center.y()), end_y) + 0.5) top_left_x = int(center.x() - self.brush_stats['size'] / 2) top_left_y = int(center.y() - self.brush_stats['size'] / 2) rect = QRectF(top_left_x, top_left_y, self.brush_stats['size'], self.brush_stats['size']) if self.brush_stats['type'] == BRUSH_TYPE_CIRCLE_BRUSH: self.paint_brush_rect_item.setVisible(False) self.paint_brush_circle_item.setVisible(True) self.paint_brush_circle_item.setRect(rect) if self.brush_stats['type'] == BRUSH_TYPE_RECT_BRUSH: self.paint_brush_rect_item.setVisible(True) self.paint_brush_circle_item.setVisible(False) self.paint_brush_rect_item.setPolygon(QPolygonF(rect)) def anno_paint(self, x, y, erase=False, new_step=False): pos_on_item = self.raw_img_item.mapFromScene(self.mapToScene(x, y)) if self.objectName() == 'aGraphicsView': paint_point = [pos_on_item.y(), pos_on_item.x(), 999999] if self.objectName() == 'sGraphicsView': paint_point = [999999, pos_on_item.y(), pos_on_item.x()] if self.objectName() == 'cGraphicsView': paint_point = [pos_on_item.y(), 999999, pos_on_item.x()] self.paint_anno_on_point_signal.emit(math.floor(paint_point[0]), math.floor(paint_point[1]), math.floor(paint_point[2]), self.brush_stats['type'], self.brush_stats['size'], erase, new_step) @pyqtSlot('int') def on_slice_scroll_bar_changed(self, value): if not self.is_valid: return ratios = [-1, -1, -1] ratio = (value - self.slice_scroll_bar.minimum()) / \ (self.slice_scroll_bar.maximum() - self.slice_scroll_bar.minimum()) if self.objectName() == 'aGraphicsView': ratios[2] = ratio if self.objectName() == 'sGraphicsView': ratios[0] = ratio if self.objectName() == 'cGraphicsView': ratios[1] = ratio self.set_focus_point_percent_signal.emit(ratios[0], ratios[1], ratios[2]) @pyqtSlot('int', 'int') def set_brush_stats(self, b_type, size): self.brush_stats = [b_type, size] @pyqtSlot('int', 'int', 'int') def set_cross_bar(self, x, y, z): if self.objectName() == 'aGraphicsView': cross_bar_x = y cross_bar_y = x slice_bar_ratio = z / self.image_size[2] if self.objectName() == 'sGraphicsView': cross_bar_x = z cross_bar_y = y slice_bar_ratio = x / self.image_size[0] if self.objectName() == 'cGraphicsView': cross_bar_x = z cross_bar_y = x slice_bar_ratio = y / self.image_size[1] # cross line in voxel center cross_bar_x = cross_bar_x + 0.5 cross_bar_y = cross_bar_y + 0.5 start_x = self.raw_img_item.boundingRect().topLeft().x() start_y = self.raw_img_item.boundingRect().topLeft().y() end_x = self.raw_img_item.boundingRect().bottomRight().x() end_y = self.raw_img_item.boundingRect().bottomRight().y() self.cross_bar_v_line_item.setLine(cross_bar_x, start_y, cross_bar_x, end_y) self.cross_bar_h_line_item.setLine(start_x, cross_bar_y, end_x, cross_bar_y) slice_bar_value = round(slice_bar_ratio * (self.slice_scroll_bar.maximum() - self.slice_scroll_bar.minimum())) \ + self.slice_scroll_bar.minimum() self.slice_scroll_bar.setValue(slice_bar_value) def mousePressEvent(self, event: QtGui.QMouseEvent): self._last_button_press = event.button() if self.brush_stats['type'] == BRUSH_TYPE_NO_BRUSH: if event.button() == Qt.LeftButton: item_coord_pos = self.raw_img_item.mapFromScene( self.mapToScene(event.pos())) if self.objectName() == 'aGraphicsView': new_focus_point = [ item_coord_pos.y(), item_coord_pos.x(), 999999 ] if self.objectName() == 'sGraphicsView': new_focus_point = [ 999999, item_coord_pos.y(), item_coord_pos.x() ] if self.objectName() == 'cGraphicsView': new_focus_point = [ item_coord_pos.y(), 999999, item_coord_pos.x() ] self.set_focus_point_signal.emit( math.floor(new_focus_point[0]), math.floor(new_focus_point[1]), math.floor(new_focus_point[2])) elif event.button() == Qt.MiddleButton: self._last_pos_middle_button = event.pos() self.setCursor(Qt.ClosedHandCursor) elif event.button() == Qt.RightButton: self._last_pos_right_button = event.pos() else: super(ASCGraphicsView, self).mousePressEvent(event) if self.brush_stats['type'] in [ BRUSH_TYPE_CIRCLE_BRUSH, BRUSH_TYPE_RECT_BRUSH ]: if event.button() == Qt.LeftButton: self.anno_paint(event.x(), event.y(), erase=False, new_step=True) elif event.button() == Qt.MiddleButton: self._last_pos_middle_button = event.pos() self.setCursor(Qt.ClosedHandCursor) elif event.button() == Qt.RightButton: self.anno_paint(event.x(), event.y(), erase=True, new_step=True) def mouseMoveEvent(self, event: QtGui.QMouseEvent): if self.brush_stats['type'] == BRUSH_TYPE_NO_BRUSH: if self._last_button_press == Qt.LeftButton: item_coord_pos = self.raw_img_item.mapFromScene( self.mapToScene(event.pos())) if self.objectName() == 'aGraphicsView': new_focus_point = [ item_coord_pos.y(), item_coord_pos.x(), 999999 ] if self.objectName() == 'sGraphicsView': new_focus_point = [ 999999, item_coord_pos.y(), item_coord_pos.x() ] if self.objectName() == 'cGraphicsView': new_focus_point = [ item_coord_pos.y(), 999999, item_coord_pos.x() ] self.set_focus_point_signal.emit( math.floor(new_focus_point[0]), math.floor(new_focus_point[1]), math.floor(new_focus_point[2])) elif self._last_button_press == Qt.MiddleButton: delta_x = event.x() - self._last_pos_middle_button.x() delta_y = event.y() - self._last_pos_middle_button.y() self.horizontalScrollBar().setValue( self.horizontalScrollBar().value() - delta_x) self.verticalScrollBar().setValue( self.verticalScrollBar().value() - delta_y) self._last_pos_middle_button = event.pos() elif self._last_button_press == Qt.RightButton: delta = event.pos().y() - self._last_pos_right_button.y() scale = 1 - float(delta) / float(self.size().height()) self.scale_signal.emit(scale, scale) self._last_pos_right_button = event.pos() else: super(ASCGraphicsView, self).mouseMoveEvent(event) if self.brush_stats['type'] in [ BRUSH_TYPE_CIRCLE_BRUSH, BRUSH_TYPE_RECT_BRUSH ]: self.update_brush_preview(event.x(), event.y()) if self._last_button_press == Qt.LeftButton: self.anno_paint(event.x(), event.y(), erase=False, new_step=False) elif self._last_button_press == Qt.MiddleButton: delta_x = event.x() - self._last_pos_middle_button.x() delta_y = event.y() - self._last_pos_middle_button.y() self.horizontalScrollBar().setValue( self.horizontalScrollBar().value() - delta_x) self.verticalScrollBar().setValue( self.verticalScrollBar().value() - delta_y) self._last_pos_middle_button = event.pos() elif self._last_button_press == Qt.RightButton: self.anno_paint(event.x(), event.y(), erase=True, new_step=False) def mouseReleaseEvent(self, event: QtGui.QMouseEvent): self._last_button_press = Qt.NoButton if self.brush_stats['type'] == BRUSH_TYPE_NO_BRUSH: if event.button() == Qt.MiddleButton: self.setCursor(Qt.ArrowCursor) if self.brush_stats['type'] in [ BRUSH_TYPE_CIRCLE_BRUSH, BRUSH_TYPE_RECT_BRUSH ]: if event.button() == Qt.LeftButton: pass elif event.button() == Qt.RightButton: pass else: super(ASCGraphicsView, self).mouseReleaseEvent(event) def wheelEvent(self, event: QtGui.QWheelEvent): # super(ASCGraphicsView, self).wheelEvent(event) if self.objectName() == 'aGraphicsView': if event.angleDelta().y() > 0: self.move_focus_point_signal.emit(0, 0, -1) elif event.angleDelta().y() < 0: self.move_focus_point_signal.emit(0, 0, 1) if self.objectName() == 'sGraphicsView': if event.angleDelta().y() > 0: self.move_focus_point_signal.emit(-1, 0, 0) elif event.angleDelta().y() < 0: self.move_focus_point_signal.emit(1, 0, 0) if self.objectName() == 'cGraphicsView': if event.angleDelta().y() > 0: self.move_focus_point_signal.emit(0, -1, 0) elif event.angleDelta().y() < 0: self.move_focus_point_signal.emit(0, 1, 0) def leaveEvent(self, event: QtCore.QEvent): self._last_button_press = Qt.NoButton if self.brush_stats['type'] == BRUSH_TYPE_NO_BRUSH: super(ASCGraphicsView, self).leaveEvent(event) else: self.update_brush_preview(0, 0, out_of_sight=True)
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)
def mainFunc(self, playing, scriptList, scriptPos): if self.inputImg is not None: self.outputImg = self.inputImg pass_ = 0 fx = 1.0 fy = 1.0 offset_x = 0.0 offset_y = 0.0 for i in range(scriptPos): widget = scriptList[i][1] if widget.objectName() == 'resize': fx *= widget.dblFx.value() fy *= widget.dblFx.value() pass_ += widget.pass_ for item in widget.graphicsItems: try: # ~ print('item.rect() ', item.rect()) if fx != 1.0: item.setScale(1 / fx) # ~ if fy != 1.0: # ~ item.setHeight(item.height()/fy) if offset_x: item.setX((item.x() + offset_x) / fx) # ~ item.setX( (item.x() + offset_x)/1.0 ) # ~ print('offset_x: ', offset_x, ' fx: ', fx) if offset_y: item.setY((item.y() + offset_y) / fy) # ~ item.setY( (item.y() + offset_y)/1.0 ) self.graphicsView.scene().addItem(item) item.setZValue(1) except RuntimeError as e: print(time.time(), 'Runtime Error show_graphics_items mainFunc') print(e) if widget.objectName() == 'circles': if widget.chkCrop.isChecked(): try: circle = widget.graphicsItems[0] # ~ circle = item offset_x = circle.x() offset_x = circle.rect().left() offset_y = circle.y() offset_y = circle.rect().top() # ~ print('offer setter ', offset_x) except IndexError as e: print(e) if self.chkShowPass.isChecked(): # ~ h = self.graphicsView.scene().height() # ~ w = self.graphicsView.scene().width() h = self.inputImg.shape[0] w = self.inputImg.shape[1] # ~ sfh = float(h)/float(self.graphicsView.minimumHeight()) # ~ sfw = float(w)/float(self.graphicsView.minimumWidth()) # ~ sf = min(sfh,sfw) outlineRect = QGraphicsRectItem(0, 0, w, h) outlineLine = QGraphicsLineItem(0, h, w, 0) font = QFont("DejaVu Sans", 45) if pass_ == scriptPos: # the overall result is pass text = 'PASS' pen = QPen(QColor(0, 255, 0), 8) #green for pass brush = QBrush(QColor(0, 255, 0)) else: # overall result is fail text = 'FAIL' pen = QPen(QColor(255, 0, 0), 8) #red for fail brush = QBrush(QColor(255, 0, 0)) outlineLine.setPen(pen) self.graphicsView.scene().addItem(outlineLine) outlineLine.setZValue(1) outlineRect.setPen(pen) self.graphicsView.scene().addItem(outlineRect) textItem = QGraphicsSimpleTextItem(text, ) textItem.setFont(font) textItem.setBrush(brush) textItem.setPos(w / 10.0, h / 10.0) self.graphicsView.scene().addItem(textItem) textItem.setZValue(1)
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()