class TreeVisualizer(QWidget): def __init__(self): """Initial configuration.""" super().__init__() # GLOBAL VARIABLES # graph variables self.graph: Graph = Graph() self.selected_node: Node = None self.selected_vertex: Tuple[Node, Node] = None # offset of the mouse from the position of the currently dragged node self.mouse_drag_offset: Vector = None # position of the mouse; is updated when the mouse moves self.mouse_position: Vector = Vector(-1, -1) # variables for visualizing the graph self.node_radius: float = 20 self.weight_rectangle_size: float = self.node_radius / 3 self.arrowhead_size: float = 8 self.arrow_separation: float = pi / 7 self.selected_color = Qt.red self.regular_node_color = Qt.white self.regular_vertex_weight_color = Qt.black # limit the displayed length of labels for each node self.node_label_limit: int = 10 # UI variables self.font_family: str = "Times New Roman" self.font_size: int = 18 self.layout_margins: float = 8 self.layout_item_spacing: float = 2 * self.layout_margins # canvas positioning (scale and translation) self.scale: float = 1 self.scale_coefficient: float = 2 # by how much the scale changes on scroll self.translation: float = Vector(0, 0) # by how much the rotation of the nodes changes self.node_rotation_coefficient: float = 0.7 # TIMERS # timer that runs the simulation (60 times a second... once every ~= 16ms) self.simulation_timer = QTimer( interval=16, timeout=self.perform_simulation_iteration) # WIDGETS self.canvas = QFrame(self, minimumSize=QSize(0, 400)) self.canvas_size: Vector = None self.canvas.resizeEvent = self.adjust_canvas_translation # toggles between directed/undirected graphs self.directed_toggle_button = QPushButton( text="undirected", clicked=self.toggle_directed_graph) # for showing the labels of the nodes self.labels_checkbox = QCheckBox(text="labels") # sets, whether the graph is weighted or not self.weighted_checkbox = QCheckBox(text="weighted", clicked=self.set_weighted_graph) # enables/disables forces (True by default - they're fun!) # self.forces_checkbox = QCheckBox(text="forces", checked=False) # input of the labels and vertex weights self.input_line_edit = QLineEdit( enabled=self.labels_checkbox.isChecked(), textChanged=self.input_line_edit_changed, ) # displays information about the app self.about_button = QPushButton( text="?", clicked=self.show_help, sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed), ) # for creating complements of the graph self.complement_button = QPushButton( text="complement", clicked=self.graph.complement, ) # imports/exports the current graph self.import_graph_button = QPushButton(text="import", clicked=self.import_graph) self.export_graph_button = QPushButton(text="export", clicked=self.export_graph) # WIDGET LAYOUT self.main_v_layout = QVBoxLayout(self, margin=0) self.main_v_layout.addWidget(self.canvas) self.option_h_layout = QHBoxLayout(self, margin=self.layout_margins) self.option_h_layout.addWidget(self.directed_toggle_button) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.weighted_checkbox) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.labels_checkbox) self.option_h_layout.addSpacing(self.layout_item_spacing) # self.option_h_layout.addWidget(self.forces_checkbox) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.input_line_edit) self.bottom_h_layout = QHBoxLayout(self, margin=self.layout_margins) self.bottom_h_layout.addWidget(self.complement_button) self.bottom_h_layout.addSpacing(self.layout_item_spacing) self.bottom_h_layout.addWidget(self.import_graph_button) self.bottom_h_layout.addSpacing(self.layout_item_spacing) self.bottom_h_layout.addWidget(self.export_graph_button) self.bottom_h_layout.addSpacing(self.layout_item_spacing) self.bottom_h_layout.addWidget(self.about_button) self.main_v_layout.addLayout(self.option_h_layout) self.main_v_layout.addSpacing(-self.layout_margins) self.main_v_layout.addLayout(self.bottom_h_layout) self.setLayout(self.main_v_layout) # WINDOW SETTINGS self.setWindowTitle("Graph Visualizer") self.setFont(QFont(self.font_family, self.font_size)) self.setWindowIcon(QIcon("icon.ico")) self.show() # start the simulation self.simulation_timer.start() def set_weighted_graph(self): """Is called when the weighted checkbox is pressed; sets, whether the graph is weighted or not.""" self.graph.set_weighted(self.weighted_checkbox.isChecked()) def adjust_canvas_translation(self, event): """Is called when the canvas widget is resized; changes translation so the center stays in the center.""" size = Vector(event.size().width(), event.size().height()) if self.canvas_size is not None: self.translation += self.scale * (size - self.canvas_size) / 2 self.canvas_size = size def repulsion_force(self, distance: float) -> float: """Calculates the strength of the repulsion force at the specified distance.""" # return 1 / distance * 10 if self.forces_checkbox.isChecked() else 0 return 0 def attraction_force(self, distance: float, leash_length=80) -> float: """Calculates the strength of the attraction force at the specified distance and leash length.""" # return ( # -(distance - leash_length) / 10 if self.forces_checkbox.isChecked() else 0 # ) return 0 def import_graph(self): """Is called when the import button is clicked; imports a graph from a file.""" path = QFileDialog.getOpenFileName()[0] if path != "": try: with open(path, "r") as file: # a list of vertices of the graph data = [line.strip() for line in file.read().splitlines()] # set the properties of the graph by its first vertex sample = data[0].split(" ") directed = True if sample[1] in ["->", "<-", "<>" ] else False weighted = (False if len(sample) == 2 or directed and len(sample) == 3 else True) graph = Graph(directed=directed, weighted=weighted) node_dictionary = {} # add each of the nodes of the vertex to the graph for vertex in data: vertex_components = vertex.split(" ") # the formats are either 'A B' or 'A <something> B' nodes = [ vertex_components[0], vertex_components[2] if directed else vertex_components[1], ] # if weights are present, the formats are: # - 'A B num' for undirected graphs # - 'A <something> B num (num)' for directed graphs weights_strings = (None if not weighted else [ vertex_components[2] if not directed else vertex_components[3], None if not directed or vertex_components[1] != "<>" else vertex_components[4], ]) for node in nodes: if node not in node_dictionary: # slightly randomize the coordinates, so the graph # doesn't stay in one place x = self.canvas.width() / 2 + (random() - 0.5) y = self.canvas.height() / 2 + (random() - 0.5) # add it to graph with default values node_dictionary[node] = graph.add_node( Vector(x, y), self.node_radius, node) # get the node objects from the names n1, n2 = node_dictionary[nodes[0]], node_dictionary[ nodes[1]] graph.add_vertex( n2 if vertex_components[1] == "<-" else n1, n1 if vertex_components[1] == "<-" else n2, 0 if not weighted else ast.literal_eval( weights_strings[0]), ) # possibly add the other way if vertex_components[1] == "<>": graph.add_vertex( n2, n1, 0 if not weighted else ast.literal_eval( weights_strings[1]), ) # if everything was successful, override the current graph self.graph = graph except UnicodeDecodeError: QMessageBox.critical(self, "Error!", "Can't read binary files!") except ValueError: QMessageBox.critical( self, "Error!", "The weights of the graph are not numbers!") except Exception: QMessageBox.critical( self, "Error!", "An error occurred when importing the graph. Make sure that the " + "file is in the correct format and that it isn't currently being " + "used!", ) # make sure that the UI is in order self.deselect_node() self.deselect_vertex() self.set_checkbox_values() def set_checkbox_values(self): """Sets the values of the checkboxes from the graph.""" self.weighted_checkbox.setChecked(self.graph.is_weighted()) self.update_directed_toggle_button_text() def export_graph(self): """Is called when the export button is clicked; exports a graph to a file.""" path = QFileDialog.getSaveFileName()[0] if path != "": try: with open(path, "w") as file: # look at every pair of nodes and examine the vertices for i, n1 in enumerate(self.graph.get_nodes()): for j, n2 in enumerate(self.graph.get_nodes()[i + 1:]): # information about vertices and weights v1_exists = self.graph.does_vertex_exist(n1, n2) v2_exists = self.graph.does_vertex_exist(n2, n1) if not v1_exists and v2_exists: continue w1_value = self.graph.get_weight(n1, n2) w1 = ("" if not self.graph.is_weighted() or w1_value is None else str(w1_value)) # undirected graphs if not self.graph.is_directed() and v1_exists: file.write( f"{n1.get_label()} {n2.get_label()} {w1}\n" ) else: w2_value = self.graph.get_weight(n2, n1) w2 = ("" if not self.graph.is_weighted() or w2_value is None else str(w2_value)) symbol = ("<>" if v1_exists and v2_exists else "->" if v1_exists else "<-") vertex = f"{n1.get_label()} {symbol} {n2.get_label()}" if w1 != "": vertex += f" {w1}" if w2 != "": vertex += f" {w2}" file.write(vertex + "\n") except Exception: QMessageBox.critical( self, "Error!", "An error occurred when exporting the graph. Make sure that you " "have permission to write to the specified file and try again!", ) def show_help(self): """Is called when the help button is clicked; displays basic information about the application.""" message = """ <p>Welcome to <strong>Graph Visualizer</strong>.</p> <p>The app aims to help with creating, visualizing and exporting graphs. It is powered by PyQt5 – a set of Python bindings for the C++ library Qt.</p> <hr /> <p>The controls are as follows:</p> <ul> <li><em>Left Mouse Button</em> – selects nodes and moves them</li> <li><em>Right Mouse Button</em> – creates/removes nodes and vertices</li> <li><em>Mouse Wheel</em> – zooms in/out</li> <li><em>Shift + Left Mouse Button</em> – moves connected nodes</li> <li><em>Shift + Mouse Wheel</em> – rotates nodes around the selected node</li> </ul> <hr /> <p>If you spot an issue, or would like to check out the source code, see the app's <a href="https://github.com/xiaoxiae/GraphVisualizer">GitHub repository</a>.</p> """ QMessageBox.information(self, "About", message) def toggle_directed_graph(self): """Is called when the directed checkbox changes; toggles between directed and undirected graphs.""" self.graph.set_directed(not self.graph.is_directed()) self.update_directed_toggle_button_text() def update_directed_toggle_button_text(self): """Changes the text of the directed toggle button, according to whether the graph is directer or not.""" self.directed_toggle_button.setText( "directed" if self.graph.is_directed() else "undirected") def input_line_edit_changed(self, text: str): """Is called when the input line edit changes; changes either the label of the node selected node, or the value of the selected vertex.""" palette = self.input_line_edit.palette() text = text.strip() if self.selected_node is not None: # text is restricted for rendering and graph export purposes if 0 < len(text) < self.node_label_limit and " " not in text: self.selected_node.set_label(text) palette.setColor(self.input_line_edit.backgroundRole(), Qt.white) else: palette.setColor(self.input_line_edit.backgroundRole(), Qt.red) elif self.selected_vertex is not None: # try to parse the input text either as an integer, or as a float weight = None try: weight = int(text) except ValueError: try: weight = float(text) except ValueError: pass # if the parsing was unsuccessful, set the input line edit background to # red to indicate this if weight is None: palette.setColor(self.input_line_edit.backgroundRole(), Qt.red) else: self.graph.add_vertex(self.selected_vertex[0], self.selected_vertex[1], weight) palette.setColor(self.input_line_edit.backgroundRole(), Qt.white) self.input_line_edit.setPalette(palette) def select_node(self, node: Node): """Sets the selected node to the specified node, sets the input line edit to its label and enables it.""" self.selected_node = node self.input_line_edit.setText(node.get_label()) self.input_line_edit.setEnabled(True) self.input_line_edit.setFocus() def deselect_node(self): """Sets the selected node to None and disables the input line edit.""" self.selected_node = None self.input_line_edit.setEnabled(False) def select_vertex(self, vertex): """Sets the selected vertex to the specified vertex, sets the input line edit to its weight and enables it.""" self.selected_vertex = vertex self.input_line_edit.setText(str(self.graph.get_weight(*vertex))) self.input_line_edit.setEnabled(True) self.input_line_edit.setFocus() def deselect_vertex(self): """Sets the selected vertex to None and disables the input line edit.""" self.selected_vertex = None self.input_line_edit.setEnabled(False) def mousePressEvent(self, event): """Is called when a mouse button is pressed; creates and moves nodes/vertices.""" pos = self.get_mouse_position(event) # if we are not on canvas, don't do anything if pos is None: return # sets the focus to the window (for the keypresses to register) self.setFocus() # (potentially) find a node that has been pressed pressed_node = None for node in self.graph.get_nodes(): if distance(pos, node.get_position()) <= node.get_radius(): pressed_node = node # (potentially) find a vertex that has been pressed pressed_vertex = None if self.graph.is_weighted(): for n1 in self.graph.get_nodes(): for n2, weight in n1.get_neighbours().items(): if self.graph.is_directed() or id(n1) < id(n2): weight = self.graph.get_weight(n1, n2) # the bounding box of this weight weight_rect = self.get_vertex_weight_rect( n1, n2, weight) if weight_rect.contains(QPointF(*pos)): pressed_vertex = (n1, n2) if event.button() == Qt.LeftButton: # nodes have the priority in selection over vertices if pressed_node is not None: self.deselect_vertex() self.select_node(pressed_node) self.mouse_drag_offset = pos - self.selected_node.get_position( ) self.mouse_position = pos elif pressed_vertex is not None: self.deselect_node() self.select_vertex(pressed_vertex) else: self.deselect_node() self.deselect_vertex() elif event.button() == Qt.RightButton: if pressed_node is not None: if self.selected_node is not None: self.graph.toggle_vertex(self.selected_node, pressed_node) else: self.graph.remove_node(pressed_node) self.deselect_node() elif pressed_vertex is not None: self.graph.remove_vertex(*pressed_vertex) self.deselect_vertex() else: node = self.graph.add_node(pos, self.node_radius) # if a selected node exists, connect it to the newly created node if self.selected_node is not None: self.graph.add_vertex(self.selected_node, node) self.deselect_vertex() self.select_node(node) def mouseReleaseEvent(self, event): """Is called when a mouse button is released; stops node drag.""" self.mouse_drag_offset = None def mouseMoveEvent(self, event): """Is called when the mouse is moved across the window; updates mouse coordinates.""" self.mouse_position = self.get_mouse_position(event, scale_down=True) def wheelEvent(self, event): """Is called when the mouse wheel is moved; node rotation and zoom.""" # positive/negative for scrolling away from/towards the user scroll_distance = radians(event.angleDelta().y() / 8) if QApplication.keyboardModifiers() == Qt.ShiftModifier: if self.selected_node is not None: self.rotate_nodes_around( self.selected_node.get_position(), scroll_distance * self.node_rotation_coefficient, ) else: mouse_coordinates = self.get_mouse_position(event) # only do something, if we're working on canvas if mouse_coordinates is None: return prev_scale = self.scale self.scale *= 2**(scroll_distance) # adjust translation so the x and y of the mouse stay in the same spot self.translation -= mouse_coordinates * (self.scale - prev_scale) def rotate_nodes_around(self, point: Vector, angle: float): """Rotates coordinates of all of the nodes in the same component as the selected node by a certain angle (in radians) around it.""" for node in self.graph.get_nodes(): if self.graph.share_component(node, self.selected_node): node.set_position((node.position - point).rotated(angle) + point) def get_mouse_position(self, event, scale_down=False) -> Vector: """Returns mouse coordinates if they are within the canvas and None if they are not. If scale_down is True, the function will scale down the coordinates to be within the canvas (useful for dragging) and return them instead.""" x = event.pos().x() y = event.pos().y() x_on_canvas = 0 <= x <= self.canvas.width() y_on_canvas = 0 <= y <= self.canvas.height() # scale down the coordinates if scale_down is True, or return None if we are # not on canvas if scale_down: x = x if x_on_canvas else 0 if x <= 0 else self.canvas.width() y = y if y_on_canvas else 0 if y <= 0 else self.canvas.height() elif not x_on_canvas or not y_on_canvas: return None # return the mouse coordinates, accounting for canvas translation and scale return (Vector(x, y) - self.translation) / self.scale def perform_simulation_iteration(self): """Performs one iteration of the simulation.""" # evaluate forces that act upon each pair of nodes for i, n1 in enumerate(self.graph.get_nodes()): for j, n2 in enumerate(self.graph.get_nodes()[i + 1:]): # if they are not in the same component, no forces act on them if not self.graph.share_component(n1, n2): continue # if the nodes are right on top of each other, no forces act on them d = distance(n1.get_position(), n2.get_position()) if n1.get_position() == n2.get_position(): continue uv = (n2.get_position() - n1.get_position()).unit() # the size of the repel force between the two nodes #fr = self.repulsion_force(d) # add a repel force to each of the nodes, in the opposite directions # n1.add_force(-uv * fr) # n2.add_force(uv * fr) # if they are also connected, add the attraction force #if self.graph.does_vertex_exist(n1, n2, ignore_direction=True): # fa = self.attraction_force(d) # n1.add_force(-uv * fa) # n2.add_force(uv * fa) # since this node will not be visited again, we can evaluate the forces # n1.evaluate_forces() # drag the selected node if self.selected_node is not None and self.mouse_drag_offset is not None: prev_node_position = self.selected_node.get_position() self.selected_node.set_position(self.mouse_position - self.mouse_drag_offset) # move the rest of the nodes that are connected to the selected node if # shift is pressed if QApplication.keyboardModifiers() == Qt.ShiftModifier: pos_delta = self.selected_node.get_position( ) - prev_node_position for node in self.graph.get_nodes(): if node is not self.selected_node and self.graph.share_component( node, self.selected_node): node.set_position(node.get_position() + pos_delta) self.update() def paintEvent(self, event): """Paints the board.""" painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing, True) painter.setPen(QPen(Qt.black, Qt.SolidLine)) painter.setBrush(QBrush(Qt.white, Qt.SolidPattern)) painter.setClipRect(0, 0, self.canvas.width(), self.canvas.height()) # background painter.drawRect(0, 0, self.canvas.width(), self.canvas.height()) painter.translate(*self.translation) painter.scale(self.scale, self.scale) # draw vertexes for n1 in self.graph.get_nodes(): for n2, weight in n1.get_neighbours().items(): self.draw_vertex(n1, n2, weight, painter) # draw nodes for node in self.graph.get_nodes(): self.draw_node(node, painter) def draw_node(self, node: Node, painter): """Draw the specified node.""" painter.setBrush( QBrush( self.selected_color if node is self.selected_node else self.regular_node_color, Qt.SolidPattern, )) node_position = node.get_position() node_radius = Vector(node.get_radius()).repeat(2) painter.drawEllipse(QPointF(*node_position), *node_radius) if self.labels_checkbox.isChecked(): label = node.get_label() # scale font down, depending on the length of the label of the node painter.setFont( QFont(self.font_family, self.font_size / len(label))) # draw the node label within the node dimensions painter.drawText( QRectF(*(node_position - node_radius), *(2 * node_radius)), Qt.AlignCenter, label, ) def draw_vertex(self, n1: Node, n2: Node, weight: float, painter): """Draw the specified vertex.""" # special case for a node pointing to itself if n1 is n2: r = n1.get_radius() x, y = n1.get_position() painter.setPen(QPen(Qt.black, Qt.SolidLine)) painter.setBrush(QBrush(Qt.black, Qt.NoBrush)) painter.drawEllipse(QPointF(x - r / 2, y - r), r / 2, r / 2) head = Vector(x, y) - Vector(0, r) uv = Vector(0, 1) painter.setBrush(QBrush(Qt.black, Qt.SolidPattern)) painter.drawPolygon( QPointF(*head), QPointF(*(head + (-uv).rotated(radians(10)) * self.arrowhead_size)), QPointF(*(head + (-uv).rotated(radians(-50)) * self.arrowhead_size)), ) else: start, end = self.get_vertex_position(n1, n2) # draw the head of a directed arrow, which is an equilateral triangle if self.graph.is_directed(): uv = (end - start).unit() painter.setBrush(QBrush(Qt.black, Qt.SolidPattern)) painter.drawPolygon( QPointF(*end), QPointF( *(end + (-uv).rotated(radians(30)) * self.arrowhead_size)), QPointF( *(end + (-uv).rotated(radians(-30)) * self.arrowhead_size)), ) painter.setPen(QPen(Qt.black, Qt.SolidLine)) painter.drawLine(QPointF(*start), QPointF(*end)) if self.graph.is_weighted(): # set color according to whether the vertex is selected or not painter.setBrush( QBrush( self.selected_color if self.selected_vertex is not None and ((n1 is self.selected_vertex[0] and n2 is self.selected_vertex[1]) or (not self.graph.is_directed() and n2 is self.selected_vertex[0] and n1 is self.selected_vertex[1])) else self.regular_vertex_weight_color, Qt.SolidPattern, )) weight_rectangle = self.get_vertex_weight_rect(n1, n2, weight) painter.drawRect(weight_rectangle) painter.setFont(QFont(self.font_family, int(self.font_size / 4))) painter.setPen(QPen(Qt.white, Qt.SolidLine)) painter.drawText(weight_rectangle, Qt.AlignCenter, str(weight)) painter.setPen(QPen(Qt.black, Qt.SolidLine)) def get_vertex_position(self, n1: Node, n2: Node) -> Tuple[Vector, Vector]: """Return the position of the vertex on the screen.""" # positions of the nodes n1_p = Vector(*n1.get_position()) n2_p = Vector(*n2.get_position()) # unit vector from n1 to n2 uv = (n2_p - n1_p).unit() # start and end of the vertex to be drawn start = n1_p + uv * n1.get_radius() end = n2_p - uv * n2.get_radius() if self.graph.is_directed(): # if the graph is directed and a vertex exists that goes the other way, we # have to move the start end end so the vertexes don't overlap if self.graph.does_vertex_exist(n2, n1): start = start.rotated(self.arrow_separation, n1_p) end = end.rotated(-self.arrow_separation, n2_p) return start, end def get_vertex_weight_rect(self, n1: Node, n2: Node, weight: float): """Get a RectF surrounding the weight of the node.""" r = self.weight_rectangle_size # width adjusted to number of chars in weight label adjusted_width = len(str(weight)) / 3 * r + r / 3 weight_vector = Vector(r if adjusted_width <= r else adjusted_width, r) if n1 is n2: # special case for a vertex pointing to itself mid = n1.get_position() - Vector(r * 3, r * 4) else: start, end = self.get_vertex_position(n1, n2) mid = (start + end) / 2 return QRectF(*(mid - weight_vector), *(2 * weight_vector))
class filterDialog(QDialog): """ FIXME existe duda si en guias jerarquicas filtra con toda la amplitud """ def __init__(self,recordStructure,currentData,title,parent=None,driver=None): super().__init__(parent) # cargando parametros de defecto self.record = recordStructure self.campos = [ elem['name'] for elem in recordStructure ] self.formatos =[ elem['format'] for elem in recordStructure ] self.driver = driver self.context = [] self.context.append(('campo','formato','condicion','valores')) self.context.append((None,WComboBox,None,self.campos)) self.context.append((None,QLineEdit,{'setEnabled':False},None)) self.context.append(('=',WComboBox,None,tuple(LOGICAL_OPERATOR))) self.context.append((None,QLineEdit,None,None)) self.data = [] self.sheet = WDataSheet(self.context,len(recordStructure)) cabeceras = [ item for item in self.context[0] ] self.sheet.verticalHeader().hide() self.sheet.setHorizontalHeaderLabels(cabeceras) self.sheet.initialize() self.load(currentData) for i in range(self.sheet.rowCount()): #for j in range(2): self.sheet.item(i,1).setBackground(QColor(Qt.gray)) #for k in range(len(self.record)): #self.addRow(k) self.sheet.resizeRowsToContents() self.sheet.horizontalHeader().setStretchLastSection(True) self.origMsg = 'Recuerde: en SQL el separador decimal es el punto "."' # super(filterDialog,self).__init__('Defina el filtro',self.context,len(self.data),self.data,parent=parent) InicioLabel = QLabel(title) # self.mensaje = QLineEdit('') self.mensaje.setReadOnly(True) freeSqlLbl = QLabel('Texto Libre') self.freeSql = QLineEdit() #QPlainTextEdit() buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel, Qt.Horizontal) #formLayout = QHBoxLayout() #self.meatLayout = QVBoxLayout() self.meatLayout = QVBoxLayout() #QGridLayout() buttonLayout = QHBoxLayout() formLayout = QVBoxLayout() self.meatLayout.addWidget(InicioLabel) #,0,0) self.meatLayout.addWidget(self.sheet) #,1,0,6,5) self.meatLayout.addWidget(freeSqlLbl) #,8,0) self.meatLayout.addWidget(self.freeSql) #,8,1,1,4) self.meatLayout.addWidget(self.mensaje) #,10,0,1,4) buttonLayout.addWidget(buttonBox) formLayout.addLayout(self.meatLayout) formLayout.addLayout(buttonLayout) self.setLayout(formLayout) self.setMinimumSize(QSize(480,480)) self.sheet.itemChanged.connect(self.cambioCampo) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) self.setWindowTitle(title) #--- end super self.setMinimumSize(QSize(800,480)) self.mensaje.setText(self.origMsg) self.defaultBackground = self.mensaje.backgroundRole() #for k in range(4): #self.sheet.resizeColumnToContents(k) def load(self,currentData): data = [] if not currentData: return for item in currentData: if item[0]: #FIXME y si el campo no existe porque es calculado ¿? editable, etc ... linea = item[:] if not linea[1]: linea[1] = self.formatos[self.campos.index(item[0])] if not linea[2]: linea[2] = '=' data.append(linea) else: continue self.sheet.loadData(data) def cambioCampo(self,item): if item.column() != 0: return if item.text() == '': return pos = self.campos.index(item.text()) self.sheet.setData(item.row(),1,self.formatos[pos]) def accept(self): self.mensaje.setText(self.origMsg) fallo = False errorTxt = '' self.queryArray = [] values = self.sheet.unloadData() for pos,item in enumerate(values): opcode = item[2] values = item[3] if opcode in ('is null','is not null'): #TODO, esto no es así self.queryArray.append((item[0], opcode.upper(), None,None)) continue if not values: # or item[3] == '': #Existe de datos continue aslist = norm2List(values) #primero comprobamos la cardinalidad. Ojo en sentencias separadas o el elif no funciona bien if opcode in ('between','not between'): if len(aslist) != 2: errorTxt = 'La operacion between exige exactamente dos valores' fallo = True elif opcode not in ('in','not in') : if len(aslist) != 1: errorTxt = ' La operacion elegida exige un único valor' fallo = True if not fallo: testElem = aslist[0].lower().strip() formato = item[1] if formato in ('numerico','entero') and not is_number(testElem): # vago. no distingo entre ambos tipos numericos FIXME errorTxt = 'No contiene un valor numerico aceptable' fallo = True #elif formato in ('texto','binario'): #pass elif formato in ('booleano',) and testElem not in ('true','false'): errorTxt = 'Solo admitimos como booleanos: True y False' fallo = True elif formato in ('fecha','fechahora','hora') and not isDate(testElem): errorTxt = 'Formato o fecha incorrecta. Verifique que es del tipo AAAA-MM-DD HH:mm:SS' fallo = True else: pass if fallo: self.mensaje.setText('ERROR @{}: {}'.format(item[0],errorTxt)) #self.sheet.cellWidget(pos,3).selectAll() FIXME ¿que hay para combos ? self.sheet.setCurrentCell(pos,3) self.sheet.setFocus() return qfmt = 't' if formato in ('entero','numerico'): qfmt = 'n' elif formato in ('fecha','fechahora','hora'): qfmt = 'f' elif formato in ('booleano'): qfmt = 'n' #me parece self.queryArray.append((item[0], opcode.upper(), aslist[0] if len(aslist) == 1 else aslist, qfmt)) self.result = mergeStrings('AND', searchConstructor('where',where=self.queryArray,driver=self.driver), self.freeSql.text(), spaced=True) self.data = self.sheet.values() QDialog.accept(self)
class demowind(QWidget): #def datad_function(self, parent=None): # dw.scratchpad2.setText("datad_function called") # mylog() def __init__(self, parent=None): QWidget.__init__(self, parent) faulthandler.enable() self.setGeometry(200, 200, 780, 650) self.setWindowTitle('Weather Station') myfont = PyQt5.QtGui.QFont('SansSerif', 13) yellowpalette = PyQt5.QtGui.QPalette() #yellowpalette.setColor(PyQt5.QtGui.QPalette.Text, QColor(255,165,0)) yellowpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.red) cyanpalette = PyQt5.QtGui.QPalette() cyanpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.blue) redpalette = PyQt5.QtGui.QPalette() redpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.red) bluepalette = PyQt5.QtGui.QPalette() bluepalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.blue) greenpalette = PyQt5.QtGui.QPalette() greenpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.green) datad = QPushButton('data\r\ndump', self) datad.setFont(myfont) datad.setGeometry(10, 10, 90, 620) datad.clicked.connect(self.datad_function) self.textfield = QLineEdit(self) self.textfield.setFont(myfont) self.textfield.setGeometry(120, 10, 630, 50) self.textfield.setText("time:") self.identfield2 = QLineEdit(self) self.identfield2.setFont(myfont) self.identfield2.setGeometry(120, 80, 310, 50) self.identfield2.setText("IP: 192.168.1.121") self.tempfield2 = QLineEdit(self) self.tempfield2.setAutoFillBackground(True) redpalette.setColor(self.tempfield2.backgroundRole(), PyQt5.QtCore.Qt.white) self.tempfield2.setPalette(redpalette) self.tempfield2.setFont(myfont) self.tempfield2.setGeometry(120, 140, 310, 50) self.tempfield2.setText("inside temperature:") self.humidfield2 = QLineEdit(self) self.humidfield2.setAutoFillBackground(True) bluepalette.setColor(self.humidfield2.backgroundRole(), PyQt5.QtCore.Qt.white) self.humidfield2.setPalette(bluepalette) self.humidfield2.setFont(myfont) self.humidfield2.setGeometry(120, 200, 310, 50) self.humidfield2.setText("inside humidity:") self.pressfield2 = QLineEdit(self) self.pressfield2.setAutoFillBackground(True) greenpalette.setColor(self.pressfield2.backgroundRole(), PyQt5.QtCore.Qt.white) self.pressfield2.setPalette(greenpalette) self.pressfield2.setFont(myfont) self.pressfield2.setGeometry(440, 140, 310, 50) self.pressfield2.setText("inside atm pressure:") self.battfield2 = QLineEdit(self) self.battfield2.setFont(myfont) self.battfield2.setGeometry(440, 200, 310, 50) self.battfield2.setText("battery voltage:") self.identfield3 = QLineEdit(self) self.identfield3.setFont(myfont) self.identfield3.setGeometry(120, 300, 310, 50) self.identfield3.setText("IP: 192.168.1.113") self.tempfield3 = QLineEdit(self) self.tempfield3.setAutoFillBackground(True) yellowpalette.setColor(self.tempfield3.backgroundRole(), PyQt5.QtCore.Qt.white) self.tempfield3.setPalette(redpalette) self.tempfield3.setFont(myfont) self.tempfield3.setGeometry(120, 360, 310, 50) self.tempfield3.setText("outside temperature:") self.humidfield3 = QLineEdit(self) self.humidfield3.setAutoFillBackground(True) cyanpalette.setColor(self.humidfield3.backgroundRole(), PyQt5.QtCore.Qt.white) self.humidfield3.setPalette(bluepalette) self.humidfield3.setFont(myfont) self.humidfield3.setGeometry(120, 420, 310, 50) self.humidfield3.setText("outside humidity:") self.pressfield3 = QLineEdit(self) self.pressfield3.setAutoFillBackground(True) #greenpalette.setColor(self.pressfield3.backgroundRole(), PyQt5.QtCore.Qt.black) self.pressfield3.setPalette(greenpalette) self.pressfield3.setFont(myfont) self.pressfield3.setGeometry(440, 360, 310, 50) self.pressfield3.setText("outside atm pressure:") self.battfield3 = QLineEdit(self) self.battfield3.setFont(myfont) self.battfield3.setGeometry(440, 420, 310, 50) self.battfield3.setText("battery voltage:") self.scratchpad2 = QLineEdit(self) self.scratchpad2.setFont(myfont) self.scratchpad2.setGeometry(120, 510, 630, 50) self.scratchpad2.setText("scratchpad2") self.scratchpad3 = QLineEdit(self) self.scratchpad3.setFont(myfont) self.scratchpad3.setGeometry(120, 580, 630, 50) self.scratchpad3.setText("scratchpad3") self.temp_array2 = numpy.zeros(1000) self.humid_array2 = numpy.zeros(1000) self.press_array2 = 29.1 + numpy.zeros(1000) self.batt_array2 = numpy.zeros(1000) self.x2 = numpy.zeros(1000) self.temp_array3 = numpy.zeros(1000) self.humid_array3 = numpy.zeros(1000) self.press_array3 = 29.1 + numpy.zeros(1000) self.batt_array3 = numpy.zeros(1000) self.x3 = numpy.zeros(1000) self.time_array = numpy.zeros(1000) self.temp_plt = pg.plot() self.press_plt = pg.plot() pg.setConfigOptions(antialias=True) #view = pg.GraphicsView() #layout = pg.GraphicsLayout() #view.setCentralItem(layout) #view.resize(2000,400) #self.scratchpad2.setText("sizing") #self.scratchpad3.setText("sizing") self.temp_plt.setBackground('w') self.temp_plt.resize(1000, 850) self.press_plt.setBackground('w') self.press_plt.resize(1000, 700) #self.scratchpad2.setText("sized") #self.scratchpad3.setText("sized") self.p12 = self.temp_plt.plotItem self.p12.showAxis('right') self.p12.setYRange(0, 100) self.p13 = self.press_plt.plotItem self.p13.showAxis('right') self.p13.setYRange(29, 30.5) self.p22 = pg.ViewBox() self.p32 = pg.ViewBox() self.p42 = pg.ViewBox() self.p23 = pg.ViewBox() self.timeplot = pg.ViewBox() self.p12.scene().addItem(self.p22) self.p12.scene().addItem(self.p32) self.p12.scene().addItem(self.p42) self.p13.scene().addItem(self.p23) self.p12.scene().addItem(self.timeplot) self.p12.getAxis('right').linkToView(self.p22) self.p12.getAxis('right').linkToView(self.p32) self.p12.getAxis('right').linkToView(self.p42) #self.p12.getAxis('left').linkToView(self.p32) self.p13.getAxis('right').linkToView(self.p23) #self.p13.getAxis('left').linkToView(self.p33) self.p12.getAxis('left').linkToView(self.timeplot) self.p22.setYRange(0, 100) self.p32.setYRange(0, 100) self.p42.setYRange(0, 100) self.p23.setYRange(29, 30.5) self.timeplot.setYRange(0, 100) self.p22.setXLink(self.p12) self.p32.setXLink(self.p12) self.p42.setXLink(self.p12) self.p23.setXLink(self.p13) self.timeplot.setXLink(self.p12) self.p12.setGeometry(self.p12.vb.sceneBoundingRect()) self.p22.setGeometry(self.p12.vb.sceneBoundingRect()) self.p32.setGeometry(self.p12.vb.sceneBoundingRect()) self.p42.setGeometry(self.p12.vb.sceneBoundingRect()) self.p13.setGeometry(self.p13.vb.sceneBoundingRect()) self.p23.setGeometry(self.p13.vb.sceneBoundingRect()) self.timeplot.setGeometry(self.p13.vb.sceneBoundingRect()) #self.mylegend = self.plt.addLegend((100,100),(50,50)) self.my_temp_legend = self.temp_plt.addLegend() self.my_press_legend = self.press_plt.addLegend() #self.legend2 = pg.LegendItem() #self.legend3 = pg.LegendItem() #self.p2.addItem(self.legend2) #self.p3.addItem(self.legend3) #self.curve13=self.plt.plot(x=[] , y=[], pen = pg.mkPen(color=(255,165,0), width=4), name="Outside temperature", style=PyQt5.QtCore.Qt.DotLine) self.curve12 = self.temp_plt.plot(x=[], y=[], pen=pg.mkPen('r', width=6), name="inside temperature", style=PyQt5.QtCore.Qt.DotLine) self.curve13 = self.press_plt.plot(x=[], y=[], pen=pg.mkPen('g', width=6), name="inside air pressure", style=PyQt5.QtCore.Qt.DotLine) self.curve22 = pg.PlotCurveItem(pen=pg.mkPen('r', width=3), name="outside temperature", style=PyQt5.QtCore.Qt.DotLine) self.curve32 = pg.PlotCurveItem(pen=pg.mkPen('b', width=6), name="inside humidity", style=PyQt5.QtCore.Qt.DotLine) self.curve42 = pg.PlotCurveItem(pen=pg.mkPen('b', width=3), name="outside humidity", style=PyQt5.QtCore.Qt.DotLine) self.curve23 = pg.PlotCurveItem(pen=pg.mkPen('g', width=3), name="outside air pressure") self.curve_tp = pg.PlotCurveItem(pen=pg.mkPen(QColor(128, 128, 128), width=4), name="time") #self.my_temp_legend.addItem(self.curve12, name = self.curve12.opts['name']) self.my_temp_legend.addItem(self.curve22, name=self.curve22.opts['name']) self.my_temp_legend.addItem(self.curve32, name=self.curve32.opts['name']) self.my_temp_legend.addItem(self.curve42, name=self.curve42.opts['name']) #self.my_press_legend.addItem(self.curve13, name = self.curve13.opts['name']) self.my_press_legend.addItem(self.curve23, name=self.curve23.opts['name']) #self.p12.addItem(self.curve12) self.p22.addItem(self.curve22) self.p32.addItem(self.curve32) self.p42.addItem(self.curve42) #self.p13.addItem(self.curve13) self.p23.addItem(self.curve23) self.timeplot.addItem(self.curve_tp) #self.plt_temp = self.plt.plot(self.x, self.temp_array, title = "my plot", pen = pg.mkPen('r', width=4)) #self.plt_humid = self.plt.plot(self.x, self.humid_array, title = "my plot", pen = pg.mkPen('g', width=4)) #self.plt_press = self.plt.plot(self.x, self.press_array, title = "my plot", pen = pg.mkPen('b', width=4)) #threading.Timer(10.0, data_display).start() #threading.Timer(10.0, datad_function).start() #self.scratchpad2.setText("about to start QTimer") self.timer = QTimer() self.timer.setInterval(120000) self.timer.timeout.connect(self.datad_function) self.timer.start() #self.scratchpad2.setText("QTimer should have started") def test_function(self, parent=None): dw.scratchpad2.setText("test_function called") def datad_function(self, parent=None): dw.scratchpad2.setText("datad_function called") self.mylog() def repeat(self): # # Arduino 2: 192.168.1.121 # #dw.scratchpad2.setText("starting socket opps") global temperature_2 global temperature_3 global temperature_num_2 global temperature_num_3 global humidity_num_2 global humidity_num_3 global pressure_num_2 global pressure_num_3 global V_num_2 global V_num_3 global I_num_2 global I_num_3 #update_temp = pyqtSignal(str) #update_temp.emit(temperature_2) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #s.setblocking(0) HOST = '192.168.1.121' PORT = 80 s.connect((HOST, PORT)) #connect to 192.168.1.121, Arduino 2 s.sendto("**dump2".encode(), (HOST, PORT)) message = s.recv(60) temperature_2 = message.decode('utf-8') s.close() temperature_string = temperature_2[0:4:1] #dw.scratchpad2.setText(temperature_2) #ts = time.gmtime() #time_str = time.strftime("time: %Y-%m-%d %H-%M-%S", ts) #dw.textfield.setText(time_str) #dw.textfield.setText("current time is %f" % time.time()) now = datetime.now() s_s_m = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() d = 50 - (50 * math.cos(s_s_m * 6.28318 / 86400)) if self.isfloat(temperature_string): temperature_num_2 = (float(temperature_string)) / 100 temperature_num_2 = (temperature_num_2 * 1.8) + 32 #dw.tempfield2.setText("temperature: %0.2f deg F" % temperature_num_2) #dw.scratchpad.setText("True") i = 0 while i < 999: dw.temp_array2[i] = dw.temp_array2[i + 1] dw.time_array[i] = dw.time_array[i + 1] i += 1 dw.temp_array2[999] = temperature_num_2 dw.time_array[999] = d #else: # dw.scratchpad2.setText("temp 2 error: %s" % time_str) index = temperature_2.find('HM') humidity_string = temperature_2[index + 2:index + 6:1] #dw.scratchpad2.setText(humidity_string) if self.isfloat(humidity_string): humidity_num_2 = (float(humidity_string)) / 100 #dw.humidfield2.setText("humidity: %0.2f %%" % humidity_num_2) i = 0 while i < 999: dw.humid_array2[i] = dw.humid_array2[i + 1] i += 1 dw.humid_array2[999] = humidity_num_2 #else: # dw.scratchpad2.setText("humidity 2 error: %s" % time_str) index = temperature_2.find('PS') pressure_string = temperature_2[index + 2:index + 6:1] if self.isfloat(pressure_string): pressure_num_2 = (float(pressure_string)) * 0.02953 #dw.pressfield2.setText("atm pressure: %0.2f inHg" % pressure_num_2) i = 0 while i < 999: dw.press_array2[i] = dw.press_array2[i + 1] i += 1 dw.press_array2[999] = 0.2 * ( pressure_num_2 + dw.press_array2[998] + dw.press_array2[997] + dw.press_array2[996] + dw.press_array2[995]) #dw.scratchpad.setText("atm pressure: %d" % dw.press_array2[359]) #else: # dw.scratchpad2.setText("atm pressure 2 error: %s" % time_str) indexV1 = temperature_2.find('LV') #indexV2=temperature_2.find('CU') V_string = temperature_2[indexV1 + 2:indexV1 + 6:1] indexI1 = temperature_2.find('CU') #indexI2=len(temperature_2) I_string = temperature_2[indexI1 + 2:indexI1 + 7:1] if (self.isfloat(V_string) and self.isfloat(I_string)): V_num_2 = (float(V_string)) I_num_2 = (float(I_string)) #dw.battfield2.setText("battery: %0.2fmA@%0.2fV" % (I_num_2, V_num_2)) #i=0 #while i<359: # dw.batt_array2[i]=dw.batt_array2[i+1] # i+=1 #dw.batt_array2[359]=V_num #else: # dw.scratchpad2.setText("I or V, 2 error: %s" % time_str) #s.close() # # Arduino 3: 192.168.1.113 # s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #s.setblocking(0) HOST = '192.168.1.113' PORT = 80 s.connect((HOST, PORT)) #connect to 192.168.1.113, Arduino 3 s.sendto("**dump".encode(), (HOST, PORT)) message = s.recv(60) temperature_3 = message.decode('utf-8') s.close() temperature_string = temperature_3[0:4:1] #dw.scratchpad3.setText(temperature_3) if self.isfloat(temperature_string): temperature_num_3 = (float(temperature_string)) / 100 temperature_num_3 = (temperature_num_3 * 1.8) + 32 #dw.tempfield3.setText("temperature: %0.2f deg F" % temperature_num_3) #dw.scratchpad.setText("True") i = 0 while i < 999: dw.temp_array3[i] = dw.temp_array3[i + 1] i += 1 dw.temp_array3[999] = temperature_num_3 #else: # dw.scratchpad3.setText("temp 3 error: %s" % time_str) index = temperature_3.find('HM') humidity_string = temperature_3[index + 2:index + 6:1] #dw.scratchpad3.setText(humidity_string) if self.isfloat(humidity_string): humidity_num_3 = (float(humidity_string)) / 100 #dw.humidfield3.setText("humidity: %0.2f %%" % humidity_num_3) i = 0 while i < 999: dw.humid_array3[i] = dw.humid_array3[i + 1] i += 1 dw.humid_array3[999] = humidity_num_3 #else: # dw.scratchpad3.setText("humidity 3 error: %s" % time_str) index = temperature_3.find('PS') pressure_string = temperature_3[index + 2:index + 6:1] if self.isfloat(pressure_string): pressure_num_3 = (float(pressure_string)) * 0.02953 #pressure_num_3 = pressure_num_3 + 0.01 #dw.pressfield3.setText("atm pressure: %0.2f inHg" % pressure_num_3) i = 0 while i < 999: dw.press_array3[i] = dw.press_array3[i + 1] i += 1 dw.press_array3[999] = 0.2 * ( pressure_num_3 + dw.press_array3[998] + dw.press_array3[997] + dw.press_array3[996] + dw.press_array3[995]) #dw.scratchpad.setText("atm pressure: %d" % dw.press_array3[359]) #else: # dw.scratchpad3.setText("atm pressure 3 error: %s" % time_str) indexV1 = temperature_3.find('LV') #indexV2=temperature_3.find('CU') V_string = temperature_3[indexV1 + 2:indexV1 + 6:1] indexI1 = temperature_3.find('CU') #indexI2=len(temperature_3) I_string = temperature_3[indexI1 + 2:indexI1 + 7:1] if (self.isfloat(V_string) and self.isfloat(I_string)): V_num_3 = (float(V_string)) I_num_3 = (float(I_string)) #dw.battfield3.setText("battery: %0.2fmA@%0.2fV" % (I_num_3, V_num_3)) #i=0 #while i<359: # dw.batt_array3[i]=dw.batt_array3[i+1] # i+=1 #dw.batt_array3[359]=V_num #else: # dw.scratchpad3.setText("I or V, 3 error: %s" % time_str) #s.close() def mylog(self): #dw.scratchpad2.setText("mylog called") dw.p22.setGeometry(dw.p12.vb.sceneBoundingRect()) dw.p32.setGeometry(dw.p12.vb.sceneBoundingRect()) dw.p42.setGeometry(dw.p12.vb.sceneBoundingRect()) dw.p23.setGeometry(dw.p13.vb.sceneBoundingRect()) dw.timeplot.setGeometry(dw.p13.vb.sceneBoundingRect()) self.repeat() ts = time.localtime() time_str = time.strftime("time: %Y-%m-%d %H-%M-%S", ts) dw.textfield.setText(time_str) dw.scratchpad2.setText(temperature_2) dw.scratchpad3.setText(temperature_3) dw.tempfield2.setText("inside temperature: %0.2f deg F" % temperature_num_2) dw.tempfield3.setText("outside temperature: %0.2f deg F" % temperature_num_3) dw.humidfield2.setText("inside humidity: %0.2f %%" % humidity_num_2) dw.humidfield3.setText("outside humidity: %0.2f %%" % humidity_num_3) dw.pressfield2.setText("inside atm pressure: %0.2f inHg" % pressure_num_2) dw.pressfield3.setText("outside atm pressure: %0.2f inHg" % pressure_num_3) dw.battfield2.setText("inside battery: %0.2fmA@%0.2fV" % (I_num_2, V_num_2)) dw.battfield3.setText("outside battery: %0.2fmA@%0.2fV" % (I_num_3, V_num_3)) #dw.p1.plot(dw.x, dw.temp_array) #dw.curve.setData(dw.x, dw.temp_array) #dw.plt_humid.setData(dw.x, dw.humid_array) dw.curve12.setData(dw.temp_array2) dw.curve22.setData(dw.temp_array3) dw.curve32.setData(dw.humid_array2) dw.curve42.setData(dw.humid_array3) dw.curve13.setData(dw.press_array2) dw.curve23.setData(dw.press_array3) dw.curve_tp.setData(dw.time_array) #dw.curve.setData(dw.x, dw.temp_array) #dw.curve2.setData(dw.x, dw.humid_array) dw.p22.setGeometry(dw.p12.vb.sceneBoundingRect()) dw.p32.setGeometry(dw.p12.vb.sceneBoundingRect()) dw.p42.setGeometry(dw.p12.vb.sceneBoundingRect()) dw.p23.setGeometry(dw.p13.vb.sceneBoundingRect()) #dw.scratchpad.setText("resizing") #dw.plt.resize(1801,850) #dw.plt.resize(1800,850) #dw.scratchpad.setText("resized") pg.QtGui.QGuiApplication.processEvents() #threading.Timer(60.0, self.mylog).start() def data_display(): ts = time.localtime() time_str = time.strftime("time: %Y-%m-%d %H-%M-%S", ts) dw.textfield.setText(time_str) dw.scratchpad2.setText(temperature_2) dw.scratchpad3.setText(temperature_3) dw.tempfield2.setText("temperature: %0.2f deg F" % temperature_num_2) dw.tempfield3.setText("temperature: %0.2f deg F" % temperature_num_3) dw.humidfield2.setText("humidity: %0.2f %%" % humidity_num_2) dw.humidfield3.setText("humidity: %0.2f %%" % humidity_num_3) dw.pressfield2.setText("atm pressure: %0.2f inHg" % pressure_num_2) dw.pressfield3.setText("atm pressure: %0.2f inHg" % pressure_num_3) dw.battfield2.setText("battery: %0.2fmA@%0.2fV" % (I_num_2, V_num_2)) dw.battfield2.setText("battery: %0.2fmA@%0.2fV" % (I_num_3, V_num_3)) #threading.Timer(10.0, data_display).start() def isfloat(self, s): try: float(s) return True except ValueError: return False
class TreeVisualizer(QWidget): def __init__(self): """Initial configuration.""" super().__init__() # GLOBAL VARIABLES # graph variables self.graph = Graph() self.selected_node = None self.vertex_positions = [] self.selected_vertex = None # offset of the mouse from the position of the currently dragged node self.mouse_drag_offset = None # position of the mouse; is updated when the mouse moves self.mouse_x = -1 self.mouse_y = -1 # variables for visualizing the graph self.node_radius = 20 self.weight_rectangle_size = self.node_radius / 3 self.arrowhead_size = 4 self.arrow_separation = pi / 7 self.selected_color = Qt.red self.regular_node_color = Qt.white self.regular_vertex_weight_color = Qt.black # limit the displayed length of labels for each node self.node_label_limit = 10 # UI variables self.font_family = "Times New Roman" self.font_size = 18 self.layout_margins = 8 self.layout_item_spacing = 2 * self.layout_margins # canvas positioning - scale and translation self.scale = 1 self.scale_coefficient = 1.1 # by how much the scale changes on scroll self.translation = [0, 0] # angle (in degrees) by which all of the nodes rotate self.node_rotation_angle = 15 # TIMERS # runs the simulation 60 times a second (1000/60 ~= 16ms) self.simulation_timer = QTimer( interval=16, timeout=self.perform_simulation_iteration) # WIDGETS self.canvas = QFrame(self, minimumSize=QSize(0, 400)) self.canvas_size = None self.canvas.resizeEvent = self.adjust_canvas_translation # toggles between directed/undirected graphs self.directed_toggle_button = QPushButton( text="undirected", clicked=self.toggle_directed_graph) # for showing the labels of the nodes self.labels_checkbox = QCheckBox(text="labels") # sets, whether the graph is weighted or not self.weighted_checkbox = QCheckBox(text="weighted", clicked=self.set_weighted_graph) # enables/disables forces (True by default - they're fun!) self.forces_checkbox = QCheckBox(text="forces", checked=True) # input of the labels and vertex weights self.input_line_edit = QLineEdit( enabled=self.labels_checkbox.isChecked(), textChanged=self.input_line_edit_changed) # displays information about the app self.about_button = QPushButton(text="?", clicked=self.show_help, sizePolicy=QSizePolicy( QSizePolicy.Fixed, QSizePolicy.Fixed)) # imports/exports the current graph self.import_graph_button = QPushButton(text="import", clicked=self.import_graph) self.export_graph_button = QPushButton(text="export", clicked=self.export_graph) # WIDGET LAYOUT self.main_v_layout = QVBoxLayout(self, margin=0) self.main_v_layout.addWidget(self.canvas) self.option_h_layout = QHBoxLayout(self, margin=self.layout_margins) self.option_h_layout.addWidget(self.directed_toggle_button) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.weighted_checkbox) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.labels_checkbox) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.forces_checkbox) self.option_h_layout.addSpacing(self.layout_item_spacing) self.option_h_layout.addWidget(self.input_line_edit) self.io_h_layout = QHBoxLayout(self, margin=self.layout_margins) self.io_h_layout.addWidget(self.import_graph_button) self.io_h_layout.addSpacing(self.layout_item_spacing) self.io_h_layout.addWidget(self.export_graph_button) self.io_h_layout.addSpacing(self.layout_item_spacing) self.io_h_layout.addWidget(self.about_button) self.main_v_layout.addLayout(self.option_h_layout) self.main_v_layout.addSpacing(-self.layout_margins) self.main_v_layout.addLayout(self.io_h_layout) self.setLayout(self.main_v_layout) # WINDOW SETTINGS self.setWindowTitle('Graph Visualizer') self.setFont(QFont(self.font_family, self.font_size)) self.setWindowIcon(QIcon("icon.ico")) self.show() # start the simulation self.simulation_timer.start() def set_weighted_graph(self): """Is called when the weighted checkbox is pressed; sets, whether the graph is weighted or not.""" self.graph.set_weighted(self.weighted_checkbox.isChecked()) def adjust_canvas_translation(self, event): """Is called when the canvas widget is resized; changes translation so the center stays in the center.""" size = (event.size().width(), event.size().height()) # don't translate on the initial resize if self.canvas_size is not None: self.translation[0] += self.scale * (size[0] - self.canvas_size[0]) / 2 self.translation[1] += self.scale * (size[1] - self.canvas_size[1]) / 2 self.canvas_size = size def repulsion_force(self, distance): """Calculates the strength of the repulsion force at the specified distance.""" return 1 / distance * 10 if self.forces_checkbox.isChecked() else 0 def attraction_force(self, distance, leash_length=80): """Calculates the strength of the attraction force at the specified distance and leash length.""" return -(distance - leash_length) / 10 if self.forces_checkbox.isChecked() else 0 def import_graph(self): """Is called when the import button is clicked; imports a graph from a file.""" path = QFileDialog.getOpenFileName()[0] if path != "": try: with open(path, "r") as file: # a list of vertices of the graph data = [line.strip() for line in file.read().splitlines()] sample = data[0].split(" ") vertex_types = ["->", "<", "<>"] # whether the graph is directed and weighted; done by looking at a sample input vertex directed = True if sample[1] in vertex_types else False weighted = False if len( sample) == 2 or directed and len(sample) == 3 else True graph = Graph(directed=directed, weighted=weighted) # to remember the created nodes and to connect them later node_dictionary = {} # add each of the nodes of the vertex to the graph for vertex in data: vertex_components = vertex.split(" ") # the formats are either 'A B' or 'A ... B', where ... is one of the vertex types nodes = [ vertex_components[0], vertex_components[2] if directed else vertex_components[1] ] # if weights are present, the formats are either 'A B num' or 'A ... B num (num)' weights_strings = None if not weighted else [ vertex_components[2] if not directed else vertex_components[3], None if not directed or vertex_components[1] != vertex_types[2] else vertex_components[4] ] for node in nodes: if node not in node_dictionary: # slightly randomize the coordinates so the graph doesn't stay in one place x = self.canvas.width() / 2 + (random() - 0.5) y = self.canvas.height() / 2 + (random() - 0.5) # add it to graph with default values node_dictionary[node] = graph.add_node( x, y, self.node_radius, node) # get the node objects from the names n1, n2 = node_dictionary[nodes[0]], node_dictionary[ nodes[1]] # export the graph, according to the direction of the vertex, and whether it's weighted or not if not directed or vertex_components[1] == "->": if weighted: graph.add_vertex( n1, n2, self._convert_string_to_number( weights_strings[0])) else: graph.add_vertex(n1, n2) elif vertex_components[1] == "<-": if weighted: graph.add_vertex( n2, n1, self._convert_string_to_number( weights_strings[0])) else: graph.add_vertex(n2, n1) else: if weighted: graph.add_vertex( n1, n2, self._convert_string_to_number( weights_strings[0])) graph.add_vertex( n2, n1, self._convert_string_to_number( weights_strings[1])) else: graph.add_vertex(n1, n2) graph.add_vertex(n2, n1) self.graph = graph except UnicodeDecodeError: QMessageBox.critical(self, "Error!", "Can't read binary files!") except ValueError: QMessageBox.critical( self, "Error!", "The weights of the graph are not numbers!") except Exception: QMessageBox.critical( self, "Error!", "An error occurred when importing the graph. Make sure that the " "file is in the correct format!") # make sure that the UI is in order self.deselect_node() self.deselect_vertex() self.set_checkbox_values() def _convert_string_to_number(self, str): """Attempts to convert the specified string to a number. Throws error if it fails to do so!""" try: return int(str) except ValueError: return float(str) def set_checkbox_values(self): """Sets the values of the checkboxes from the graph.""" self.weighted_checkbox.setChecked(self.graph.is_weighted()) self.update_directed_toggle_button_text() def export_graph(self): """Is called when the export button is clicked; exports a graph to a file.""" path = QFileDialog.getSaveFileName()[0] if path != "": try: with open(path, "w") as file: # look at every pair of nodes and examine the vertices for i in range(len(self.graph.get_nodes())): n1 = self.graph.get_nodes()[i] for j in range(i + 1, len(self.graph.get_nodes())): n2 = self.graph.get_nodes()[j] v1_exists = self.graph.does_vertex_exist(n1, n2) w1_value = self.graph.get_weight(n1, n2) w1 = "" if not self.graph.is_weighted( ) or w1_value is None else str(w1_value) if not self.graph.is_directed() and v1_exists: # for undirected graphs, no direction symbols are necessary file.write(n1.get_label() + " " + n2.get_label() + " " + w1 + "\n") else: v2_exists = self.graph.does_vertex_exist( n2, n1) w2_value = self.graph.get_weight(n2, n1) w2 = "" if not self.graph.is_weighted( ) or w2_value is None else str(w2_value) # node that w1 and w2 might be empty strings, so we can combine # directed weighted and unweighted graphs in one command if v1_exists and v2_exists: file.write("%s <> %s %s %s\n" % (n1.get_label(), n2.get_label(), str(w1), str(w2))) elif v1_exists: file.write("%s -> %s %s\n" % (n1.get_label(), n2.get_label(), str(w1))) elif v2_exists: file.write("%s <- %s %s\n" % (n1.get_label(), n2.get_label(), str(w2))) except Exception: QMessageBox.critical( self, "Error!", "An error occurred when exporting the graph. Make sure that you " "have permission to write to the specified file and try again!" ) def show_help(self): """Is called when the help button is clicked; displays basic information about the application.""" message = """ <p>Welcome to <strong>Graph Visualizer</strong>.</p> <p>The app aims to help with creating, visualizing and exporting graphs. It is powered by PyQt5 – a set of Python bindings for the C++ library Qt.</p> <hr /> <p>The controls are as follows:</p> <ul> <li><em>Left Mouse Button</em> – selects nodes and moves them</li> <li><em>Right Mouse Button</em> – creates/removes nodes and vertices</li> <li><em>Mouse Wheel</em> – zooms in/out</li> <li><em>Shift + Left Mouse Button</em> – moves connected nodes</li> <li><em>Shift + Mouse Wheel</em> – rotates nodes around the selected node</li> </ul> <hr /> <p>If you spot an issue, or would like to check out the source code, see the app's <a href="https://github.com/xiaoxiae/GraphVisualizer">GitHub repository</a>.</p> """ QMessageBox.information(self, "About", message) def toggle_directed_graph(self): """Is called when the directed checkbox changes; toggles between directed and undirected graphs.""" self.graph.set_directed(not self.graph.is_directed()) self.update_directed_toggle_button_text() def update_directed_toggle_button_text(self): """Changes the text of the directed toggle button, according to whether the graph is directer or not.""" self.directed_toggle_button.setText( "directed" if self.graph.is_directed() else "undirected") def input_line_edit_changed(self, text): """Is called when the input line edit changes; changes either the label of the node selected node, or the value of the selected vertex.""" palette = self.input_line_edit.palette() if self.selected_node is not None: # the text has to be non-zero and not contain spaces, for the import/export language to work properly # the text length is also restricted, for rendering purposes if 0 < len(text) < self.node_label_limit and " " not in text: self.selected_node.set_label(text) palette.setColor(self.input_line_edit.backgroundRole(), Qt.white) else: palette.setColor(self.input_line_edit.backgroundRole(), Qt.red) elif self.selected_vertex is not None: # try to parse the input text either as an integer, or as a float weight = None try: weight = int(text) except ValueError: try: weight = float(text) except ValueError: pass # if the parsing was unsuccessful, set the input line edit background to red to indicate this fact # if it worked, set the weight by adding a new vertex with the input weight if weight is None: palette.setColor(self.input_line_edit.backgroundRole(), Qt.red) else: self.graph.add_vertex(self.selected_vertex[0], self.selected_vertex[1], weight) palette.setColor(self.input_line_edit.backgroundRole(), Qt.white) self.input_line_edit.setPalette(palette) def select_node(self, node): """Sets the selected node to the specified node, sets the input line edit to its label and enables it.""" self.selected_node = node self.input_line_edit.setText(node.get_label()) self.input_line_edit.setEnabled(True) self.input_line_edit.setFocus() def deselect_node(self): """Sets the selected node to None and disables the input line edit.""" self.selected_node = None self.input_line_edit.setEnabled(False) def select_vertex(self, vertex): """Sets the selected vertex to the specified vertex, sets the input line edit to its weight and enables it.""" self.selected_vertex = vertex self.input_line_edit.setText(str(self.graph.get_weight(*vertex))) self.input_line_edit.setEnabled(True) self.input_line_edit.setFocus() def deselect_vertex(self): """Sets the selected vertex to None and disables the input line edit.""" self.selected_vertex = None self.input_line_edit.setEnabled(False) def mousePressEvent(self, event): """Is called when a mouse button is pressed; creates and moves nodes/vertices.""" mouse_coordinates = self.get_mouse_coordinates(event) # if we are not on canvas, don't do anything if mouse_coordinates is None: return # sets the focus to the entire window, for the keypresses to register self.setFocus() x = mouse_coordinates[0] y = mouse_coordinates[1] # (potentially) find a node that has been pressed pressed_node = None for node in self.graph.get_nodes(): if self.distance(x, y, node.get_x(), node.get_y()) <= node.get_radius(): pressed_node = node # (potentially) find a vertex that has been pressed pressed_vertex = None for vertex in self.vertex_positions: # vertex position items have the structure [x, y, (n1, n2)] if abs(vertex[0] - x) < self.weight_rectangle_size and abs( vertex[1] - y) < self.weight_rectangle_size: pressed_vertex = vertex[2] # select on left click # create/connect on right click if event.button() == Qt.LeftButton: # nodes have the priority in selection before vertices if pressed_node is not None: self.deselect_vertex() self.select_node(pressed_node) self.mouse_drag_offset = (x - self.selected_node.get_x(), y - self.selected_node.get_y()) self.mouse_x = x self.mouse_y = y elif pressed_vertex is not None: self.deselect_node() self.select_vertex(pressed_vertex) else: self.deselect_node() self.deselect_vertex() elif event.button() == Qt.RightButton: # either make/remove a connection if we right clicked a node, or create a new node if we haven't if pressed_node is not None: if self.selected_node is not None and pressed_node is not self.selected_node: # if a connection does not exist between the nodes, create it; otherwise remove it if self.graph.does_vertex_exist(self.selected_node, pressed_node): self.graph.remove_vertex(self.selected_node, pressed_node) else: self.graph.add_vertex(self.selected_node, pressed_node) else: self.graph.remove_node(pressed_node) self.deselect_node() elif pressed_vertex is not None: self.graph.remove_vertex(*pressed_vertex) self.deselect_vertex() else: node = self.graph.add_node(x, y, self.node_radius) # if a selected node exists, connect it to the newly created node if self.selected_node is not None: self.graph.add_vertex(self.selected_node, node) self.select_node(node) self.deselect_vertex() def mouseReleaseEvent(self, event): """Is called when a mouse button is released; stops node drag.""" self.mouse_drag_offset = None def mouseMoveEvent(self, event): """Is called when the mouse is moved across the window; updates mouse coordinates.""" mouse_coordinates = self.get_mouse_coordinates(event, scale_down=True) self.mouse_x = mouse_coordinates[0] self.mouse_y = mouse_coordinates[1] def wheelEvent(self, event): """Is called when the mouse wheel is moved; node rotation and zoom.""" if QApplication.keyboardModifiers() == Qt.ShiftModifier: if self.selected_node is not None: # positive/negative for scrolling away from/towards the user angle = self.node_rotation_angle if event.angleDelta().y( ) > 0 else -self.node_rotation_angle self.rotate_nodes_around(self.selected_node.get_x(), self.selected_node.get_y(), angle) else: mouse_coordinates = self.get_mouse_coordinates(event) # only do something, if we're working on canvas if mouse_coordinates is None: return x, y = mouse_coordinates[0], mouse_coordinates[1] prev_scale = self.scale # adjust the canvas scale, depending on the scroll direction # if angleDelta.y() is positive, scroll away (zoom out) from the user (and vice versa) if event.angleDelta().y() > 0: self.scale *= self.scale_coefficient else: self.scale /= self.scale_coefficient # adjust translation so the x and y of the mouse stay in the same spot scale_delta = self.scale - prev_scale self.translation[0] += -(x * scale_delta) self.translation[1] += -(y * scale_delta) def rotate_nodes_around(self, x, y, angle): """Rotates coordinates of all of the points by a certain angle (in degrees) around the specified point.""" angle = radians(angle) for node in self.graph.get_nodes(): # only rotate points that are in the same continuity set if self.graph.share_continuity_set(node, self.selected_node): # translate the coordinates to origin for the rotation to work node_x, node_y = node.get_x() - x, node.get_y() - y # rotate and translate the coordinates of the node node.set_x(node_x * cos(angle) - node_y * sin(angle) + x) node.set_y(node_x * sin(angle) + node_y * cos(angle) + y) def get_mouse_coordinates(self, event, scale_down=False): """Returns mouse coordinates if they are within the canvas and None if they are not. If scale_down is True, the function will scale down the coordinates to be within the canvas (useful for dragging) and return them instead.""" x = event.pos().x() y = event.pos().y() # booleans for whether the coordinate components are on canvas x_on_canvas = 0 <= x <= self.canvas.width() y_on_canvas = 0 <= y <= self.canvas.height() # scale down the coordinates if scale_down is True, or return none if we are not on canvas if scale_down: x = x if x_on_canvas else 0 if x <= 0 else self.canvas.width() y = y if y_on_canvas else 0 if y <= 0 else self.canvas.height() elif not x_on_canvas or not y_on_canvas: return None # return the mouse coordinates, accounting for the translation and scale of the canvas return ((x - self.translation[0]) / self.scale, (y - self.translation[1]) / self.scale) def perform_simulation_iteration(self): """Performs one iteration of the simulation.""" # evaluate forces that act upon each pair of nodes for i in range(len(self.graph.get_nodes())): n1 = self.graph.get_nodes()[i] for j in range(i + 1, len(self.graph.get_nodes())): n2 = self.graph.get_nodes()[j] # if they are not in the same continuity set, no forces act on them if not self.graph.share_continuity_set(n1, n2): continue # calculate the distance of the nodes and a unit vector from the first to the second d = self.distance(n1.get_x(), n1.get_y(), n2.get_x(), n2.get_y()) # if the nodes are right on top of each other, the force can't be calculated if d == 0: continue ux, uy = (n2.get_x() - n1.get_x()) / d, (n2.get_y() - n1.get_y()) / d # the size of the repel force between the two nodes fr = self.repulsion_force(d) # add a repel force to each of the nodes, in the opposite directions n1.add_force((-ux * fr, -uy * fr)) n2.add_force((ux * fr, uy * fr)) # if they are connected, add the leash force, regardless of whether the graph is directed or not if self.graph.does_vertex_exist(n1, n2, ignore_direction=True): # the size of the attraction force between the two nodes fa = self.attraction_force(d) # add the repel force to each of the nodes, in the opposite directions n1.add_force((-ux * fa, -uy * fa)) n2.add_force((ux * fa, uy * fa)) # since this node will not be visited again, evaluate the forces n1.evaluate_forces() # drag the selected node, after all of the forces have been applied, so it doesn't move anymore if self.selected_node is not None and self.mouse_drag_offset is not None: prev_x = self.selected_node.get_x() prev_y = self.selected_node.get_y() self.selected_node.set_x(self.mouse_x - self.mouse_drag_offset[0]) self.selected_node.set_y(self.mouse_y - self.mouse_drag_offset[1]) # move the rest of the nodes that are connected to the selected node, if shift is pressed if QApplication.keyboardModifiers() == Qt.ShiftModifier: x_delta = self.selected_node.get_x() - prev_x y_delta = self.selected_node.get_y() - prev_y for node in self.graph.get_nodes(): if node is not self.selected_node and self.graph.share_continuity_set( node, self.selected_node): node.set_x(node.get_x() + x_delta) node.set_y(node.get_y() + y_delta) self.update() def paintEvent(self, event): """Paints the board.""" painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing, True) painter.setPen(QPen(Qt.black, Qt.SolidLine)) painter.setBrush(QBrush(Qt.white, Qt.SolidPattern)) # bound the canvas area to not draw outside of it painter.setClipRect(0, 0, self.canvas.width(), self.canvas.height()) # draw the background painter.drawRect(0, 0, self.canvas.width(), self.canvas.height()) # transform and scale the painter accordingly painter.translate(self.translation[0], self.translation[1]) painter.scale(self.scale, self.scale) # if the graph is weighted, reset the positions, since they will be re-drawn later on if self.graph.is_weighted(): self.vertex_positions = [] # draw vertices; has to be drawn before nodes, so they aren't drawn on top of them for node in self.graph.get_nodes(): for neighbour, weight in node.get_neighbours().items(): x1, y1, x2, y2 = node.get_x(), node.get_y(), neighbour.get_x( ), neighbour.get_y() # create a unit vector from the first to the second graph d = self.distance(x1, y1, x2, y2) ux, uy = (x2 - x1) / d, (y2 - y1) / d r = neighbour.get_radius() # if it's directed, draw the head of the arrow if self.graph.is_directed(): # in case there is a vertex going the other way, we will move the line up the circles by an angle, # so there is separation between the vertices if self.graph.does_vertex_exist(neighbour, node): nx = -uy * r * sin(self.arrow_separation) + ux * r * ( 1 - cos(self.arrow_separation)) ny = ux * r * sin(self.arrow_separation) + uy * r * ( 1 - cos(self.arrow_separation)) x1, x2, y1, y2 = x1 + nx, x2 + nx, y1 + ny, y2 + ny # the position of the head of the arrow xa, ya = x1 + ux * (d - r), y1 + uy * (d - r) # calculate the two remaining points of the arrow # this is done the same way as the previous calculation (shift by vector) d = self.distance(x1, y1, xa, ya) ux_arrow, uy_arrow = (xa - x1) / d, (ya - y1) / d # position of the base of the arrow x, y = x1 + ux_arrow * ( d - self.arrowhead_size * 2), y1 + uy_arrow * ( d - self.arrowhead_size * 2) # the normal vectors to the unit vector of the arrow head nx_arrow, ny_arrow = -uy_arrow, ux_arrow # draw the tip of the arrow, as the triangle painter.setBrush(QBrush(Qt.black, Qt.SolidPattern)) painter.drawPolygon( QPointF(xa, ya), QPointF(x + nx_arrow * self.arrowhead_size, y + ny_arrow * self.arrowhead_size), QPointF(x - nx_arrow * self.arrowhead_size, y - ny_arrow * self.arrowhead_size)) # draw only one of the two vertices, if the graph is undirected if self.graph.is_directed() or id(node) < id(neighbour): painter.drawLine(QPointF(x1, y1), QPointF(x2, y2)) if self.graph.is_weighted(): x_middle, y_middle = (x2 + x1) / 2, (y2 + y1) / 2 # if the graph is directed, the vertices are offset (so they aren't draw on top of each other), # so we need to shift them back to be at the midpoint between the nodes if self.graph.is_directed(): x_middle -= ux * r * (1 - cos(self.arrow_separation)) y_middle -= uy * r * (1 - cos(self.arrow_separation)) r = self.weight_rectangle_size self.vertex_positions.append( (x_middle, y_middle, (node, neighbour))) # make the selected vertex rectangle background different, if it's selected (for aesthetics) if self.selected_vertex is not None and node is self.selected_vertex[0] and neighbour is \ self.selected_vertex[1]: painter.setBrush( QBrush(self.selected_color, Qt.SolidPattern)) else: painter.setBrush( QBrush(self.regular_vertex_weight_color, Qt.SolidPattern)) # draw the square square_rectangle = QRectF(x_middle - r, y_middle - r, 2 * r, 2 * r) painter.drawRect(square_rectangle) # adjust the length of the weight string, so the minus sign doesn't make the number smaller length = len(str(weight)) - (1 if weight < 0 else 0) painter.setFont( QFont(self.font_family, self.font_size / (length * 3))) # draw the value of the vertex (in white, so it's visible against the background) painter.setPen(QPen(Qt.white, Qt.SolidLine)) painter.drawText(square_rectangle, Qt.AlignCenter, str(weight)) painter.setPen(QPen(Qt.black, Qt.SolidLine)) # draw nodes for node in self.graph.get_nodes(): # selected nodes are red to make them distinct; others are white if node is self.selected_node: painter.setBrush(QBrush(self.selected_color, Qt.SolidPattern)) else: painter.setBrush( QBrush(self.regular_node_color, Qt.SolidPattern)) x, y, r = node.get_x(), node.get_y(), node.get_radius() painter.drawEllipse(QPointF(x, y), r, r) # only draw labels if the label checkbox is checked if self.labels_checkbox.isChecked(): label = node.get_label() # scale font down, depending on the length of the label of the node painter.setFont( QFont(self.font_family, self.font_size / len(label))) # draw the node label within the node dimensions painter.drawText(QRectF(x - r, y - r, 2 * r, 2 * r), Qt.AlignCenter, label) def distance(self, x1, y1, x2, y2): """Returns the distance of two points in space.""" return sqrt((x1 - x2)**2 + (y1 - y2)**2)