class LeftFrame(QFrame): def __init__(self, father, top): super().__init__(father) self.setObjectName('api_frame') self.father, self.top = father, top self.setGeometry(0, 0, 150, 300) self.all_api = self.top.all_api[1:] self.now_api = self.all_api[0] self.inside_frame = QFrame(self) self.inside_frame.resize(self.width(), len(self.all_api)*55+5) self.inside_frame.move(0, 0) self.inside_frame.setObjectName('inside') for idx in range(len(self.all_api)): label = ApiLabel(self.all_api[idx], self, self.top) label.move(7, idx*55+5) def wheelEvent(self, e): if self.inside_frame.height() > self.height(): if e.angleDelta().y() > 0: self.inside_frame.move(0, self.inside_frame.y() + 60) if self.inside_frame.y() > 0: self.inside_frame.move(0, 0) else: self.inside_frame.move(0, self.inside_frame.y() - 60) if self.inside_frame.y() < self.height()-self.inside_frame.height(): self.inside_frame.move(0, self.height()-self.inside_frame.height()) self.father.list_scroll.bar.setValue(abs(self.inside_frame.y()))
class Demo(QWidget): def __init__(self): super(Demo, self).__init__() self.resize(500, 400) self.label = QLineEdit(self) self.label.setGeometry(0, 10, 450, 38) self.frame = QFrame(self) self.frame.setGeometry(120, 120, 100, 100) self.frame.setStyleSheet('background-color : #B9F9C5') print(self.frame.width()) print(self.frame.height()) print(self.frame.pos()) self.setAcceptDrops(True) def dragEnterEvent(self, a0: QtGui.QDragEnterEvent) -> None: self.setWindowTitle('mouse in') print(a0.mimeData().text()) a0.accept() def dragMoveEvent(self, a0: QtGui.QDragMoveEvent) -> None: print(a0.pos()) # pass def dropEvent(self, a0: QtGui.QDropEvent) -> None: self.setWindowTitle('mouse drop') if self.frame.pos().x() <= a0.pos().x() <= self.frame.pos().x() + self.frame.width() \ and self.frame.pos().y() <= a0.pos().y() <= self.frame.pos().y() + self.frame.height(): self.label.setText(a0.mimeData().text().replace('file:///', '')) print(a0.mimeData().text())
class RightFrame(QFrame): def __init__(self, father, top): super().__init__(father) self.setObjectName('tag_frame') self.father, self.top = father, top self.resize(322, 300) self.inside_frame = QFrame(self) self.inside_frame.setObjectName('right_inside') self.inside_frame.move(0, 0) self.now_api = None self.init() def init(self): self.now_api = eval('self.top.%s' % self.father.list.now_api) for c in self.inside_frame.children(): delete(c) self.inside_frame.resize( self.width(), ceil(len(self.now_api.cate[1:]) / ((self.width()) // 95)) * 45) self.inside_frame.move(0, 0) for idx in range(len(self.now_api.cate[1:])): label = TagLabel(self.now_api.cate[1:][idx], self, self.top) label.move(95 * (idx % (self.width() // 95)), (idx // (self.width() // 95)) * 45) def wheelEvent(self, e): if self.inside_frame.height() > self.height(): if e.angleDelta().y() > 0: self.inside_frame.move(0, self.inside_frame.y() + 60) if self.inside_frame.y() > 0: self.inside_frame.move(0, 0) else: self.inside_frame.move(0, self.inside_frame.y() - 60) if self.inside_frame.y( ) < self.height() - self.inside_frame.height(): self.inside_frame.move( 0, self.height() - self.inside_frame.height()) self.father.tags_scroll.bar.setValue(abs(self.inside_frame.y())) def resizeEvent(self, e): self.inside_frame.move(0, 0) self.inside_frame.resize( self.width(), ceil(len(self.now_api.cate[1:]) / ((self.width()) // 95)) * 45) self.father.tags_scroll.mid_frame.setGeometry( 0, 0, 0, self.inside_frame.height()) a_line = self.width() // 95 for c in range(len(self.inside_frame.children())): self.inside_frame.children()[c].move( 95 * (c % a_line) + (self.width() - a_line * 95) * (c % a_line) / a_line, 45 * (c // a_line) + 5)
def __init__(self, items: t_.Sequence[str], parent=None): super().__init__(parent=parent) # self.setFrameStyle(QFrame.Panel) self._bGroup = QButtonGroup(self) l = QHBoxLayout(self) for i, itemName in enumerate(items): b = QPushButton(itemName, parent=self) b.setCheckable(True) # Toggleable self._bGroup.addButton(b, id=i) l.addWidget(b) self._selectedButtonId = self._bGroup.id(self._bGroup.buttons()[0]) self._bGroup.buttons()[0].click( ) # Make sure at least one button is selected. self._bGroup.buttonClicked.connect(self._buttonSelected) l.setSpacing(1) # Move buttons close together l.setContentsMargins(0, 0, 0, 0) w = QFrame(self) w.setFrameStyle(QFrame.Box) w.setLayout(l) scrollArea = QScrollArea(parent=self) scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) scrollArea.setStyleSheet("""QScrollBar:horizontal { height:10px; }""") scrollArea.setWidget(w) scrollArea.setFixedHeight(10 + w.height()) ll = QHBoxLayout() ll.setContentsMargins(0, 0, 0, 0) ll.addWidget(scrollArea) self.setLayout(ll)
class QShadowFrame(QFrame): def __init__(self, master=None, only_pressed=True): super().__init__(master) self.only_pressed = only_pressed self._be_pressing = False self.build_inter() def build_inter(self): self.init_images() self.main_frame = QFrame() self.main_frame.setAlignment(Qt.AlignCenter) self.main_frame.setSizePolicy(self.sizePolicy()) self.main_frame.setContentsMargins(0, 0, 0, 0) self.left_lb = QLabel(self) self.setContentsMargins(0, 0, 0, 0) def init_images(self): self._left = Image.open(os.path.join(paths.IMAGE, "left_shadow.png")) self._right = left.rotate(180) self._top = Image.open(os.path.join(paths.IMAGE, "top_shadow.png")) self._bottom = top.rotate(180) self.left = self._left.toqpixmap() self.left.fill() self.right = self._right.toqpixmap() self.right.fill() self.top = self._top.toqpixmap() self.top.fill() self.bottom = self._bottom.toqpixmap() self.bottom.fill() self.adjust_size() def adjust_size(self): self.left = self.left.scaled(self.left.width(), self.main_frame.height() + 2 * self.top.height()) self.right = self.right.scaled(self.left.width(), self.left.height()) self.top = self.top.scaled(self.main_frame.width(), self.top.height()) self.bottom = self.bottom.scaled(self.top.width(), self.top.height()) def update_lb(self): self.left_lb.setPixmap(self.left) self.right_lb.setPixmap(self.right) self.top_lb.setPixmap(self.top) self.bottom.setPixmap(self.bottom) def mousePressEvent(self, *args, **kwargs): pass def resizeEvent(self): if self.only_pressed and not self._be_pressing: return self.adjust_size() self.update_lb()
class ProgramWindow(QtWidgets.QMainWindow): """ How to increase QFrame.HLine line separator width and distance with the other buttons? https://stackoverflow.com/questions/50825126/how-to-increase-qframe-hline-line-separator-width-and-distance-with-the-other-bu """ def __init__(self): QtWidgets.QMainWindow.__init__( self ) self.setup_main_window() self.create_input_text() self.set_window_layout() def setup_main_window(self): self.resize( 400, 300 ) self.centralwidget = QWidget() self.setCentralWidget( self.centralwidget ) def create_input_text(self): self.separatorLine = QFrame() self.separatorLine.setFrameShape( QFrame.HLine ) self.separatorLine.setFrameShadow( QFrame.Raised ) self.separatorLine.setLineWidth( 150 ) self.separatorLine.setMidLineWidth( 150 ) rect = self.separatorLine.frameRect() print( "frameShape: %s" % rect ) print( "width: %s" % self.separatorLine.width() ) print( "height: %s" % self.separatorLine.height() ) self.redoButton = QPushButton( "Redo Operations" ) self.calculate = QPushButton( "Compute and Follow" ) self.open = QPushButton( "Open File" ) self.save = QPushButton( "Save File" ) self.verticalGridLayout = QGridLayout() self.verticalGridLayout.addWidget( self.redoButton , 1 , 0) self.verticalGridLayout.addWidget( self.calculate , 2 , 0) self.verticalGridLayout.addWidget( self.separatorLine , 3 , 0) self.verticalGridLayout.addWidget( self.open , 4 , 0) self.verticalGridLayout.addWidget( self.save , 5 , 0) self.verticalGridLayout.setSpacing( 0 ) self.verticalGridLayout.setRowMinimumHeight(3, 20) self.verticalGridLayout.setAlignment(Qt.AlignTop) self.innerLayout = QHBoxLayout() self.innerLayout.addLayout( self.verticalGridLayout ) def set_window_layout(self): main_vertical_layout = QVBoxLayout( self.centralwidget ) main_vertical_layout.addLayout( self.innerLayout )
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 DataStatistics(QDialog): def __init__(self, parent=None): super(DataStatistics, self).__init__(parent) self.initData() self.initModule() self.setModule() self.funcLink() self.drawGraph() self.analyzeSingleMemo() def initModule(self): self.setWindowTitle(u'数据统计') self.setWindowIcon(QIcon(css.dataBtnPath)) self.resize(500, 500) pg.setConfigOptions(foreground=QColor(113, 148, 116), antialias=True) self.btnframe = QFrame(self) self.mainframe = QFrame(self) self.btngroup = QButtonGroup(self.btnframe) self.stacklayout = QStackedLayout(self.mainframe) self.btn1 = QToolButton(self.btnframe) self.btn2 = QToolButton(self.btnframe) self.btngroup.addButton(self.btn1, 1) self.btngroup.addButton(self.btn2, 2) self.frame1 = QMainWindow() self.frame1_bar = QStatusBar() self.frame1.setStatusBar(self.frame1_bar) self.frame1_bar.showMessage(self.date + ': ' + str(readFinishRate(self.date)[2] * 100) + '%') self.frame2 = QMainWindow() self.frame2_bar = QStatusBar() self.frame2.setStatusBar(self.frame2_bar) self.frame2_bar.showMessage("坚持,就是每一天很难,可一年一年越来越容易。") self.stacklayout.addWidget(self.frame1) self.stacklayout.addWidget(self.frame2) def setModule(self): self.btnframe.setGeometry(0, 0, self.width(), 35) self.btnframe.setStyleSheet("border-color: rgb(0, 0, 0);") self.btnframe.setFrameShape(QFrame.Panel) self.btnframe.setFrameShadow(QFrame.Raised) self.mainframe.setGeometry(0, 35, self.width(), self.height() - self.btnframe.height()) self.btn1.setCheckable(True) self.btn1.setText("整体完成率") self.btn1.resize(100, 35) self.btn2.setCheckable(True) self.btn2.setText("单条完成情况") self.btn2.resize(100, 35) self.btn2.move(self.btn1.width(), 0) def funcLink(self): self.btn1.clicked.connect(self.showFrame1) self.btn2.clicked.connect(self.showFrame2) def initData(self): date = QDate.currentDate() self.date = date.toString(Qt.ISODate) self.statistics = readStatistics(css.statistics, self.date) def drawGraph(self): self.myplot = pg.PlotWidget(self.frame1, title='每日任务完成率') self.frame1.setCentralWidget(self.myplot) x = [] for key in self.statistics.keys(): x.append(key) points = [] for key in x: points.append(readFinishRate(key)[2]) tick_b = [list(zip(range(len(x)), x))] bottom = self.myplot.getAxis('bottom') bottom.setTicks(tick_b) self.myplot.setBackground((210, 240, 240)) # 背景色 self.myplot.showGrid(y=True) pen = pg.mkPen({'color': (155, 200, 160), 'width': 4}) # 画笔设置 self.myplot.plot(points[0:], clear=True, pen=pen, symbol='o', symbolBrush=QColor(113, 148, 116)) def analyzeSingleMemo(self): #data extract data = read(css.userdata) self.content = [] self.set_date = [] self.if_done = [] if data['memo_data']: for memo in data['memo_data']: self.content.append(memo['content']) self.set_date.append(memo['set_date']) self.if_done.append(memo['if_done']) #UI init self.ui = QWidget(self.frame2) self.option = QComboBox(self.ui) self.label0 = QLabel(self.ui) self.label1 = QLabel(self.ui) self.label2 = QLabel(self.ui) self.label3 = QLabel(self.ui) self.frame2.setCentralWidget(self.ui) #UI set self.option.setGeometry(10, 95, self.width() - 100, 40) self.option.setStyleSheet(css.combobox_style) self.option.addItems(self.content) self.option.currentIndexChanged.connect(self.getMemoMessage) self.label0.setGeometry(10, 5, self.width() - 100, 80) self.label1.setGeometry(10, 145, self.width() - 100, 80) self.label2.setGeometry(10, 235, self.width() - 100, 80) self.label3.setGeometry(10, 325, self.width() - 100, 80) self.label0.setStyleSheet(css.label_style) self.label1.setStyleSheet(css.label_style) self.label2.setStyleSheet(css.label_style) self.label3.setStyleSheet(css.label_style) self.label0.setText('选择需要查看的memo:') self.label1.setText('设立已 ' + str(getDateDiffer(self.set_date[0], self.date)) + ' 天') self.label2.setText('已完成 ' + str(len(self.if_done[0])) + ' 天') self.label3.setText('最大连续完成 ' + str(len(getLongest(self.if_done[0]))) + ' 天') def getMemoMessage(self): self.label1.setText('设立已 ' + str( getDateDiffer(self.set_date[self.option.currentIndex()], self.date)) + ' 天') self.label2.setText( '已完成 ' + str(len(self.if_done[self.option.currentIndex()])) + ' 天') self.label3.setText( '最大连续完成 ' + str(len(getLongest(self.if_done[self.option.currentIndex()]))) + ' 天') def showFrame1(self): if self.stacklayout.currentIndex() != 0: self.stacklayout.setCurrentIndex(0) def showFrame2(self): if self.stacklayout.currentIndex() != 1: self.stacklayout.setCurrentIndex(1)
class P2(QWidget): def __init__(self): """Initial game configuration.""" super().__init__() # GAME VARIABLES # valid moves for the king self.valid_moves = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] # starting and ending tiles self.start = None self.end = None # a boolean array, where True values are obstacles/pieces self.obstacles = [[False] * 8 for _x in range(8)] # WIDGET LAYOUT self.canvas = QFrame(self, minimumSize=QSize(600, 600)) self.main_v_layout = QVBoxLayout(self, margin=0) self.main_v_layout.addWidget(self.canvas) self.setLayout(self.main_v_layout) self.setWindowTitle('Graph Visualizer') self.show() self.setFixedSize(self.size()) def are_coordinates_valid(self, x, y): """Returns True, if the coordinates are valid board coordinates and False if they are not.""" return 0 <= x < len(self.obstacles) and 0 <= y < len(self.obstacles[0]) def calculate_shortest_path(self): """Returns either the shortest possible path connecting (and including) the start and the end , or None if such path doesn't exist.""" # don't calculate the path if either start or end are unknown if self.start is None or self.end is None: return None # squares to be explored (a linked list unexplored_squares = [self.start] # for tracking where we came from board = [[None] * len(self.obstacles[0]) for _x in range(len(self.obstacles))] board[self.start[0]][self.start[1]] = -1 # special value, so it isn't explored # explore, until there are tiles to explore while len(unexplored_squares) != 0: x, y = unexplored_squares.pop(0) # if we reached the end, back-track to start; if not, add unexplored tiles and repeat if x == self.end[0] and y == self.end[1]: path = [(x, y)] coordinate = (x, y) # add coordinates to the path list, until we reach the start while coordinate != self.start: coordinate = board[coordinate[0]][coordinate[1]] path.append(coordinate) return path else: for move in self.valid_moves: new_x = x + move[0] new_y = y + move[1] # add the coordinate, only if it's valid, unexplored, and not an obstacle if self.are_coordinates_valid(new_x, new_y): unexplored = board[new_x][new_y] is None no_obstacle = not self.obstacles[new_x][new_y] if unexplored and no_obstacle: unexplored_squares.append((new_x, new_y)) board[new_x][new_y] = (x, y) def mouseMoveEvent(self, event): """Is called when a mouse button is pressed; creates start, end and obstacles.""" mouse_x, mouse_y = event.pos().x(), event.pos().y() x = int((mouse_x / self.canvas.width()) * len(self.obstacles)) y = int((mouse_y / self.canvas.height()) * len(self.obstacles[0])) if self.are_coordinates_valid(x, y): if event.buttons() == Qt.LeftButton and not self.obstacles[x][y]: self.start = (x, y) if event.buttons() == Qt.RightButton and not self.obstacles[x][y]: self.end = (x, y) if event.buttons() == Qt.MiddleButton and (x, y) != self.start and (x, y) != self.end: self.obstacles[x][y] = True self.update() # make mouse press the same thing as mouse move mousePressEvent = mouseMoveEvent 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.drawRect(0, 0, self.canvas.width(), self.canvas.height()) tile_width = self.canvas.width() / len(self.obstacles) tile_height = self.canvas.height() / len(self.obstacles[0]) # draw obstacles for x in range(len(self.obstacles)): for y in range(len(self.obstacles[0])): if self.obstacles[x][y]: painter.setBrush(QBrush(Qt.black, Qt.SolidPattern)) else: painter.setBrush(QBrush(Qt.white, Qt.SolidPattern)) painter.drawRect(x * tile_width, y * tile_height, tile_width, tile_height) # draw path if it exists path = self.calculate_shortest_path() if path is not None: painter.setBrush(QBrush(Qt.green, Qt.SolidPattern)) for p in path: painter.drawRect(p[0] * tile_width, p[1] * tile_height, tile_width, tile_height) # draw start if it exists if self.start is not None: painter.setBrush(QBrush(Qt.blue, Qt.SolidPattern)) painter.drawRect(self.start[0] * tile_width, self.start[1] * tile_height, tile_width, tile_height) # draw end if it exists if self.end is not None: painter.setBrush(QBrush(Qt.red, Qt.SolidPattern)) painter.drawRect(self.end[0] * tile_width, self.end[1] * tile_height, tile_width, tile_height)
class RunFrame(QFrame): def __init__(self, q_main_window): super(RunFrame, self).__init__() # Split the frame self.split_frame = QVBoxLayout(self) self.output_frame = QFrame() self.button_frame = QFrame() self.split_frame.addWidget(self.output_frame, 2) self.split_frame.addWidget(self.button_frame) self.output = QTextEdit(self.output_frame) self.output.setReadOnly(True) # print(self.output.isReadOnly()) # self.output_cursor = QTextCursor(self.output) self.button_layout = QHBoxLayout(self.button_frame) # Store the parent self.q_main_window = q_main_window # Define Layout self.define_settings() self.define_text_output() # Running # self.stop_running = False self.m2m = Makr2Maker(self.q_main_window) def update_text_listener(self, is_same, new_string): if is_same == '0': self.new_text(new_string) def new_text(self, text): self.output.setText(text) QApplication.processEvents() def define_settings(self): self.define_controls() def define_controls(self): def add_button(self, text, callback): button = QPushButton(self.button_frame) button.clicked.connect(callback) button.setText(text) self.button_layout.addWidget(button) return button self.bt_validate = add_button(self, 'Validate', self.validate) self.bt_run = add_button(self, 'Run', self.run) self.bt_interrupt = add_button(self, 'Interrupt', self.interrupt) self.bt_clear = add_button(self, 'Clear Output', self.clear_output) self.bt_remove_results = add_button(self, 'Remove Results', self.remove_results) def interrupt(self): self.q_main_window.is_interrupting = True self.q_main_window.statusBar.showMessage('Interrupting') self.q_main_window.match_maker.stopSearch() self.q_main_window.match_maker.is_running = False self.q_main_window.statusBar.showMessage('Matchmaking Interrupted') self.q_main_window.is_interrupting = False def validate(self): self.q_main_window.statusBar.showMessage('Validating...') self.m2m.apply_settings() t = threading.Thread(target=self.q_main_window.match_maker.validate) t.start() def run(self): self.m2m.apply_settings() t = threading.Thread(target=self.q_main_window.match_maker.main) t.start() # self.q_main_window.is_running = False def clear_output(self): self.output.setText('') def remove_results(self): working_dir = self.q_main_window.settings_frame.tb_path.text() results_dir = self.q_main_window.settings_frame.tb_results_dir.text() self.dir_to_remove = path.join(working_dir, results_dir) def callback(button_pressed): if button_pressed.text() == '&OK': try: shutil.rmtree(self.dir_to_remove) print('Directory Removed: ' + self.dir_to_remove) except: print('Directory not found: ' + self.dir_to_remove) else: return dialog = QMessageBox() dialog.setIcon(QMessageBox.Critical) dialog.setText( 'You are about to delete the results of the optimization.') dialog.setInformativeText(self.dir_to_remove) dialog.setWindowTitle('Delete Optimization Results?') dialog.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) dialog.buttonClicked.connect(callback) dialog.exec_() def define_text_output(self): self.output.setReadOnly(True) self.resize_text_output() def resize_text_output(self): frame_width = self.output_frame.width() frame_height = self.output_frame.height() self.output.resize(frame_width, frame_height)
class Demo(QWidget): def __init__(self): super().__init__() self.__setup_ui__() def __setup_ui__(self): self.setWindowTitle("测试") #窗口大小 self.resize(1400, 800) # 工具栏 self.frame_tool = QFrame(self) self.frame_tool.setObjectName("frame_tool") self.frame_tool.setGeometry(0, 0, self.width(), 25) self.frame_tool.setStyleSheet("border-color: rgb(0, 0, 0);") self.frame_tool.setFrameShape(QFrame.Panel) self.frame_tool.setFrameShadow(QFrame.Raised) # 1.1 界面1按钮 self.window1_btn = QToolButton(self.frame_tool) self.window1_btn.setCheckable(True) self.window1_btn.setText("window1") self.window1_btn.setObjectName("menu_btn") self.window1_btn.resize(100, 25) self.window1_btn.clicked.connect(self.click_window1) self.window1_btn.setAutoRaise(True) # 1.2 界面2按钮 self.window2_btn = QToolButton(self.frame_tool) self.window2_btn.setCheckable(True) self.window2_btn.setText("window2") self.window2_btn.setObjectName("menu_btn") self.window2_btn.resize(100, 25) self.window2_btn.move(self.window1_btn.width(), 0) self.window2_btn.clicked.connect(self.click_window2) self.window2_btn.setAutoRaise(True) self.btn_group = QButtonGroup(self.frame_tool) self.btn_group.addButton(self.window1_btn, 1) self.btn_group.addButton(self.window2_btn, 2) # 2. 工作区域 self.main_frame = QFrame(self) self.main_frame.setGeometry(0, 25, self.width(), self.height() - self.frame_tool.height()) # self.main_frame.setStyleSheet("background-color: rgb(65, 95, 255)") # 创建堆叠布局 self.stacked_layout = QStackedLayout(self.main_frame) # 第一个布局界面 self.main_frame1 = QMainWindow() self.frame1_bar = QStatusBar() self.frame1_bar.setObjectName("frame1_bar") self.main_frame1.setStatusBar(self.frame1_bar) self.frame1_bar.showMessage("欢迎进入frame1") rom_frame = QFrame(self.main_frame1) rom_frame.setGeometry(0, 0, self.width(), self.main_frame.height() - 25) rom_frame.setFrameShape(QFrame.Panel) rom_frame.setFrameShadow(QFrame.Raised) frame1_bar_frame = QFrame(self.main_frame1) frame1_bar_frame.setGeometry(0, self.main_frame.height(), self.width(), 25) # 第二个布局界面 self.main_frame2 = QMainWindow() self.frame2_bar = QStatusBar() self.frame2_bar.setObjectName("frame2_bar") self.main_frame2.setStatusBar(self.frame2_bar) self.frame2_bar.showMessage("欢迎进入frame2") custom_frame = QFrame(self.main_frame2) custom_frame.setGeometry(0, 0, self.width(), self.main_frame.height() - 25) custom_frame.setFrameShape(QFrame.Panel) custom_frame.setFrameShadow(QFrame.Raised) frame2_bar_frame = QFrame(self.main_frame2) frame2_bar_frame.setGeometry(0, self.main_frame.height(), self.width(), 25) # 把两个布局界面放进去 self.stacked_layout.addWidget(self.main_frame1) self.stacked_layout.addWidget(self.main_frame2) def click_window1(self): if self.stacked_layout.currentIndex() != 0: self.stacked_layout.setCurrentIndex(0) self.frame1_bar.showMessage("欢迎进入frame1") def click_window2(self): if self.stacked_layout.currentIndex() != 1: self.stacked_layout.setCurrentIndex(1) self.frame2_bar.showMessage("欢迎进入frame2")
class Demo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("测试") # 窗口大小 self.resize(1400, 800) # self.setFixedSize(1500, 600) # 设置窗口为固定尺寸, 此时窗口不可调整大小 # self.setMinimumSize(1800, 1000) # 设置窗口最大尺寸 # self.setMaximumSize(900, 300) # 设置窗口最小尺寸 # self.setWindowFlag(Qt.WindowStaysOnTopHint) # 设置窗口顶层显示 # self.setWindowFlags(Qt.FramelessWindowHint) # 设置无边框窗口样式,不显示最上面的标题栏 self.content_font = QFont("微软雅黑", 12, QFont.Medium) # 定义字体样式 self.center() self.__setup_ui__() # 控制窗口显示在屏幕中心的方法 def center(self): # 获得窗口 qr = self.frameGeometry() # 获得屏幕中心点 cp = QDesktopWidget().availableGeometry().center() # 显示到屏幕中心 qr.moveCenter(cp) self.move(qr.topLeft()) # 关闭窗口的时候,触发了QCloseEvent,需要重写closeEvent()事件处理程序,这样就可以弹出是否退出的确认窗口 def closeEvent(self, event): reply = QMessageBox.question( self, "退出程序", # 提示框标题 "确定退出xxxx程序吗?", # 消息对话框中显示的文本 QMessageBox.Yes | QMessageBox.No, # 指定按钮的组合 Yes和No QMessageBox.No # 默认的按钮焦点,这里默认是No按钮 ) # 判断按钮的选择 if reply == QMessageBox.Yes: event.accept() else: event.ignore() def __setup_ui__(self): # 工具栏 self.frame_tool = QFrame(self) self.frame_tool.setObjectName("frame_tool") self.frame_tool.setGeometry(0, 0, self.width(), 25) self.frame_tool.setStyleSheet("border-color: rgb(0, 0, 0);") self.frame_tool.setFrameShape(QFrame.Panel) self.frame_tool.setFrameShadow(QFrame.Raised) # 1.1 界面1按钮 self.window1_btn = QToolButton(self.frame_tool) self.window1_btn.setCheckable(True) self.window1_btn.setText("window1") self.window1_btn.setObjectName("menu_btn") self.window1_btn.resize(100, 25) self.window1_btn.clicked.connect(self.click_window1) self.window1_btn.setAutoRaise( True) # 去掉工具按钮的边框线如果是QPushButton按钮的话,就是用setFlat(True)这个方法,用法相同 # 1.2 界面2按钮 self.window2_btn = QToolButton(self.frame_tool) self.window2_btn.setCheckable(True) self.window2_btn.setText("window2") self.window2_btn.setObjectName("menu_btn") self.window2_btn.resize(100, 25) self.window2_btn.move(self.window1_btn.width(), 0) self.window2_btn.clicked.connect(self.click_window2) self.window2_btn.setAutoRaise(True) self.btn_group = QButtonGroup(self.frame_tool) self.btn_group.addButton(self.window1_btn, 1) self.btn_group.addButton(self.window2_btn, 2) # 1.3 帮助下拉菜单栏 # 创建帮助工具按钮 help_btn = QToolButton(self.frame_tool) help_btn.setText("帮助") help_btn.setObjectName("menu_btn") help_btn.resize(100, 25) help_btn.move(self.window2_btn.x() + self.window2_btn.width(), 0) help_btn.setAutoRaise(True) help_btn.setPopupMode(QToolButton.InstantPopup) # 创建关于菜单 help_menu = QMenu("帮助", self.frame_tool) feedback_action = QAction(QIcon("xxx.png"), "反馈", help_menu) feedback_action.triggered.connect(self.click_feedback) about_action = QAction(QIcon("xxx.png"), "关于", help_menu) about_action.triggered.connect(self.click_about) # 把两个QAction放入help_menu help_menu.addAction(feedback_action) help_menu.addAction(about_action) # 把help_menu放入help_btn help_btn.setMenu(help_menu) # 2. 工作区域 self.main_frame = QFrame(self) self.main_frame.setGeometry(0, 25, self.width(), self.height() - self.frame_tool.height()) # self.main_frame.setStyleSheet("background-color: rgb(65, 95, 255)") # 创建堆叠布局 self.stacked_layout = QStackedLayout(self.main_frame) # 第一个布局 self.main_frame1 = QMainWindow() self.frame1_bar = QStatusBar() self.frame1_bar.setObjectName("frame1_bar") self.main_frame1.setStatusBar(self.frame1_bar) self.frame1_bar.showMessage("欢迎进入frame1") rom_frame = QFrame(self.main_frame1) rom_frame.setGeometry(0, 0, self.width(), self.main_frame.height() - 25) rom_frame.setFrameShape(QFrame.Panel) rom_frame.setFrameShadow(QFrame.Raised) # 超链接 self.super_link = QLabel(rom_frame) self.super_link.setText(""" 超链接: <a href="https://blog.csdn.net/s_daqing">点击打开查看</a> """) self.super_link.setGeometry(20, 30, 300, 25) self.super_link.setFont(self.content_font) # 使用字体样式 self.super_link.setOpenExternalLinks(True) # 使其成为超链接 self.super_link.setTextInteractionFlags( Qt.TextBrowserInteraction) # 双击可以复制文本 self.start_btn = QPushButton("开 始", rom_frame) self.start_btn.setGeometry(self.width() * 0.7, self.height() * 0.8, 100, 40) # self.start_btn.clicked.connect(self.start_btn_click) self.quit_btn = QPushButton("退 出", rom_frame) self.quit_btn.setGeometry(self.width() * 0.85, self.height() * 0.8, 100, 40) self.quit_btn.setStatusTip("点击关闭程序") # self.quit_btn.clicked.connect(QCoreApplication.instance().quit) # 点击退出可以直接退出 self.quit_btn.clicked.connect(self.close) # 点击退出按钮的退出槽函数 #rom_frame1 = QFrame() #rom_frame1.setFrameShape(QFrame.Panel) #rom_frame1.setFrameShadow(QFrame.Raised) #rom_frame2 = QFrame() #rom_frame2.setFrameShape(QFrame.Panel) #rom_frame2.setFrameShadow(QFrame.Raised) # 创建布局管理器 self.layout1 = QBoxLayout(QBoxLayout.TopToBottom) # 给管理器对象设置父控件 rom_frame.setLayout(self.layout1) self.main_frame1.setCentralWidget(rom_frame) # 把子控件添加到布局管理器中 #self.layout1.addWidget(rom_frame1, 1) #self.layout1.addWidget(rom_frame2, 1) self.layout1.setContentsMargins(0, 0, 0, 0) # 设置布局的左上右下外边距 self.layout1.setSpacing(0) # 设置子控件的内边距 frame1_bar_frame = QFrame(self.main_frame1) frame1_bar_frame.setGeometry(0, self.main_frame.height(), self.width(), 25) # 第二个布局 self.main_frame2 = QMainWindow() self.frame2_bar = QStatusBar() self.frame2_bar.setObjectName("frame2_bar") self.main_frame2.setStatusBar(self.frame2_bar) self.frame2_bar.showMessage("欢迎进入frame2") custom_frame = QFrame(self.main_frame2) custom_frame.setGeometry(0, 0, self.width(), self.main_frame.height() - 25) custom_frame.setFrameShape(QFrame.Panel) custom_frame.setFrameShadow(QFrame.Raised) custom_frame1 = QFrame() custom_frame1.setFrameShape(QFrame.Panel) custom_frame1.setFrameShadow(QFrame.Raised) custom_frame2 = QFrame() custom_frame2.setFrameShape(QFrame.Panel) custom_frame2.setFrameShadow(QFrame.Raised) custom_frame3 = QFrame() custom_frame3.setFrameShape(QFrame.Panel) custom_frame3.setFrameShadow(QFrame.Raised) # 创建布局管理器 self.layout2 = QBoxLayout(QBoxLayout.TopToBottom) # 给管理器对象设置父控件 custom_frame.setLayout(self.layout2) """ 使用了父类为QMainWindow的话,在里面使用布局类,QGridLayout, QHBoxLayout ,QVBoxLayout 等等时,发现不好用, 加上下面这句代码就可以了,QMainWindow对象.setCentralWidget(这里填布局管理器的父控件对象) """ self.main_frame2.setCentralWidget(custom_frame) # 把子控件添加到布局管理器中 self.layout2.addWidget(custom_frame1, 1) self.layout2.addWidget(custom_frame2, 1) self.layout2.addWidget(custom_frame3, 1) self.layout2.setContentsMargins(0, 0, 0, 0) # 设置布局的左上右下外边距 self.layout2.setSpacing(0) # 设置子控件的内边距 frame2_bar_frame = QFrame(self.main_frame2) frame2_bar_frame.setGeometry(0, self.main_frame.height(), self.width(), 25) # 把两个布局放进去 self.stacked_layout.addWidget(self.main_frame1) self.stacked_layout.addWidget(self.main_frame2) def click_window1(self): if self.stacked_layout.currentIndex() != 0: self.stacked_layout.setCurrentIndex(0) self.frame1_bar.showMessage("欢迎进入frame1") def click_window2(self): if self.stacked_layout.currentIndex() != 1: self.stacked_layout.setCurrentIndex(1) self.frame2_bar.showMessage("欢迎进入frame2") QDesktopServices.openUrl(QUrl("https://www.csdn.net/") ) # 点击window2按钮后,执行这个槽函数的时候,会在浏览器自动打开这个网址 def click_feedback(self, event): QMessageBox.about(self, "反馈", "使用过程中如有疑问,请联系:xxxx.163.com\r\n\r\n版本:V1.0.1") def click_about(self, event): QMessageBox.about(self, "关于", "使用文档,请参考:xxxxxx")
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Tree Viewer v2") self.setGeometry(0,0,800,600) self.setObjectName("Layer1") self.offsets = [0, 35, 0, 2] self.removeThread = None self.searchThread = None self.addThread = None self.selected = None self.knotsSize = 30 self.fontSize = 10 self.windowError = QMessageBox() self.windowError.setIcon(QMessageBox.Information) self.windowError.setObjectName("Error") self.setUI() def setUI(self): s = QGraphicsDropShadowEffect() s.setColor(QColor("#000000")) s.setBlurRadius(5) s.setOffset(1,1) self.frameTree = QFrame(self) self.frameTree.setGeometry(0, 0, 600, 600) self.defaultTree() self.frameInfo = QFrame(self) self.frameInfo.setGeometry(605, 5, 190, 590) self.frameInfo.setObjectName("Layer2") self.frameInfo.setGraphicsEffect(s) self.labelAddKnot = QLabel("Add Knot:", self.frameInfo) self.labelAddKnot.setGeometry(10, 150, 60, 30) self.labelAddKnot.setAlignment(Qt.AlignCenter) self.labelAddKnot.setObjectName("Layer2NoBG") self.labelAddKnot.setFont(QFont("Arial", 15)) self.entryAddKnot = QLineEdit(self.frameInfo) self.entryAddKnot.setGeometry(120, 150, 60, 30) self.entryAddKnot.setAlignment(Qt.AlignCenter) self.entryAddKnot.setObjectName("Layer2") self.entryAddKnot.setFont(QFont("Arial", 15)) self.entryAddKnot.setValidator(QRegExpValidator(QRegExp("[0-9]+"), self.entryAddKnot)) self.buttonAddKnot = QPushButton(QApplication.style().standardIcon(QStyle.SP_DialogApplyButton), '', self.frameInfo) self.buttonAddKnot.setGeometry(180, 150, 60, 30) self.buttonAddKnot.setObjectName("Layer2") self.buttonAddKnot.clicked.connect(self.addKnot) self.labelVisualizeKnot = QLabel("Search for:", self.frameInfo) self.labelVisualizeKnot.setGeometry(10, 300, 60, 30) self.labelVisualizeKnot.setAlignment(Qt.AlignCenter) self.labelVisualizeKnot.setObjectName("Layer2NoBG") self.labelVisualizeKnot.setFont(QFont("Arial", 15)) self.entryVisualizeKnot = QLineEdit(self.frameInfo) self.entryVisualizeKnot.setGeometry(70, 300, 60, 30) self.entryVisualizeKnot.setAlignment(Qt.AlignCenter) self.entryVisualizeKnot.setObjectName("Layer2") self.entryVisualizeKnot.setFont(QFont("Arial", 15)) self.entryVisualizeKnot.setValidator(QRegExpValidator(QRegExp("[0-9]+"), self.entryVisualizeKnot)) self.buttonVisualizeStartPause = QPushButton(QApplication.style().standardIcon(QStyle.SP_MediaPlay), '', self.frameInfo) self.buttonVisualizeStartPause.setGeometry(130, 300, 60, 30) self.buttonVisualizeStartPause.setObjectName("Layer2") self.buttonVisualizeStartPause.clicked.connect(self.startSearch) self.labelRemoveKnot = QLabel("Remove\nKnot:", self.frameInfo) self.labelRemoveKnot.setGeometry(10, 450, 60, 40) self.labelRemoveKnot.setAlignment(Qt.AlignCenter) self.labelRemoveKnot.setObjectName("Layer2NoBG") self.labelRemoveKnot.setFont(QFont("Arial", 15)) self.entryRemoveKnot = QLineEdit(self.frameInfo) self.entryRemoveKnot.setGeometry(70, 450, 60, 30) self.entryRemoveKnot.setAlignment(Qt.AlignCenter) self.entryRemoveKnot.setObjectName("Layer2") self.entryRemoveKnot.setFont(QFont("Arial", 15)) self.entryRemoveKnot.setValidator(QRegExpValidator(QRegExp("[0-9]+"), self.entryRemoveKnot)) self.buttonRemoveKnot = QPushButton(QApplication.style().standardIcon(QStyle.SP_TrashIcon), '', self.frameInfo) self.buttonRemoveKnot.setGeometry(130, 450, 60, 30) self.buttonRemoveKnot.setObjectName("Layer2") self.buttonRemoveKnot.clicked.connect(self.deleteKnot) self.labelSize = QLabel("Change Knots Size:", self.frameInfo) self.labelSize.setGeometry(10, 540, 180, 30) self.labelSize.setAlignment(Qt.AlignCenter) self.labelSize.setObjectName("Layer2NoBG") self.labelSize.setFont(QFont("Arial", 15)) self.buttonMinusSize = QPushButton("-", self.frameInfo) self.buttonMinusSize.setGeometry(10, 560, 90, 30) self.buttonMinusSize.setObjectName("Layer2") self.buttonMinusSize.setFont(QFont("Arial", 20)) self.buttonMinusSize.clicked.connect(lambda: self.changeSize('-')) self.buttonMinusSize.setAutoRepeat(True) self.buttonPlusSize = QPushButton("+", self.frameInfo) self.buttonPlusSize.setGeometry(170, 560, 90, 30) self.buttonPlusSize.setObjectName("Layer2") self.buttonPlusSize.setFont(QFont("Arial", 20)) self.buttonPlusSize.clicked.connect(lambda: self.changeSize('+')) self.buttonPlusSize.setAutoRepeat(True) def defaultTree(self): self.rootTree = sortedKnot(20, parent=self) self.rootTree.addSorted(5) self.rootTree.addSorted(3) self.rootTree.addSorted(12) self.rootTree.addSorted(8) self.rootTree.addSorted(6) self.rootTree.addSorted(13) self.rootTree.addSorted(25) self.rootTree.addSorted(21) self.rootTree.addSorted(28) self.rootTree.addSorted(29) self.rootTree.addSorted(24) self.rootTree.update() def deleteKnot(self): if self.entryRemoveKnot.text(): if int(self.entryRemoveKnot.text()) != self.rootTree.value: if not self.removeThread: self.buttonRemoveKnot.setIcon(QApplication.style().standardIcon(QStyle.SP_DialogDiscardButton)) self.removeThread = visualRemoveThread(self, int(self.entryRemoveKnot.text())) self.removeThread.finished.connect(self.deleteRemoveThread) self.removeThread.start() else: self.deleteRemoveThread() else: self.showError("You can't remove the root", "Deletion Error") else: self.showError("You have to enter a valid value to remove", "Deletion Error") def startSearch(self): if self.entryVisualizeKnot.text(): if not self.searchThread: self.buttonVisualizeStartPause.setIcon(QApplication.style().standardIcon(QStyle.SP_MediaStop)) self.searchThread = visualSearchThread(self, int(self.entryVisualizeKnot.text())) self.searchThread.finished.connect(self.deleteSearchThread) self.searchThread.start() else: self.deleteSearchThread() else: self.showError("You have to enter a valid value to search for", "Search Error") def addKnot(self): if self.entryAddKnot.text(): if not self.addThread: self.buttonAddKnot.setIcon(QApplication.style().standardIcon(QStyle.SP_DialogCancelButton)) self.addThread = visualAddThread(self, int(self.entryAddKnot.text())) self.addThread.finished.connect(self.deleteAddThread) self.addThread.start() else: self.deleteAddThread() else: self.showError("You have to enter a valid value to insert", "Insertion Error") def deleteRemoveThread(self): if self.removeThread: self.buttonRemoveKnot.setIcon(QApplication.style().standardIcon(QStyle.SP_TrashIcon)) for i in self.removeThread.labels: i.setParent(None) self.showError("The knot you wanted to remove has been succesfully annihilated", "Knot Deleted") if self.removeThread.deleted else self.showError("The knot you tried to remove encountered an error", "Deletion Error") if self.removeThread.isRunning(): self.removeThread.terminate() self.removeThread = None self.selected = None self.rootTree.update() self.update() def deleteAddThread(self): if self.addThread: self.buttonAddKnot.setIcon(QApplication.style().standardIcon(QStyle.SP_DialogApplyButton)) if self.addThread.added: self.addThread.added[0].addLeft(self.addThread.toAdd) if self.addThread.added[1] == 'left' else self.addThread.added[0].addRight(self.addThread.toAdd) self.addThread.added[0].left.label.show() if self.addThread.added[1] == 'left' else self.addThread.added[0].right.label.show() self.showError("Your knot has been added with success", "Added Knot") else: self.showError("The value you tried to insert is already taken", "Value Error") if self.addThread.isRunning(): self.addThread.terminate() self.addThread = None self.selected = None self.rootTree.update() self.update() def deleteSearchThread(self): if self.searchThread: self.buttonVisualizeStartPause.setIcon(QApplication.style().standardIcon(QStyle.SP_MediaPlay)) self.showError("The knot you were searching for has been found", "Knot Found") if self.searchThread.found else self.showError("The knot you were searching for was not found", "Knot Not Found") if self.searchThread.isRunning(): self.searchThread.terminate() self.searchThread = None self.selected = None self.update() def changeSize(self, mode): if mode == '-': if self.fontSize-2 >= 10: self.fontSize -= 2 self.knotsSize -= 4 for i in self.frameTree.children(): i.setGeometry(i.x()+2, i.y()+2, self.knotsSize, self.knotsSize) i.setFont(QFont("Arial", self.fontSize)) self.offsets[0] -= 0 self.offsets[1] -= .18 self.offsets[2] += 0 self.offsets[3] += .18 elif mode == '+': if self.fontSize+2 <= 42: self.fontSize += 2 self.knotsSize += 4 for i in self.frameTree.children(): i.setGeometry(i.x()-2, i.y()-2, i.width()+4, i.height()+4) i.setFont(QFont("Arial", self.fontSize)) self.offsets[0] += 0 self.offsets[1] += .18 self.offsets[2] -= 0 self.offsets[3] -= .18 self.update() def paintEvent(self, event): painter = QPainter() painter.begin(self) painter.setPen(QPen(QColor(3,218,197), 3, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) for cos in self.knotCoordinates: painter.drawLine(int(cos[0]+self.offsets[0]), int(cos[1]+self.offsets[1]), int(cos[2]+self.offsets[2]), int(cos[3]+self.offsets[3])) if self.selected: painter.setPen(QPen(QColor("#D8DEE9"), 3, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawLine(int(self.selected[0]+self.offsets[0]), int(self.selected[1]+self.offsets[1]), int(self.selected[2]+self.offsets[2]), int(self.selected[3]+self.offsets[3])) painter.end() def resizeEvent(self, event): self.frameTree.setGeometry(0, 0, int(6*(self.width()/8)), self.height()) self.frameInfo.setGeometry(int(6*(self.width()/8))+5, 10, int(2*(self.width()/8))-10, self.height()-20) self.labelAddKnot.setGeometry(10, int(self.frameInfo.height()/4)-30, int((self.frameInfo.width()-20)/3), 30) self.entryAddKnot.setGeometry(10 + int((self.frameInfo.width()-20)/3), int(self.frameInfo.height()/4)-30, int((self.frameInfo.width()-20)/3), 30) self.buttonAddKnot.setGeometry(10 + 2*int((self.frameInfo.width()-20)/3), int(self.frameInfo.height()/4)-30, int((self.frameInfo.width()-20)/3), 30) self.labelVisualizeKnot.setGeometry(10, int(self.frameInfo.height()/2), int((self.frameInfo.width()-20)/3), 30) self.entryVisualizeKnot.setGeometry(10 + int((self.frameInfo.width()-20)/3), int(self.frameInfo.height()/2), int((self.frameInfo.width()-20)/3), 30) self.buttonVisualizeStartPause.setGeometry(10 + 2*int((self.frameInfo.width()-20)/3), int(self.frameInfo.height()/2), int((self.frameInfo.width()-20)/3), 30) self.labelRemoveKnot.setGeometry(10, 3*int(self.frameInfo.height()/4)-10, int((self.frameInfo.width()-20)/3), 50) self.entryRemoveKnot.setGeometry(10 + int((self.frameInfo.width()-20)/3), 3*int(self.frameInfo.height()/4), int((self.frameInfo.width()-20)/3), 30) self.buttonRemoveKnot.setGeometry(10 + 2*int((self.frameInfo.width()-20)/3), 3*int(self.frameInfo.height()/4), int((self.frameInfo.width()-20)/3), 30) self.labelSize.setGeometry(10, self.frameInfo.height()-70, self.frameInfo.width()-30, 30) self.buttonPlusSize.setGeometry(int((self.frameInfo.width()-20)/2+10), self.frameInfo.height()-40, int((self.frameInfo.width()-30)/2), 30) self.buttonMinusSize.setGeometry(10, self.frameInfo.height()-40, int((self.frameInfo.width()-30)/2), 30) self.rootTree.update() def showError(self, text, title): self.windowError.setText(text) self.windowError.setWindowTitle(title) self.windowError.show()
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)
class explore(QWidget): files = None icon_size = 120 hidden = False maxWidth = 1200 scroll_pos_old = 0 scroll_pos = 0 btns = [] #targetType = None mouse = [0, 0] collized = [] #Print with hotkey keyPrint = None selectBtn = None anitimer = None newSelect = 0 select_old = [0] def __init__(self, parent=None): super(explore, self).__init__(parent) self.parent = parent self.setAcceptDrops(True) self.init() def init(self): sys.modules['explore'] = self self.core = core() self.keyPrint = lambda: (print( self.btns), print(len(self.btns), print('f:', self.files))) self.history = self.parent.parent().history self.scroll = QScrollArea(self) self.window = QWidget(self) self.window.setAcceptDrops(True) self.scroll.setWidget(self.window) self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) #self.scroll.verticalScrollBar().valueChanged.connect(self.wh_change) #self.scroll.verticalScrollBar().sliderMoved.connect(self.scrollMoved) #self.scroll.mousePressEvent = lambda e: print(e, 'de') self.scrBar = QScrollBar() self.scroll.setVerticalScrollBar(self.scrBar) self.scrBar.valueChanged.connect(self.wh_change) self.scrBar.sliderMoved.connect(self.scrollMoved) self.scrBar.wheelEvent = self.wh self.scrBar.mousePressEvent = self.scrollBarPress self.scrBar.mouseMoveEvent = self.scrollBarMove self.scroll.wheelEvent = self.wh self.anitimer = QTimer() self.anitimer.setInterval(1000 / 60) self.anitimer.timeout.connect(self.ani_func) self.anitimer.start() self.qrun = qrun(self) self.app = QApplication([]) hk = sys.modules['scr.hotkey'].hotkey def setkey(name, func): key = initKey() hk.setInitName(hk, name) key.func = func hk.add(hk, key) setkey('redo', lambda: self.redo()) setkey('undo', lambda: self.undo()) setkey('selectall', lambda: self.selectAll()) setkey('deselectall', lambda: self.deselect()) setkey('keyright', lambda: self.keyArrow('right')) setkey('keyleft', lambda: self.keyArrow('left')) setkey('keyup', lambda: self.keyArrow('up')) setkey('keydown', lambda: self.keyArrow('down')) setkey('ctrlkeyright', lambda: self.keyArrow('right', 1)) setkey('ctrlkeyleft', lambda: self.keyArrow('left', 1)) setkey('ctrlkeyup', lambda: self.keyArrow('up', 1)) setkey('ctrlkeydown', lambda: self.keyArrow('down', 1)) setkey('keyenter', lambda: self.keyEnter()) setkey('keyreturn', lambda: self.keyEnter()) setkey('copy', lambda: self.copyCB()) setkey('paste', lambda: self.pasteCB()) setkey('toggleqrun', lambda: self.qrun.toggle()) setkey('escape', lambda: print('esc explore')) sys.modules['m'].callbackResize.append(self.res) self.res() def copyCB(self): #CLIPBOARD self.collized = [x for x in self.btns if x.select == True] self.sourceDir = os.getcwd() if self.collized: if self.current_dir[-1] == "/": self.current_dir = self.current_dir[:-1] self.mimeData = QMimeData() self.urls = [] for e in self.collized: self.urls.append( QUrl.fromLocalFile(self.current_dir + '/' + e.value)) self.mimeData.setUrls(self.urls) #-------------------------------- cb = self.app.clipboard() cb.clear(mode=cb.Clipboard) self.app.clipboard().setMimeData(self.mimeData, mode=cb.Clipboard) def pasteCB(self): #CLIPBOARD print("paste") print(self.app.clipboard().mimeData().urls()) links = [] mimeurls = self.app.clipboard().mimeData().urls() for url in mimeurls: url = QUrl(url) links.append(url.toLocalFile()) cp = sys.modules['scr.core'].core.copy cp(sys.modules['scr.core'].core, self.sourceDir, links, os.getcwd()) self.refresh() pass def keyEnter(self): try: self.selectBtn = self.btns[self.newSelect] except: self.selectBtn = self.btns[0] self.selectFile = os.path.normpath(self.current_dir + '/' + self.selectBtn.value) if os.path.isdir(self.selectFile): self.setDir(self.current_dir + '/' + self.selectBtn.value) self.history.set(self.current_dir, self.scroll_pos) else: self.setDir(self.current_dir + '/' + self.selectBtn.value) self.lm_menu() def selectFiles(self): self.collized = [x.value for x in self.btns if x.select == True] return self.collized def keyArrow(self, e, mod=0): if self.btns: iconPerRow = int(self.scroll.width() / self.icon_size) iconPerCol = int(self.scroll.height() / self.icon_size) selectOn = [x for x in self.btns if x.select == True] frameTop = self.scroll_pos frameBottom = self.scroll_pos + iconPerCol * 120 if selectOn: #select = selectOn[:1][0].value slo = self.select_old[-1] #select = self.btns[self.select_old[-1]][0].value select = self.btns[slo].value else: select = self.btns[0].value for i in range(len(self.btns)): if select == self.btns[i].value: if e == 'left': if i - 1 >= -1: self.deselect() if i - 1 == -1: self.newSelect = 0 else: self.newSelect = i - 1 b = self.btns[self.newSelect] b.select = True b.leaveEvent(QMouseEvent) elif e == 'right': if i + 1 < len(self.btns): self.deselect() self.newSelect = i + 1 b = self.btns[self.newSelect] b.select = True b.leaveEvent(QMouseEvent) elif e == 'up': if i - iconPerRow >= 0: self.deselect() self.newSelect = i - iconPerRow b = self.btns[self.newSelect] b.select = True b.leaveEvent(QMouseEvent) elif e == 'down': if i + iconPerRow < len(self.btns): self.deselect() self.newSelect = i + iconPerRow b = self.btns[self.newSelect] b.select = True b.leaveEvent(QMouseEvent) if mod == 1: for e in self.select_old: b = self.btns[e] b.select = True b.leaveEvent(QMouseEvent) self.select_old += [self.newSelect] else: self.select_old = [self.newSelect] print(self.select_old) ###################################################### selectOnLine = int(self.newSelect / iconPerRow) PosOnSelect = selectOnLine * self.icon_size if PosOnSelect < frameTop: self.scroll_pos = PosOnSelect elif PosOnSelect > frameBottom - 120: self.scroll_pos = PosOnSelect - 120 * 3 if e == 'enter': pass def scrollMoved(self): #self.targetType = None pass def mousePressed(self, e): #НЕ РАБОТАЕТ self.globalPos = e.globalPos() def scrollBarPress(self, e): self.vbar = self.scroll.verticalScrollBar() frameHeight = self.scroll.height() current = self.vbar.value() currentClick = int(self.window.height() * (e.y() / frameHeight)) min = current max = min + (self.window.height() - self.vbar.maximum()) if not (currentClick > min and currentClick < max): if currentClick > max: self.scroll_pos += self.scroll.height() elif currentClick < min: self.scroll_pos -= self.scroll.height() else: #Scroll Btn self.dragStartScroll = currentClick - min # нужно, чтобы убрать дерганье super(explore, self).mousePressEvent(e) def scrollBarMove(self, e): scrH = self.scroll.height() wH = self.window.height() #self.vbar.maximum() pos = e.y() / scrH try: self.hardScroll((wH * pos) - self.dragStartScroll) except: pass def wh_change(self, e): #print(e, self.scroll_pos, self.targetType) #if self.targetType == None: #EEEE #self.scroll_pos = self.scroll_pos_old = e pass def wh(self, e): max = self.scroll.verticalScrollBar().maximum() #self.targetType = 'wheel' self.scroll_pos = self.scroll_pos - e.angleDelta().y() if self.scroll_pos > max: self.hardScroll(max - 1) elif self.scroll_pos < 0: self.hardScroll(0) def hardScroll(self, e): self.scroll_pos = self.scroll_pos_old = e def ani_func(self): self.scroll_pos_old = self.scroll_pos_old - ( (self.scroll_pos_old - self.scroll_pos) / 4) self.scroll.verticalScrollBar().setValue(self.scroll_pos_old) def setSize(self, *size): h, v = size print(self.size()) self.window.setFixedWidth(h) self.setFixedSize(*size) self.scroll.setFixedSize(h, v - 20) self.maxWidth = int(self.width() / self.icon_size) * self.icon_size self.reposition() def run_c(self, command): """Executes a system command.""" def lm_cmd(self): cmd = self.lb.text() + ' ' + "'" + self.selectFile + "'" self.cmd_run(cmd) def rm_cmd(self): cmd = self.lb.text() self.cmd_run(cmd) def cmd_run(self, command): print(command) #subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) pros = QProcess(self) pros.finished.connect(self.proc_finish) str = "/bin/sh -c \"" + command + "\"" pros.start(str) try: self.mousemenu.close() except: pass #self.scroll.verticalScrollBar().setValue = self.scroll_pos #update pass def proc_finish(self): self.refresh() def lm_menu(self): print(self.selectFile) self.cmd_run("xdg-open " + self.selectFile) return #НУЖНО ЛИ МЕНЮ НА ЛЕВЫЙ КЛИК? #НАВЕРНОЕ ЛУЧШЕ ОРГАНИЗОВАТЬ ЧЕРЕЗ ПРОБЕЛ self.mousemenu = QMenu(self) act = QWidgetAction(self) self.menuMain = QWidget(self) self.menuMain.setStyleSheet('background: #111') #XDG-OPEN self.btn_open = QPushButton(self.menuMain) self.btn_open.setStyleSheet( "background: gray; text-align: left; padding-left: 3px") self.btn_open.setText("Open") self.btn_open.setFixedSize(194, 20) self.btn_open.pressed.connect( lambda: self.cmd_run("xdg-open " + self.selectFile)) self.btn_open.move(3, 3) #INPUT CMD self.lb = QLineEdit(self.menuMain) self.lb.move(3, 23) self.lb.setFixedSize(194, 20) self.lb.setPlaceholderText('cmd') self.lb.setStyleSheet('background: white') self.lb.returnPressed.connect(self.lm_cmd) self.menuMain.setFixedSize(200, 100) act.setDefaultWidget(self.menuMain) self.mousemenu.addAction(act) try: self.mousemenu.exec_(self.globalPos) except: self.mousemenu.exec_([0, 0]) def contextMenuEvent(self, event): self.mousemenu = QMenu(self) act = QWidgetAction(self) self.menuSecond = QWidget(self) self.menuSecond.setStyleSheet('background: #511') self.lb = QLineEdit(self.menuSecond) self.lb.move(3, 3) self.lb.setFixedSize(194, 20) self.lb.setPlaceholderText('cmd') self.lb.setStyleSheet('background: white') self.lb.returnPressed.connect(self.rm_cmd) self.menuSecond.setFixedSize(200, 100) act.setDefaultWidget(self.menuSecond) self.mousemenu.addAction(act) self.mousemenu.exec_(self.globalPos) def btnsClear(self): if len(self.btns): for i in self.btns: i.deleteLater() del i del self.btns[:] # че за хрень def setDir(self, dir): dir = dir.replace("//", "/") #dir_enc = self.dFilter.enc(dir) #dir_dec = self.dFilter.dec(dir_enc) if os.path.isdir(dir) != True: if self.selectBtn.select != True: self.selectBtn.select = True return self.btnsClear() self.current_dir = dir.replace("//", "/") self.core.sort = 'abs123' self.files = self.core.read_dir(self.current_dir) #FILTER DIR self.parent.parent().address.setText(dir) os.chdir(dir) #self.history.set(self.current_dir) self.cycle = self.tmpL = 0 self.asyncRender() self.refreshtime = QTimer() self.refreshtime.timeout.connect(self.refresh) self.refreshtime.start(1000) """ self.ftime = QTimer() self.ftime.timeout.connect(self.dir_final) self.ftime.start(1000/60) """ sys.modules['m'].setProgramName(sys.modules['appName'] + ' - ' + os.path.basename(os.getcwd())) #def dir_final(self): # self.ftime.deleteLater() """ try: self.btns[0].select = True self.btns[0].leaveEvent(QMouseEvent) except: print("first select btns error") """ def redo(self): link = self.history.get(1) if link != None: #self.hardScroll() self.hardScroll(link[2]) self.setDir(link[1]) def undo(self): link = self.history.get(-1) if link != None: self.hardScroll(link[2]) self.setDir(link[1]) def selectAll(self): #selectOn = next((x for x in self.btns if x.select == True), None) selectOn = [x for x in self.btns if x.select == True] if not len(selectOn) == len(self.btns): for b in self.btns: b.selected() b.leaveEvent(QMouseEvent) else: for b in self.btns: b.unselected() b.leaveEvent(QMouseEvent) def deselect(self): for b in self.btns: b.unselected() b.leaveEvent(QMouseEvent) def asyncRender(self): self.renderTime = QTimer() self.renderTime.timeout.connect(self.render) self.renderTime.start(1000 / 60) def getTextSplit(self, text): def width(t): return label.fontMetrics().boundingRect(t).width() label = QLabel() mw = self.icon_size - 15 spl = text.split(' ') group = '' for item in spl: if width(item) > mw: i = 0 w = 0 letter = '' while True: w = w + width(item[i]) if w > mw: w = 0 letter += ' ' + item[i] elif item[i] == '-': w = 0 letter += item[i] + ' ' else: letter += item[i] i = i + 1 if i == len(item): break group += letter + ' ' else: group += item + ' ' return group def rebuild(self): btns_rebuild = [] for f in self.btns: try: f.move(1, 1) btns_rebuild.append(f) except: pass self.btns = btns_rebuild def res(self): print('a', self.window.size(), self.size()) def reposition(self): if self.btns == None: self.btns = [] self.rebuild() #if f.value not in self.files: # print(f.value) #self.create_button(f.value) #123 i = 0 l = 0 for f in self.btns: count_hoz = int(self.maxWidth / self.icon_size) x = (self.icon_size * i) % self.maxWidth y = self.icon_size * l try: f.move(x, y) except: print('ERROR BTN', f) if i % count_hoz == count_hoz - 1: l = l + 1 i = i + 1 #WINDOW SCROLL m = sys.modules['m'] if self.window.height() <= self.height() - m.addrborder.height(): self.window.setFixedHeight(self.height() - m.addrborder.height()) else: self.window.setFixedHeight((l + 1) * self.icon_size) def dir_mod(self): self.refresh() def create_button(self, f): btn = object_file(self.window) realfile = self.current_dir + '/' + f #btn.realfile = realfile if core.type_file(self, realfile) == 'folder': btn.setIconFromTheme('folder') else: ic = iconProvider() ic = ic.icon(QFileInfo(realfile)) btn.setIcon(ic) btn.setFileSize(120, 120) btn.setText(f) btn.init() btn.value = f btn.clicked.connect(self.btn_press) btn.setTrigger('123') self.btns.append(btn) def refresh(self): dir = self.current_dir #dir_dec = d.dec(dir) self.core.sort = 'abs123' #GET NEW LS` self.newfiles = self.core.read_dir(dir) l1 = set(self.files) l2 = set(self.newfiles) toDel = l1 - l2 if len(toDel) > 0: for d in toDel: for btn in self.btns: if d == btn.value: btn.deleteLater() toAdd = l2 - l1 unique = list(dict.fromkeys(toAdd)) for f in unique: self.create_button(f) def btn_text(elem): return core.nat_keys(self, elem.value)[0] if self.btns != None: self.btns.sort(key=btn_text) self.btns = self.splitFolders(self.btns) self.files = self.newfiles self.reposition() def splitFolders(self, source_files): if source_files == None: return if len(source_files) == 0: return folders = [] files = [] if type(source_files[0]) is object_file: for file in source_files: realfile = self.current_dir + '/' + file.value if file.value[0] != '.' or self.hidden != True: if core.type_file(self, realfile) == 'folder': folders.append(file) else: files.append(file) else: for file in source_files: realfile = self.current_dir + '/' + file if file[0] != '.' or self.hidden != True: if core.type_file(self, realfile) == 'folder': folders.append(file) else: files.append(file) return folders + files def render(self): if len(self.files) == 0: return i = self.cycle l = self.tmpL combine = self.splitFolders(self.files) cycleAll = int(len(combine) / 50) * 50 try: float_perc = self.cycle / cycleAll except ZeroDivisionError: float_perc = 0 self.parent.parent().setLoadPercent(float_perc * 100) if int(float_perc) == 1: self.parent.parent().hideLoader() for file in range(len(combine)): file = file + self.cycle btn = object_file(self.window) try: realfile = self.current_dir + '/' + combine[file] except: print('733 explore', 'no found file error') return btn.realfile = realfile if core.type_file(self, realfile) == 'folder': btn.setIconFromTheme('folder') else: ic = iconProvider() ic = ic.icon(QFileInfo(realfile)) btn.setIcon(ic) btn.setFileSize(120, 120) btn.setText(combine[file]) btn.init() x = (self.icon_size * i) % self.maxWidth y = self.icon_size * l btn.move(x, y) count_hoz = int(self.maxWidth / self.icon_size) if file % count_hoz == count_hoz - 1: l = l + 1 i = i + 1 btn.value = combine[file] try: btn.date = os.path.getctime(realfile) except: btn.date = 0 btn.clicked.connect(self.btn_press) btn.setTrigger(lambda: self.button_trigger) self.btns.append(btn) if i % 50 == 0: self.cycle = i self.tmpL = l break elif file == len(combine) - 1: self.renderTime = None self.cycle = -1 break self.window.setFixedHeight((l + 1) * self.icon_size) if self.cycle == -1: return self.asyncRender() def collision(self): iscoll = [] for b in self.btns: e = { 'x': b.pos().x() + 120, 'y': b.pos().y() + 120, 'w': (b.pos().x() + b.size.width()) - b.size.width(), 'h': (b.pos().y() + b.size.height()) - b.size.height() } if (e['x'] > self.select_rect_coll[0] and e['w'] < self.select_rect_coll[2]) \ and (e['y'] > self.select_rect_coll[1] and e['h'] < self.select_rect_coll[3]): iscoll.append(b) return iscoll def mousePressEvent(self, QMouseEvent): #self.unSelectAll() #Здесь сбивает работу мыши hover = self.isHoverButton() if not hover: self.deselect() self.press = True self.globalPos = QMouseEvent.globalPos() self.windowMouseCoord = self.window.mapFromGlobal( QMouseEvent.globalPos()) self.selection = QFrame(self.window) self.selection.setStyleSheet(""" background: rgba(140,200,255,.0); border-radius: 10px; border-width: 1px; border-style: solid; border-color: rgba(0,0,0,.2); """) self.startCoord = QPoint(self.windowMouseCoord) #if abs(self.startCoord.x()) > 1 and abs(self.startCoord.y()) > 1: self.selection.setFixedSize(0, 0) self.selection.move(5, 5) self.selection.move(self.windowMouseCoord) self.selection.show() if self.collized: self.itemsSelection = True else: self.itemsSelection = None def isHoverButton(self): for e in self.btns: try: if e.hover: return True except: pass def isHoverButtonSelection(self): for e in self.collized: try: if e.hover: return True except: pass def mouseMoveEvent(self, QMouseEvent): hover = self.isHoverButtonSelection() if self.itemsSelection: if hover: self.mimeData = QMimeData() urls = [] for e in self.collized: urls.append( QUrl('file://' + self.current_dir + '/' + e.value)) self.mimeData.setUrls(urls) self.drag = QDrag(self) self.drag.setMimeData(self.mimeData) #################### # ВОТ ЭТО ВАЖНО #self.drag.exec(Qt.LinkAction) self.drag.exec(Qt.CopyAction) if self.press: pass else: for b in self.btns: b.unselected() self.select_rect_coll = [] self.collized = [] else: #if self.itemsSelection if self.btns == None: return self.select_rect_coll = [ self.selection.x(), self.selection.y(), self.selection.x() + self.selection.width(), self.selection.y() + self.selection.height() ] for b in self.btns: b.unselected() b.leaveEvent(QMouseEvent) self.collized = self.collision() for b in self.collized: b.selected() b.leaveEvent(QMouseEvent) # b.clicked.emit() self.windowMouseCoord = self.window.mapFromGlobal( QMouseEvent.globalPos()) self.mouse = [self.windowMouseCoord.x(), self.windowMouseCoord.y()] self.windowMouseCoord = self.window.mapFromGlobal( QMouseEvent.globalPos()) movePoint = self.startCoord - self.windowMouseCoord invertmove = self.startCoord - movePoint #Drawing selection if movePoint.x() > 0 and movePoint.y() > 0: self.selection.setFixedSize(abs(movePoint.x()), abs(movePoint.y())) self.selection.move(invertmove) elif movePoint.x() > 0: self.selection.setFixedSize(abs(movePoint.x()), abs(movePoint.y())) self.selection.move(invertmove.x(), self.startCoord.y()) elif movePoint.y() > 0: self.selection.setFixedSize(abs(movePoint.x()), abs(movePoint.y())) self.selection.move(self.startCoord.x(), invertmove.y()) else: self.selection.setFixedSize(abs(movePoint.x()), abs(movePoint.y())) ############################################################################# def mouseReleaseEvent(self, QMouseEvent): self.press = False self.globalPos = QMouseEvent.globalPos() self.windowMouseCoord = self.window.mapFromGlobal( QMouseEvent.globalPos()) if self.startCoord == QPoint(self.windowMouseCoord): self.select_rect_coll = [] self.selection.deleteLater() pass def mimeTypes(self): mimetypes = super().mimeTypes() mimetypes.append('text/plain') return mimetypes def dropEvent(self, event): #print(event.dropAction() == Qt.CopyAction) #url = QUrl() links = [] for url in event.mimeData().urls(): url = QUrl(url) links.append(url.toLocalFile()) sys.modules['scr.core'].core.copy(sys.modules['scr.core'].core, links, os.getcwd()) self.refresh() #path = url.toLocalFile().toLocal8Bit().data() #if os.path.isfile(path): # print(path) #self.refresh() def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dragMoveEvent(self, Event): print('drag', Event.pos()) def button_trigger(self, e): print('WIN', e.value) def btn_press(self): self.globalPos = self.sender().globalPos if self.sender().target == 'None': try: self.selectBtn.select = None except: print('none target') pass if not self.sender().select and self.sender().target == 'icon': try: self.selectBtn.select = None self.selectBtn.btn_update = True self.selectBtn.update() except: pass self.sender().select = True self.selectBtn = self.sender() return if self.sender() == self.selectBtn and self.sender().target == 'icon': try: self.selectBtn.select = None self.selectBtn.btn_update = True self.selectBtn.update() except: pass self.selectBtn = self.sender() self.selectFile = os.path.normpath(self.current_dir + '/' + self.sender().value) if os.path.isdir(self.selectFile): self.setDir(self.current_dir + '/' + self.sender().value) self.history.set(self.current_dir, self.scroll_pos) else: self.setDir(self.current_dir + '/' + self.sender().value) self.lm_menu()
class Snake(QWidget): def __init__(self): """Initial game configuration.""" super().__init__() # GLOBAL VARIABLES self.zoom = 1 / 50 self.zoom_coefficient = 1.3 self.origin_position = [300, 300] self.move_start_position = [0, 0] # start of the move self.move_origin_position = [0, 0] # origin position at the beginning of the move # WIDGET CREATION self.canvas = QFrame(self, minimumSize=QSize(600, 600)) # WIDGET LAYOUT self.main_v_layout = QVBoxLayout(self, margin=0) self.equation_inputs_layout = QVBoxLayout(self, margin=10) # add 3 equation inputs for i in range(3): self.equation_inputs_layout.addWidget(QLineEdit(self, textChanged=self.update)) self.main_v_layout.addWidget(self.canvas) self.main_v_layout.addLayout(self.equation_inputs_layout) self.setLayout(self.main_v_layout) # WINDOW SETTINGS self.setWindowTitle('Window Title') self.setFont(QFont("Fira Code", 16)) self.show() def calculate_y_values(self, function: str, x_values: list): """Calculates function values from a list of x values.""" return evaluate_function_from_list(function, x_values) def paintEvent(self, event): """Paints on the canvas.""" painter = QPainter(self) painter.setPen(QPen(Qt.black, Qt.SolidLine)) painter.setBrush(QBrush(Qt.white, Qt.SolidPattern)) # draw background painter.drawRect(0, 0, self.canvas.width(), self.canvas.height()) # draw axes painter.drawLine(0, self.canvas.height() - self.origin_position[1], self.canvas.width(), self.canvas.height() - self.origin_position[1]) painter.drawLine(self.origin_position[0], 0, self.origin_position[0], self.canvas.height()) # colors of the graphs function_colors = [Qt.red, Qt.blue, Qt.green] for i in range(self.equation_inputs_layout.count()): # get the text of the function function = self.equation_inputs_layout.itemAt(i).widget().text() # set color painter.setPen(QPen(function_colors[i], Qt.SolidLine)) x_values = [] for j in range(self.canvas.width()): # calculate the position from canvas to the zoomed and translated coordinates x_values.append((j - self.origin_position[0]) * self.zoom) # calculate the y values of the function y_values = self.calculate_y_values(function, x_values) # adjust the y values for the zoom for j in range(len(y_values)): if y_values[j] is not None: y_values[j] = (j, self.canvas.height() - (y_values[j] / self.zoom + self.origin_position[1])) # form the graph by connecting the points for j in range(len(y_values) - 1): # if both exist, simply connect them if y_values[j] is not None and y_values[j + 1] is not None: # if they visually don't connect on the canvas, don't connect them if abs(y_values[j][1] - y_values[j + 1][1]) < self.canvas.height(): painter.drawLine(y_values[j][0], y_values[j][1], y_values[j + 1][0], y_values[j + 1][1]) def mouseMoveEvent(self, event): """Is called when the mouse is moved and has a button pressed. Moves the axes and updates the canvas.""" self.origin_position = [ self.move_origin_position[0] + (event.pos().x() - self.move_start_position[0]), self.move_origin_position[1] - (event.pos().y() - self.move_start_position[1]) ] self.update() def mousePressEvent(self, event): """Is called when the mouse is pressed. Starts to move the axes.""" self.move_start_position = [event.pos().x(), event.pos().y()] self.move_origin_position = list(self.origin_position) def wheelEvent(self, event): """Is called when the mouse wheel is moved. Adjusts zoom and updates the canvas.""" if event.angleDelta().y() > 0: self.zoom /= self.zoom_coefficient else: self.zoom *= self.zoom_coefficient self.update()
class StreamScope( QWidget ): resize_signal = pyqtSignal(int) resolutions = [ '320x200', # CGA '320x240', # QVGA '480x320', # HVGA '640x480', # VGA '720x480', # 480p '800x480', # WVGA '854x480', # WVGA (NTSC+) '1024x576', # PAL+ '1024x768', # XGA '1280x720', # HD '1280x768', # WXGA 'Elastic' ] def __init__( self ): super().__init__() self.title_bar_h = self.style().pixelMetric( QStyle.PM_TitleBarHeight ) self.devices = get_video_devices() self.device = '/dev/{0}'.format( self.devices[0] ) res = self.resolutions[0].split( 'x' ) self.res_w = int( res[0]) self.res_h = int( res[1]) self.stream_thread = QThread(parent=self) self.streamer = DeskStreamer() self.streamer.moveToThread( self.stream_thread ) self.streamer.finished.connect( self.stream_thread.quit ) self.streamer.finished.connect( self.streamer.deleteLater ) self.stream_thread.finished.connect( self.stream_thread.deleteLater ) self.stream_thread.start() self.stream_thread.started.connect( self.streamer.long_running ) self.setWindowTitle( self.__class__.__name__ ) self.setGeometry( 0, 0, self.res_w, self.res_h ) self.init_layout() self.setWindowFlags( self.windowFlags() # Keep existing flags | Qt.WindowStaysOnTopHint ) # Show the window self.show() def init_layout( self ): layout1 = QVBoxLayout() layout2 = QHBoxLayout() # Get video devices combo = QComboBox( self ) for i, device in enumerate( self.devices ): combo.addItem( device ) if device == 'video20': combo.setCurrentIndex( i ) self.device = '/dev/{0}'.format( device ) combo.activated[str].connect( self.comboChanged ) resolution = QComboBox( self ) for res in self.resolutions: resolution.addItem( res ) resolution.activated[str].connect( self.resolutionChanged ) # Buttons self.stream_btn = QPushButton( self, objectName='stream_btn' ) self.stream_btn.setText( 'Stream' ) self.stream_btn.clicked.connect( self.stream ) self.stop_btn = QPushButton( self, objectName='stop_btn' ) self.stop_btn.setText( 'Stop' ) self.stop_btn.clicked.connect( self.stop ) self.frame = QFrame(self ) style=''' QPushButton{ text-align:center; } QPushButton#stream_btn{ background-color: orange; } QPushButton#stop_btn{ background-color: white; } QFrame{ background-color: #3E3E3E; border-bottom-right-radius: 10px; border-bottom-left-radius: 10px; } ''' self.frame.setStyleSheet( style ) layout2.addWidget( self.stream_btn ) layout2.addWidget( self.stop_btn ) layout2.addWidget( resolution ) layout2.addWidget( combo ) self.frame.setLayout( layout2 ) self.frame.setFixedHeight( self.frame.height()*1.5 ) self.viewfinder = QLabel(self, objectName='view_finder') style=''' QLabel{ background-color: cyan; } QLabel#view_finder{ border:5px solid orange; background:transparent; padding:0px; } ''' self.viewfinder.setStyleSheet( style ) self.viewfinder.setMinimumWidth( 0 ) self.viewfinder.setMinimumHeight( 0 ) self.viewfinder.setGeometry( QRect( self.frame.pos().x(), 0, self.res_w, self.res_h ) ) layout1.addWidget( self.viewfinder ) layout1.addWidget( self.frame ) layout1.setSpacing(0) layout1.setContentsMargins( 0 ,0 ,0, 0 ) self.setAttribute( Qt.WA_TranslucentBackground ) self.setLayout( layout1 ) self.update_frustum() def stream( self ): if not self.streamer.isStreaming(): self.stream_btn.setStyleSheet( 'background-color: grey;' ) self.stop_btn.setStyleSheet( 'background-color: red;' ) self.viewfinder.setStyleSheet( 'background-color: cyan; border:1px hidden red; background:transparent; ' ) self.update_frustum() self.streamer.stream() def stop( self ): if self.streamer.isStreaming(): self.stream_btn.setStyleSheet( 'background-color: orange;') self.stop_btn.setStyleSheet( 'background-color: white;') self.viewfinder.setStyleSheet( 'background-color: cyan; border:5px solid orange; background:transparent; padding:0;' ) self.streamer.stop() def comboChanged( self, text ): self.stop() self.device = '/dev/{0}'.format( text ) def resolutionChanged( self, text ): self.stop() if text == 'Elastic': min_w = 0 min_h = 0 win_w = self.viewfinder.width() win_h = self.viewfinder.height() else: res = text.split( 'x' ) self.res_w = int( res[0] ) self.res_h = int( res[1] ) min_w = self.res_w min_h = self.res_h win_w = self.res_w win_h = self.res_h self.viewfinder.setMinimumWidth( min_w ) self.viewfinder.setMinimumHeight( min_h ) self.viewfinder.setGeometry( QRect( self.frame.pos().x(), 0, win_w, win_h ) ) self.setGeometry( QRect( self.pos().x(), self.pos().y(), win_w, win_h ) ) self.adjustSize() def debug_frustum( self ): print( 'Window: {0}, {1}x{2}'.format( self.pos(), self.width(), self.height() ) ) print( 'Status: {0}'.format( self.title_bar_h ) ) print( 'Scope: {0}, {1}x{2}'.format( self.viewfinder.pos(),self.viewfinder.width(), self.viewfinder.height() ) ) print( 'Device: {0}'.format( self.device ) ) def update_frustum( self ): #self.debug_frustum() self.streamer.x = self.pos().x() + self.viewfinder.pos().x() + 0 self.streamer.y = self.pos().y() + self.viewfinder.pos().y() + self.title_bar_h + 5 self.streamer.width = self.viewfinder.width() self.streamer.height = self.viewfinder.height() self.streamer.device = self.device def moveEvent( self, event ): self.stop() self.update_frustum() super().moveEvent(event) def resizeEvent(self, event = None): self.stop() self.update_frustum() self.resize_signal.emit( 1 ) def closeEvent( self, event ): self.thread_clean_up() super().closeEvent( event ) def thread_clean_up( self ): print( 'Cleaning up thread' ) self.streamer.exit() self.stream_thread.quit() self.stream_thread.wait()