class SerialPortSelector(QWidget): open_port = pyqtSignal(str, int) close_port = pyqtSignal() def __init__(self, *args): super(SerialPortSelector, self).__init__(*args) self.disabled = False self.init_ui() self.add_ports() def init_ui(self): layout = QHBoxLayout() self.setLayout(layout) self.ports_list_combobox = QComboBox() layout.addWidget(self.ports_list_combobox) self.baud_rate_combobox = QComboBox() self.baud_rate_combobox.addItems([ '300', '600', '1200', '2400', '4800', '9600', '19200', '38400', '43000', '56000', '57600', '115200' ]) self.baud_rate_combobox.setCurrentText('115200') self.baud_rate_combobox.setEditable(True) layout.addWidget(self.baud_rate_combobox) self.open_btn = QPushButton('打开') self.open_btn.clicked.connect(self.handle_open_port) layout.addWidget(self.open_btn) self.refresh_btn = QPushButton('刷新') self.refresh_btn.clicked.connect(self.add_ports) layout.addWidget(self.refresh_btn) def add_ports(self): self.ports_list_combobox.clear() for port in comports(False): self.ports_list_combobox.addItem(port.name, port) def handle_open_port(self): if self.disabled: self.close_port.emit() else: port = self.ports_list_combobox.currentText() if port == "": return baud_rate = int(self.baud_rate_combobox.currentText()) self.open_port.emit(port, baud_rate) def set_disable(self, b): self.disabled = b self.ports_list_combobox.setDisabled(b) self.baud_rate_combobox.setDisabled(b) if self.disabled: self.open_btn.setText('关闭') else: self.open_btn.setText('打开')
class Window(QMainWindow): def __init__(self): super().__init__() self.width = 450 self.height = 250 self.xPos = 600 self.yPos = 400 self.initUI() def initUI(self): self.setGeometry(self.xPos, self.yPos, self.width, self.height) self.vBoxLayout = QVBoxLayout() self.tagbox = TagBox() self.tagbox.addTag('Homelander') self.tagbox.addTag('Queen Maeve') self.tagbox.addTag('Black Noir') self.tagbox.addTag('Transluscent') self.tagbox.addTag('A-Train') self.tagbox.addTag('The Deep') self.vBoxLayout.addWidget(self.tagbox) self.tagEdit = QLineEdit() self.vBoxLayout.addWidget(self.tagEdit) self.addButton = QPushButton() self.addButton.setText('Add New Tag') self.addButton.clicked.connect(self.addNewTag) self.vBoxLayout.addWidget(self.addButton) self.centralWidget = QWidget(self) self.centralWidget.setLayout(self.vBoxLayout) self.setCentralWidget(self.centralWidget) self.show() def addNewTag(self): self.tagbox.addTag(self.tagEdit.text())
class Window(QMainWindow): def __init__(self): super().__init__() self.width = 500 self.height = 500 self.xPos = 600 self.yPos = 400 self.initUI() def initUI(self): self.setGeometry(self.xPos, self.yPos, self.width, self.height) self.vBoxLayout = QVBoxLayout() self.slider = Slider( direction=Qt.Orientation.Horizontal, duration=750, animationType=QEasingCurve.Type.OutQuad, wrap=False, ) self.label1 = QLabel() self.label1.setText('First Slide') self.label1.setAlignment(Qt.AlignmentFlag.AlignCenter) self.label1.setStyleSheet( 'QLabel{background-color: rgb(245, 177, 66); color: rgb(21, 21, 21); font: 25pt;}' ) self.slider.addWidget(self.label1) self.label2 = QLabel() self.label2.setText('Second Slide') self.label2.setAlignment(Qt.AlignmentFlag.AlignCenter) self.label2.setStyleSheet( 'QLabel{background-color: rgb(21, 21, 21); color: rgb(245, 177, 66); font: 25pt;}' ) self.slider.addWidget(self.label2) self.label3 = QLabel() self.label3.setText('Third Slide') self.label3.setAlignment(Qt.AlignmentFlag.AlignCenter) self.label3.setStyleSheet( 'QLabel{background-color: rgb(93, 132, 48); color: rgb(245, 177, 66); font: 25pt;}' ) self.slider.addWidget(self.label3) self.buttonPrevious = QPushButton() self.buttonPrevious.setText('Previous Slide') self.buttonPrevious.clicked.connect(self.slider.slidePrevious) self.buttonNext = QPushButton() self.buttonNext.setText('Next Slide') self.buttonNext.clicked.connect(self.slider.slideNext) self.buttonLayout = QHBoxLayout() self.buttonLayout.addWidget(self.buttonPrevious) self.buttonLayout.addWidget(self.buttonNext) self.vBoxLayout.addWidget(self.slider) self.vBoxLayout.addLayout(self.buttonLayout) self.centralWidget = QWidget(self) self.centralWidget.setLayout(self.vBoxLayout) self.setCentralWidget(self.centralWidget) self.show()
def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) # region Create CartPole instance and load initial settings # Create CartPole instance self.initial_state = create_cartpole_state() self.CartPoleInstance = CartPole(initial_state=self.initial_state) # Set timescales self.CartPoleInstance.dt_simulation = dt_simulation self.CartPoleInstance.dt_controller = controller_update_interval self.CartPoleInstance.dt_save = save_interval # set other settings self.CartPoleInstance.set_controller(controller_init) self.CartPoleInstance.stop_at_90 = stop_at_90_init self.set_random_experiment_generator_init_params() # endregion # region Decide whether to save the data in "CartPole memory" or not self.save_history = save_history_init self.show_experiment_summary = show_experiment_summary_init if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # endregion # region Other variables initial values as provided in gui_default_parameters.py # Start user controlled experiment/ start random experiment/ load and replay - on start button self.simulator_mode = simulator_mode_init self.slider_on_click = slider_on_click_init # Update slider on click/update slider while hoovering over it self.speedup = speedup_init # Default simulation speed-up # endregion # region Initialize loop-timer # This timer allows to relate the simulation time to user time # And (if your computer is fast enough) run simulation # slower or faster than real-time by predefined factor (speedup) self.looper = loop_timer( dt_target=(self.CartPoleInstance.dt_simulation / self.speedup)) # endregion # region Variables controlling the state of various processes (DO NOT MODIFY) self.terminate_experiment_or_replay_thread = False # True: gives signal causing thread to terminate self.pause_experiment_or_replay_thread = False # True: gives signal causing the thread to pause self.run_set_labels_thread = True # True if gauges (labels) keep being repeatedly updated # Stop threads by setting False # Flag indicating if the "START! / STOP!" button should act as start or as stop when pressed. # Can take values "START!" or "STOP!" self.start_or_stop_action = "START!" # Flag indicating whether the pause button should pause or unpause. self.pause_or_unpause_action = "PAUSE" # Flag indicating that saving of experiment recording to csv file has finished self.experiment_or_replay_thread_terminated = False self.user_time_counter = 0 # Measures the user time # Slider instant value (which is draw in GUI) differs from value saved in CartPole instance # if the option updating slider "on-click" is enabled. self.slider_instant_value = self.CartPoleInstance.slider_value self.noise = 'OFF' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise # endregion # region Create GUI Layout # region - Create container for top level layout layout = QVBoxLayout() # endregion # region - Change geometry of the main window self.setGeometry(300, 300, 2500, 1000) # endregion # region - Matplotlib figures (CartPole drawing and Slider) # Draw Figure self.fig = Figure( figsize=(25, 10) ) # Regulates the size of Figure in inches, before scaling to window size. self.canvas = FigureCanvas(self.fig) self.fig.AxCart = self.canvas.figure.add_subplot(211) self.fig.AxSlider = self.canvas.figure.add_subplot(212) self.fig.AxSlider.set_ylim(0, 1) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) # Attach figure to the layout lf = QVBoxLayout() lf.addWidget(self.canvas) # endregion # region - Radio buttons selecting current controller self.rbs_controllers = [] for controller_name in self.CartPoleInstance.controller_names: self.rbs_controllers.append(QRadioButton(controller_name)) # Ensures that radio buttons are exclusive self.controllers_buttons_group = QButtonGroup() for button in self.rbs_controllers: self.controllers_buttons_group.addButton(button) lr_c = QVBoxLayout() lr_c.addStretch(1) for rb in self.rbs_controllers: rb.clicked.connect(self.RadioButtons_controller_selection) lr_c.addWidget(rb) lr_c.addStretch(1) self.rbs_controllers[self.CartPoleInstance.controller_idx].setChecked( True) # endregion # region - Create central part of the layout for figures and radio buttons and add it to the whole layout lc = QHBoxLayout() lc.addLayout(lf) lc.addLayout(lr_c) layout.addLayout(lc) # endregion # region - Gauges displaying current values of various states and parameters (time, velocity, angle,...) # First row ld = QHBoxLayout() # User time self.labTime = QLabel("User's time (s): ") self.timer = QTimer() self.timer.setInterval(100) # Tick every 1/10 of the second self.timer.timeout.connect(self.set_user_time_label) self.timer.start() ld.addWidget(self.labTime) # Speed, angle, motor power (Q) self.labSpeed = QLabel('Speed (m/s):') self.labAngle = QLabel('Angle (deg):') self.labMotor = QLabel('') self.labTargetPosition = QLabel('') ld.addWidget(self.labSpeed) ld.addWidget(self.labAngle) ld.addWidget(self.labMotor) ld.addWidget(self.labTargetPosition) layout.addLayout(ld) # Second row of labels # Simulation time, Measured (real) speed-up, slider-value ld2 = QHBoxLayout() self.labTimeSim = QLabel('Simulation Time (s):') ld2.addWidget(self.labTimeSim) self.labSpeedUp = QLabel('Speed-up (measured):') ld2.addWidget(self.labSpeedUp) self.labSliderInstant = QLabel('') ld2.addWidget(self.labSliderInstant) layout.addLayout(ld2) # endregion # region - Buttons "START!" / "STOP!", "PAUSE", "QUIT" self.bss = QPushButton("START!") self.bss.pressed.connect(self.start_stop_button) self.bp = QPushButton("PAUSE") self.bp.pressed.connect(self.pause_unpause_button) bq = QPushButton("QUIT") bq.pressed.connect(self.quit_application) lspb = QHBoxLayout() # Sub-Layout for Start/Stop and Pause Buttons lspb.addWidget(self.bss) lspb.addWidget(self.bp) # endregion # region - Sliders setting initial state and buttons for kicking the pole # Sliders setting initial position and angle lb = QVBoxLayout() # Layout for buttons lb.addLayout(lspb) lb.addWidget(bq) ip = QHBoxLayout() # Layout for initial position sliders self.initial_position_slider = QSlider( orientation=Qt.Orientation.Horizontal) self.initial_position_slider.setRange( -int(float(1000 * TrackHalfLength)), int(float(1000 * TrackHalfLength))) self.initial_position_slider.setValue(0) self.initial_position_slider.setSingleStep(1) self.initial_position_slider.valueChanged.connect( self.update_initial_position) self.initial_angle_slider = QSlider( orientation=Qt.Orientation.Horizontal) self.initial_angle_slider.setRange(-int(float(100 * np.pi)), int(float(100 * np.pi))) self.initial_angle_slider.setValue(0) self.initial_angle_slider.setSingleStep(1) self.initial_angle_slider.valueChanged.connect( self.update_initial_angle) ip.addWidget(QLabel("Initial position:")) ip.addWidget(self.initial_position_slider) ip.addWidget(QLabel("Initial angle:")) ip.addWidget(self.initial_angle_slider) ip.addStretch(0.01) # Slider setting latency self.LATENCY_SLIDER_RANGE_INT = 1000 self.latency_slider = QSlider(orientation=Qt.Orientation.Horizontal) self.latency_slider.setRange(0, self.LATENCY_SLIDER_RANGE_INT) self.latency_slider.setValue( int(self.CartPoleInstance.LatencyAdderInstance.latency * self.LATENCY_SLIDER_RANGE_INT / self.CartPoleInstance.LatencyAdderInstance.max_latency)) self.latency_slider.setSingleStep(1) self.latency_slider.valueChanged.connect(self.update_latency) ip.addWidget(QLabel("Latency:")) ip.addWidget(self.latency_slider) self.labLatency = QLabel('Latency (ms): {:.1f}'.format( self.CartPoleInstance.LatencyAdderInstance.latency * 1000)) ip.addWidget(self.labLatency) # Buttons activating noise self.rbs_noise = [] for mode_name in ['ON', 'OFF']: self.rbs_noise.append(QRadioButton(mode_name)) # Ensures that radio buttons are exclusive self.noise_buttons_group = QButtonGroup() for button in self.rbs_noise: self.noise_buttons_group.addButton(button) lr_n = QHBoxLayout() lr_n.addWidget(QLabel('Noise:')) for rb in self.rbs_noise: rb.clicked.connect(self.RadioButtons_noise_on_off) lr_n.addWidget(rb) self.rbs_noise[1].setChecked(True) ip.addStretch(0.01) ip.addLayout(lr_n) ip.addStretch(0.01) # Buttons giving kick to the pole kick_label = QLabel("Kick pole:") kick_left_button = QPushButton() kick_left_button.setText("Left") kick_left_button.adjustSize() kick_left_button.clicked.connect(self.kick_pole) kick_right_button = QPushButton() kick_right_button.setText("Right") kick_right_button.adjustSize() kick_right_button.clicked.connect(self.kick_pole) ip.addWidget(kick_label) ip.addWidget(kick_left_button) ip.addWidget(kick_right_button) lb.addLayout(ip) layout.addLayout(lb) # endregion # region - Text boxes and Combobox to provide settings concerning generation of random experiment l_generate_trace = QHBoxLayout() l_generate_trace.addWidget(QLabel('Random experiment settings:')) l_generate_trace.addWidget(QLabel('Length (s):')) self.textbox_length = QLineEdit() l_generate_trace.addWidget(self.textbox_length) l_generate_trace.addWidget(QLabel('Turning Points (m):')) self.textbox_turning_points = QLineEdit() l_generate_trace.addWidget(self.textbox_turning_points) l_generate_trace.addWidget(QLabel('Interpolation:')) self.cb_interpolation = QComboBox() self.cb_interpolation.addItems( ['0-derivative-smooth', 'linear', 'previous']) self.cb_interpolation.currentIndexChanged.connect( self.cb_interpolation_selectionchange) self.cb_interpolation.setCurrentText( self.CartPoleInstance.interpolation_type) l_generate_trace.addWidget(self.cb_interpolation) layout.addLayout(l_generate_trace) # endregion # region - Textbox to provide csv file name for saving or loading data l_text = QHBoxLayout() textbox_title = QLabel('CSV file name:') self.textbox = QLineEdit() l_text.addWidget(textbox_title) l_text.addWidget(self.textbox) layout.addLayout(l_text) # endregion # region - Make strip of layout for checkboxes l_cb = QHBoxLayout() # endregion # region - Textbox to provide the target speed-up value l_text_speedup = QHBoxLayout() tx_speedup_title = QLabel('Speed-up (target):') self.tx_speedup = QLineEdit() l_text_speedup.addWidget(tx_speedup_title) l_text_speedup.addWidget(self.tx_speedup) self.tx_speedup.setText(str(self.speedup)) l_cb.addLayout(l_text_speedup) self.wrong_speedup_msg = QMessageBox() self.wrong_speedup_msg.setWindowTitle("Speed-up value problem") self.wrong_speedup_msg.setIcon(QMessageBox.Icon.Critical) # endregion # region - Checkboxes # region -- Checkbox: Save/don't save experiment recording self.cb_save_history = QCheckBox('Save results', self) if self.save_history: self.cb_save_history.toggle() self.cb_save_history.toggled.connect(self.cb_save_history_f) l_cb.addWidget(self.cb_save_history) # endregion # region -- Checkbox: Display plots showing dynamic evolution of the system as soon as experiment terminates self.cb_show_experiment_summary = QCheckBox('Show experiment summary', self) if self.show_experiment_summary: self.cb_show_experiment_summary.toggle() self.cb_show_experiment_summary.toggled.connect( self.cb_show_experiment_summary_f) l_cb.addWidget(self.cb_show_experiment_summary) # endregion # region -- Checkbox: Block pole if it reaches +/-90 deg self.cb_stop_at_90_deg = QCheckBox('Stop-at-90-deg', self) if self.CartPoleInstance.stop_at_90: self.cb_stop_at_90_deg.toggle() self.cb_stop_at_90_deg.toggled.connect(self.cb_stop_at_90_deg_f) l_cb.addWidget(self.cb_stop_at_90_deg) # endregion # region -- Checkbox: Update slider on click/update slider while hoovering over it self.cb_slider_on_click = QCheckBox('Update slider on click', self) if self.slider_on_click: self.cb_slider_on_click.toggle() self.cb_slider_on_click.toggled.connect(self.cb_slider_on_click_f) l_cb.addWidget(self.cb_slider_on_click) # endregion # endregion # region - Radio buttons selecting simulator mode: user defined experiment, random experiment, replay # List available simulator modes - constant self.available_simulator_modes = [ 'Slider-Controlled Experiment', 'Random Experiment', 'Replay' ] self.rbs_simulator_mode = [] for mode_name in self.available_simulator_modes: self.rbs_simulator_mode.append(QRadioButton(mode_name)) # Ensures that radio buttons are exclusive self.simulator_mode_buttons_group = QButtonGroup() for button in self.rbs_simulator_mode: self.simulator_mode_buttons_group.addButton(button) lr_sm = QHBoxLayout() lr_sm.addStretch(1) lr_sm.addWidget(QLabel('Simulator mode:')) for rb in self.rbs_simulator_mode: rb.clicked.connect(self.RadioButtons_simulator_mode) lr_sm.addWidget(rb) lr_sm.addStretch(1) self.rbs_simulator_mode[self.available_simulator_modes.index( self.simulator_mode)].setChecked(True) l_cb.addStretch(1) l_cb.addLayout(lr_sm) l_cb.addStretch(1) # endregion # region - Add checkboxes to layout layout.addLayout(l_cb) # endregion # region - Create an instance of a GUI window w = QWidget() w.setLayout(layout) self.setCentralWidget(w) self.show() self.setWindowTitle('CartPole Simulator') # endregion # endregion # region Open controller-specific popup windows self.open_additional_controller_widget() # endregion # region Activate functions capturing mouse movements and clicks over the slider # This line links function capturing the mouse position on the canvas of the Figure self.canvas.mpl_connect("motion_notify_event", self.on_mouse_movement) # This line links function capturing the mouse position on the canvas of the Figure click self.canvas.mpl_connect("button_press_event", self.on_mouse_click) # endregion # region Introducing multithreading # To ensure smooth functioning of the app, # the calculations and redrawing of the figures have to be done in a different thread # than the one capturing the mouse position and running the animation self.threadpool = QThreadPool() # endregion # region Starts a thread repeatedly redrawing gauges (labels) of the GUI # It runs till the QUIT button is pressed worker_labels = Worker(self.set_labels_thread) self.threadpool.start(worker_labels) # endregion # region Start animation repeatedly redrawing changing elements of matplotlib figures (CartPole drawing and slider) # This animation runs ALWAYS when the GUI is open # The buttons of GUI only decide if new parameters are calculated or not self.anim = self.CartPoleInstance.run_animation(self.fig)
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) # region Create CartPole instance and load initial settings # Create CartPole instance self.initial_state = create_cartpole_state() self.CartPoleInstance = CartPole(initial_state=self.initial_state) # Set timescales self.CartPoleInstance.dt_simulation = dt_simulation self.CartPoleInstance.dt_controller = controller_update_interval self.CartPoleInstance.dt_save = save_interval # set other settings self.CartPoleInstance.set_controller(controller_init) self.CartPoleInstance.stop_at_90 = stop_at_90_init self.set_random_experiment_generator_init_params() # endregion # region Decide whether to save the data in "CartPole memory" or not self.save_history = save_history_init self.show_experiment_summary = show_experiment_summary_init if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # endregion # region Other variables initial values as provided in gui_default_parameters.py # Start user controlled experiment/ start random experiment/ load and replay - on start button self.simulator_mode = simulator_mode_init self.slider_on_click = slider_on_click_init # Update slider on click/update slider while hoovering over it self.speedup = speedup_init # Default simulation speed-up # endregion # region Initialize loop-timer # This timer allows to relate the simulation time to user time # And (if your computer is fast enough) run simulation # slower or faster than real-time by predefined factor (speedup) self.looper = loop_timer( dt_target=(self.CartPoleInstance.dt_simulation / self.speedup)) # endregion # region Variables controlling the state of various processes (DO NOT MODIFY) self.terminate_experiment_or_replay_thread = False # True: gives signal causing thread to terminate self.pause_experiment_or_replay_thread = False # True: gives signal causing the thread to pause self.run_set_labels_thread = True # True if gauges (labels) keep being repeatedly updated # Stop threads by setting False # Flag indicating if the "START! / STOP!" button should act as start or as stop when pressed. # Can take values "START!" or "STOP!" self.start_or_stop_action = "START!" # Flag indicating whether the pause button should pause or unpause. self.pause_or_unpause_action = "PAUSE" # Flag indicating that saving of experiment recording to csv file has finished self.experiment_or_replay_thread_terminated = False self.user_time_counter = 0 # Measures the user time # Slider instant value (which is draw in GUI) differs from value saved in CartPole instance # if the option updating slider "on-click" is enabled. self.slider_instant_value = self.CartPoleInstance.slider_value self.noise = 'OFF' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise # endregion # region Create GUI Layout # region - Create container for top level layout layout = QVBoxLayout() # endregion # region - Change geometry of the main window self.setGeometry(300, 300, 2500, 1000) # endregion # region - Matplotlib figures (CartPole drawing and Slider) # Draw Figure self.fig = Figure( figsize=(25, 10) ) # Regulates the size of Figure in inches, before scaling to window size. self.canvas = FigureCanvas(self.fig) self.fig.AxCart = self.canvas.figure.add_subplot(211) self.fig.AxSlider = self.canvas.figure.add_subplot(212) self.fig.AxSlider.set_ylim(0, 1) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) # Attach figure to the layout lf = QVBoxLayout() lf.addWidget(self.canvas) # endregion # region - Radio buttons selecting current controller self.rbs_controllers = [] for controller_name in self.CartPoleInstance.controller_names: self.rbs_controllers.append(QRadioButton(controller_name)) # Ensures that radio buttons are exclusive self.controllers_buttons_group = QButtonGroup() for button in self.rbs_controllers: self.controllers_buttons_group.addButton(button) lr_c = QVBoxLayout() lr_c.addStretch(1) for rb in self.rbs_controllers: rb.clicked.connect(self.RadioButtons_controller_selection) lr_c.addWidget(rb) lr_c.addStretch(1) self.rbs_controllers[self.CartPoleInstance.controller_idx].setChecked( True) # endregion # region - Create central part of the layout for figures and radio buttons and add it to the whole layout lc = QHBoxLayout() lc.addLayout(lf) lc.addLayout(lr_c) layout.addLayout(lc) # endregion # region - Gauges displaying current values of various states and parameters (time, velocity, angle,...) # First row ld = QHBoxLayout() # User time self.labTime = QLabel("User's time (s): ") self.timer = QTimer() self.timer.setInterval(100) # Tick every 1/10 of the second self.timer.timeout.connect(self.set_user_time_label) self.timer.start() ld.addWidget(self.labTime) # Speed, angle, motor power (Q) self.labSpeed = QLabel('Speed (m/s):') self.labAngle = QLabel('Angle (deg):') self.labMotor = QLabel('') self.labTargetPosition = QLabel('') ld.addWidget(self.labSpeed) ld.addWidget(self.labAngle) ld.addWidget(self.labMotor) ld.addWidget(self.labTargetPosition) layout.addLayout(ld) # Second row of labels # Simulation time, Measured (real) speed-up, slider-value ld2 = QHBoxLayout() self.labTimeSim = QLabel('Simulation Time (s):') ld2.addWidget(self.labTimeSim) self.labSpeedUp = QLabel('Speed-up (measured):') ld2.addWidget(self.labSpeedUp) self.labSliderInstant = QLabel('') ld2.addWidget(self.labSliderInstant) layout.addLayout(ld2) # endregion # region - Buttons "START!" / "STOP!", "PAUSE", "QUIT" self.bss = QPushButton("START!") self.bss.pressed.connect(self.start_stop_button) self.bp = QPushButton("PAUSE") self.bp.pressed.connect(self.pause_unpause_button) bq = QPushButton("QUIT") bq.pressed.connect(self.quit_application) lspb = QHBoxLayout() # Sub-Layout for Start/Stop and Pause Buttons lspb.addWidget(self.bss) lspb.addWidget(self.bp) # endregion # region - Sliders setting initial state and buttons for kicking the pole # Sliders setting initial position and angle lb = QVBoxLayout() # Layout for buttons lb.addLayout(lspb) lb.addWidget(bq) ip = QHBoxLayout() # Layout for initial position sliders self.initial_position_slider = QSlider( orientation=Qt.Orientation.Horizontal) self.initial_position_slider.setRange( -int(float(1000 * TrackHalfLength)), int(float(1000 * TrackHalfLength))) self.initial_position_slider.setValue(0) self.initial_position_slider.setSingleStep(1) self.initial_position_slider.valueChanged.connect( self.update_initial_position) self.initial_angle_slider = QSlider( orientation=Qt.Orientation.Horizontal) self.initial_angle_slider.setRange(-int(float(100 * np.pi)), int(float(100 * np.pi))) self.initial_angle_slider.setValue(0) self.initial_angle_slider.setSingleStep(1) self.initial_angle_slider.valueChanged.connect( self.update_initial_angle) ip.addWidget(QLabel("Initial position:")) ip.addWidget(self.initial_position_slider) ip.addWidget(QLabel("Initial angle:")) ip.addWidget(self.initial_angle_slider) ip.addStretch(0.01) # Slider setting latency self.LATENCY_SLIDER_RANGE_INT = 1000 self.latency_slider = QSlider(orientation=Qt.Orientation.Horizontal) self.latency_slider.setRange(0, self.LATENCY_SLIDER_RANGE_INT) self.latency_slider.setValue( int(self.CartPoleInstance.LatencyAdderInstance.latency * self.LATENCY_SLIDER_RANGE_INT / self.CartPoleInstance.LatencyAdderInstance.max_latency)) self.latency_slider.setSingleStep(1) self.latency_slider.valueChanged.connect(self.update_latency) ip.addWidget(QLabel("Latency:")) ip.addWidget(self.latency_slider) self.labLatency = QLabel('Latency (ms): {:.1f}'.format( self.CartPoleInstance.LatencyAdderInstance.latency * 1000)) ip.addWidget(self.labLatency) # Buttons activating noise self.rbs_noise = [] for mode_name in ['ON', 'OFF']: self.rbs_noise.append(QRadioButton(mode_name)) # Ensures that radio buttons are exclusive self.noise_buttons_group = QButtonGroup() for button in self.rbs_noise: self.noise_buttons_group.addButton(button) lr_n = QHBoxLayout() lr_n.addWidget(QLabel('Noise:')) for rb in self.rbs_noise: rb.clicked.connect(self.RadioButtons_noise_on_off) lr_n.addWidget(rb) self.rbs_noise[1].setChecked(True) ip.addStretch(0.01) ip.addLayout(lr_n) ip.addStretch(0.01) # Buttons giving kick to the pole kick_label = QLabel("Kick pole:") kick_left_button = QPushButton() kick_left_button.setText("Left") kick_left_button.adjustSize() kick_left_button.clicked.connect(self.kick_pole) kick_right_button = QPushButton() kick_right_button.setText("Right") kick_right_button.adjustSize() kick_right_button.clicked.connect(self.kick_pole) ip.addWidget(kick_label) ip.addWidget(kick_left_button) ip.addWidget(kick_right_button) lb.addLayout(ip) layout.addLayout(lb) # endregion # region - Text boxes and Combobox to provide settings concerning generation of random experiment l_generate_trace = QHBoxLayout() l_generate_trace.addWidget(QLabel('Random experiment settings:')) l_generate_trace.addWidget(QLabel('Length (s):')) self.textbox_length = QLineEdit() l_generate_trace.addWidget(self.textbox_length) l_generate_trace.addWidget(QLabel('Turning Points (m):')) self.textbox_turning_points = QLineEdit() l_generate_trace.addWidget(self.textbox_turning_points) l_generate_trace.addWidget(QLabel('Interpolation:')) self.cb_interpolation = QComboBox() self.cb_interpolation.addItems( ['0-derivative-smooth', 'linear', 'previous']) self.cb_interpolation.currentIndexChanged.connect( self.cb_interpolation_selectionchange) self.cb_interpolation.setCurrentText( self.CartPoleInstance.interpolation_type) l_generate_trace.addWidget(self.cb_interpolation) layout.addLayout(l_generate_trace) # endregion # region - Textbox to provide csv file name for saving or loading data l_text = QHBoxLayout() textbox_title = QLabel('CSV file name:') self.textbox = QLineEdit() l_text.addWidget(textbox_title) l_text.addWidget(self.textbox) layout.addLayout(l_text) # endregion # region - Make strip of layout for checkboxes l_cb = QHBoxLayout() # endregion # region - Textbox to provide the target speed-up value l_text_speedup = QHBoxLayout() tx_speedup_title = QLabel('Speed-up (target):') self.tx_speedup = QLineEdit() l_text_speedup.addWidget(tx_speedup_title) l_text_speedup.addWidget(self.tx_speedup) self.tx_speedup.setText(str(self.speedup)) l_cb.addLayout(l_text_speedup) self.wrong_speedup_msg = QMessageBox() self.wrong_speedup_msg.setWindowTitle("Speed-up value problem") self.wrong_speedup_msg.setIcon(QMessageBox.Icon.Critical) # endregion # region - Checkboxes # region -- Checkbox: Save/don't save experiment recording self.cb_save_history = QCheckBox('Save results', self) if self.save_history: self.cb_save_history.toggle() self.cb_save_history.toggled.connect(self.cb_save_history_f) l_cb.addWidget(self.cb_save_history) # endregion # region -- Checkbox: Display plots showing dynamic evolution of the system as soon as experiment terminates self.cb_show_experiment_summary = QCheckBox('Show experiment summary', self) if self.show_experiment_summary: self.cb_show_experiment_summary.toggle() self.cb_show_experiment_summary.toggled.connect( self.cb_show_experiment_summary_f) l_cb.addWidget(self.cb_show_experiment_summary) # endregion # region -- Checkbox: Block pole if it reaches +/-90 deg self.cb_stop_at_90_deg = QCheckBox('Stop-at-90-deg', self) if self.CartPoleInstance.stop_at_90: self.cb_stop_at_90_deg.toggle() self.cb_stop_at_90_deg.toggled.connect(self.cb_stop_at_90_deg_f) l_cb.addWidget(self.cb_stop_at_90_deg) # endregion # region -- Checkbox: Update slider on click/update slider while hoovering over it self.cb_slider_on_click = QCheckBox('Update slider on click', self) if self.slider_on_click: self.cb_slider_on_click.toggle() self.cb_slider_on_click.toggled.connect(self.cb_slider_on_click_f) l_cb.addWidget(self.cb_slider_on_click) # endregion # endregion # region - Radio buttons selecting simulator mode: user defined experiment, random experiment, replay # List available simulator modes - constant self.available_simulator_modes = [ 'Slider-Controlled Experiment', 'Random Experiment', 'Replay' ] self.rbs_simulator_mode = [] for mode_name in self.available_simulator_modes: self.rbs_simulator_mode.append(QRadioButton(mode_name)) # Ensures that radio buttons are exclusive self.simulator_mode_buttons_group = QButtonGroup() for button in self.rbs_simulator_mode: self.simulator_mode_buttons_group.addButton(button) lr_sm = QHBoxLayout() lr_sm.addStretch(1) lr_sm.addWidget(QLabel('Simulator mode:')) for rb in self.rbs_simulator_mode: rb.clicked.connect(self.RadioButtons_simulator_mode) lr_sm.addWidget(rb) lr_sm.addStretch(1) self.rbs_simulator_mode[self.available_simulator_modes.index( self.simulator_mode)].setChecked(True) l_cb.addStretch(1) l_cb.addLayout(lr_sm) l_cb.addStretch(1) # endregion # region - Add checkboxes to layout layout.addLayout(l_cb) # endregion # region - Create an instance of a GUI window w = QWidget() w.setLayout(layout) self.setCentralWidget(w) self.show() self.setWindowTitle('CartPole Simulator') # endregion # endregion # region Open controller-specific popup windows self.open_additional_controller_widget() # endregion # region Activate functions capturing mouse movements and clicks over the slider # This line links function capturing the mouse position on the canvas of the Figure self.canvas.mpl_connect("motion_notify_event", self.on_mouse_movement) # This line links function capturing the mouse position on the canvas of the Figure click self.canvas.mpl_connect("button_press_event", self.on_mouse_click) # endregion # region Introducing multithreading # To ensure smooth functioning of the app, # the calculations and redrawing of the figures have to be done in a different thread # than the one capturing the mouse position and running the animation self.threadpool = QThreadPool() # endregion # region Starts a thread repeatedly redrawing gauges (labels) of the GUI # It runs till the QUIT button is pressed worker_labels = Worker(self.set_labels_thread) self.threadpool.start(worker_labels) # endregion # region Start animation repeatedly redrawing changing elements of matplotlib figures (CartPole drawing and slider) # This animation runs ALWAYS when the GUI is open # The buttons of GUI only decide if new parameters are calculated or not self.anim = self.CartPoleInstance.run_animation(self.fig) # endregion # region Thread performing CartPole experiment, slider-controlled or random # It iteratively updates CartPole state and save data to a .csv file # It also put simulation time in relation to user time def experiment_thread(self): # Necessary only for debugging in Visual Studio Code IDE try: ptvsd.debug_this_thread() except: pass self.looper.start_loop() while not self.terminate_experiment_or_replay_thread: if self.pause_experiment_or_replay_thread: time.sleep(0.1) else: # Calculations of the Cart state in the next timestep self.CartPoleInstance.update_state() # Terminate thread if random experiment reached its maximal length if ((self.CartPoleInstance.use_pregenerated_target_position is True) and (self.CartPoleInstance.time >= self.CartPoleInstance.t_max_pre)): self.terminate_experiment_or_replay_thread = True # FIXME: when Speedup empty in GUI I expected inf speedup but got error Loop timer was not initialized properly self.looper.sleep_leftover_time() # Save simulation history if user chose to do so at the end of the simulation if self.save_history: csv_name = self.textbox.text() self.CartPoleInstance.save_history_csv( csv_name=csv_name, mode='init', length_of_experiment=np.around( self.CartPoleInstance.dict_history['time'][-1], decimals=2)) self.CartPoleInstance.save_history_csv(csv_name=csv_name, mode='save offline') self.experiment_or_replay_thread_terminated = True # endregion # region Thread replaying a saved experiment recording def replay_thread(self): # Necessary only for debugging in Visual Studio Code IDE try: ptvsd.debug_this_thread() except: pass # Check what is in the csv textbox csv_name = self.textbox.text() # Load experiment history history_pd, filepath = self.CartPoleInstance.load_history_csv( csv_name=csv_name) # Set cartpole in the right mode (just to ensure slider behaves properly) with open(filepath, newline='') as f: reader = csv.reader(f) for line in reader: line = line[0] if line[:len('# Controller: ')] == '# Controller: ': controller_set = self.CartPoleInstance.set_controller( line[len('# Controller: '):].rstrip("\n")) if controller_set: self.rbs_controllers[self.CartPoleInstance. controller_idx].setChecked(True) else: self.rbs_controllers[1].setChecked( True) # Set first, but not manual stabilization break # Augment the experiment history with simulation time step size dt = [] row_iterator = history_pd.iterrows() _, last = next(row_iterator) # take first item from row_iterator for i, row in row_iterator: dt.append(row['time'] - last['time']) last = row dt.append(dt[-1]) history_pd['dt'] = np.array(dt) # Initialize loop timer (with arbitrary dt) replay_looper = loop_timer(dt_target=0.0) # Start looping over history replay_looper.start_loop() global L for index, row in history_pd.iterrows(): self.CartPoleInstance.s[POSITION_IDX] = row['position'] self.CartPoleInstance.s[POSITIOND_IDX] = row['positionD'] self.CartPoleInstance.s[ANGLE_IDX] = row['angle'] self.CartPoleInstance.time = row['time'] self.CartPoleInstance.dt = row['dt'] try: self.CartPoleInstance.u = row['u'] except KeyError: pass self.CartPoleInstance.Q = row['Q'] self.CartPoleInstance.target_position = row['target_position'] if self.CartPoleInstance.controller_name == 'manual-stabilization': self.CartPoleInstance.slider_value = self.CartPoleInstance.Q else: self.CartPoleInstance.slider_value = self.CartPoleInstance.target_position / TrackHalfLength # TODO: Make it more general for all possible parameters try: L[...] = row['L'] except KeyError: pass except: print('Error while assigning L') print("Unexpected error:", sys.exc_info()[0]) print("Unexpected error:", sys.exc_info()[1]) dt_target = (self.CartPoleInstance.dt / self.speedup) replay_looper.dt_target = dt_target replay_looper.sleep_leftover_time() if self.terminate_experiment_or_replay_thread: # Means that stop button was pressed break while self.pause_experiment_or_replay_thread: # Means that pause button was pressed time.sleep(0.1) if self.show_experiment_summary: self.CartPoleInstance.dict_history = history_pd.loc[:index].to_dict( orient='list') self.experiment_or_replay_thread_terminated = True # endregion # region "START! / STOP!" button -> run/stop slider-controlled experiment, random experiment or replay experiment recording # Actions to be taken when "START! / STOP!" button is clicked def start_stop_button(self): # If "START! / STOP!" button in "START!" mode... if self.start_or_stop_action == 'START!': self.bss.setText("STOP!") self.start_thread() # If "START! / STOP!" button in "STOP!" mode... elif self.start_or_stop_action == 'STOP!': self.bss.setText("START!") self.bp.setText("PAUSE") # This flag is periodically checked by thread. It terminates if set True. self.terminate_experiment_or_replay_thread = True # The stop_thread function is called automatically by the thread when it terminates # It is implemented this way, because thread my terminate not only due "STOP!" button # (e.g. replay thread when whole experiment is replayed) def pause_unpause_button(self): # Only Pause if experiment is running if self.pause_or_unpause_action == 'PAUSE' and self.start_or_stop_action == 'STOP!': self.pause_or_unpause_action = 'UNPAUSE' self.pause_experiment_or_replay_thread = True self.bp.setText("UNPAUSE") elif self.pause_or_unpause_action == 'UNPAUSE' and self.start_or_stop_action == 'STOP!': self.pause_or_unpause_action = 'PAUSE' self.pause_experiment_or_replay_thread = False self.bp.setText("PAUSE") # Run thread. works for all simulator modes. def start_thread(self): # Check if value provided in speed-up textbox makes sense # If not abort start speedup_updated = self.get_speedup() if not speedup_updated: return # Disable GUI elements for features which must not be changed in runtime # For other features changing in runtime may not cause errors, but will stay without effect for current run self.cb_save_history.setEnabled(False) for rb in self.rbs_simulator_mode: rb.setEnabled(False) for rb in self.rbs_controllers: rb.setEnabled(False) if self.simulator_mode != 'Replay': self.cb_show_experiment_summary.setEnabled(False) # Set user-provided initial values for state (or its part) of the CartPole # Search implementation for more detail # The following line is important as it let the user to set with the slider the starting target position # After the slider was reset at the end of last experiment # With the small sliders he can also adjust starting initial_state self.reset_variables( 2, s=np.copy(self.initial_state), target_position=self.CartPoleInstance.target_position) if self.simulator_mode == 'Random Experiment': self.CartPoleInstance.use_pregenerated_target_position = True if self.textbox_length.text() == '': self.CartPoleInstance.length_of_experiment = length_of_experiment_init else: self.CartPoleInstance.length_of_experiment = float( self.textbox_length.text()) turning_points_list = [] if self.textbox_turning_points.text() != '': for turning_point in self.textbox_turning_points.text().split( ', '): turning_points_list.append(float(turning_point)) self.CartPoleInstance.turning_points = turning_points_list self.CartPoleInstance.setup_cartpole_random_experiment() self.looper.dt_target = self.CartPoleInstance.dt_simulation / self.speedup # Pass the function to execute if self.simulator_mode == "Replay": worker = Worker(self.replay_thread) elif self.simulator_mode == 'Slider-Controlled Experiment' or self.simulator_mode == 'Random Experiment': worker = Worker(self.experiment_thread) worker.signals.finished.connect(self.finish_thread) # Execute self.threadpool.start(worker) # Determine what should happen when "START! / STOP!" is pushed NEXT time self.start_or_stop_action = "STOP!" # finish_threads works for all simulation modes # Some lines mya be redundant for replay, # however as they do not take much computation time we leave them here # As it my code shorter, while hopefully still clear. # It is called automatically at the end of experiment_thread def finish_thread(self): self.CartPoleInstance.use_pregenerated_target_position = False self.initial_state = create_cartpole_state() self.initial_position_slider.setValue(0) self.initial_angle_slider.setValue(0) self.CartPoleInstance.s = self.initial_state # Some controllers may collect they own statistics about their usage and print it after experiment terminated if self.simulator_mode != 'Replay': try: self.CartPoleInstance.controller.controller_report() except: pass if self.show_experiment_summary: self.w_summary = SummaryWindow( summary_plots=self.CartPoleInstance.summary_plots) # Reset variables and redraw the figures self.reset_variables(0) # Draw figures self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) self.canvas.draw() # Enable back all elements of GUI: self.cb_save_history.setEnabled(True) self.cb_show_experiment_summary.setEnabled(True) for rb in self.rbs_simulator_mode: rb.setEnabled(True) for rb in self.rbs_controllers: rb.setEnabled(True) self.start_or_stop_action = "START!" # What should happen when "START! / STOP!" is pushed NEXT time # endregion # region Methods: "Get, set, reset, quit" # Set parameters from gui_default_parameters related to generating a random experiment target position def set_random_experiment_generator_init_params(self): self.CartPoleInstance.track_relative_complexity = track_relative_complexity_init self.CartPoleInstance.length_of_experiment = length_of_experiment_init self.CartPoleInstance.interpolation_type = interpolation_type_init self.CartPoleInstance.turning_points_period = turning_points_period_init self.CartPoleInstance.start_random_target_position_at = start_random_target_position_at_init self.CartPoleInstance.end_random_target_position_at = end_random_target_position_at_init self.CartPoleInstance.turning_points = turning_points_init # Method resetting variables which change during experimental run def reset_variables(self, reset_mode=1, s=None, target_position=None): self.CartPoleInstance.set_cartpole_state_at_t0( reset_mode, s=s, target_position=target_position) self.user_time_counter = 0 # "Try" because this function is called for the first time during initialisation of the Window # when the timer label instance is not yer there. try: self.labt.setText("Time (s): " + str(float(self.user_time_counter) / 10.0)) except: pass self.experiment_or_replay_thread_terminated = False # This is a flag informing thread terminated self.terminate_experiment_or_replay_thread = False # This is a command to terminate a thread self.pause_experiment_or_replay_thread = False # This is a command to pause a thread self.start_or_stop_action = "START!" self.pause_or_unpause_action = "PAUSE" self.looper.first_call_done = False ###################################################################################################### # (Marcin) Below are methods with less critical functions. # A thread redrawing labels (except for timer, which has its own function) of GUI every 0.1 s def set_labels_thread(self): while (self.run_set_labels_thread): self.labSpeed.setText( "Speed (m/s): " + str(np.around(self.CartPoleInstance.s[POSITIOND_IDX], 2))) self.labAngle.setText("Angle (deg): " + str( np.around( self.CartPoleInstance.s[ANGLE_IDX] * 360 / (2 * np.pi), 2))) self.labMotor.setText("Motor power (Q): {:.3f}".format( np.around(self.CartPoleInstance.Q, 2))) if self.CartPoleInstance.controller_name == 'manual-stabilization': self.labTargetPosition.setText("") else: self.labTargetPosition.setText( "Target position (m): " + str(np.around(self.CartPoleInstance.target_position, 2))) if self.CartPoleInstance.controller_name == 'manual_stabilization': self.labSliderInstant.setText( "Slider instant value (-): " + str(np.around(self.slider_instant_value, 2))) else: self.labSliderInstant.setText( "Slider instant value (m): " + str(np.around(self.slider_instant_value, 2))) self.labTimeSim.setText('Simulation time (s): {:.2f}'.format( self.CartPoleInstance.time)) mean_dt_real = np.mean(self.looper.circ_buffer_dt_real) if mean_dt_real > 0: self.labSpeedUp.setText('Speed-up (measured): x{:.2f}'.format( self.CartPoleInstance.dt_simulation / mean_dt_real)) sleep(0.1) # Function to measure the time of simulation as experienced by user # It corresponds to the time of simulation according to equations only if real time mode is on # TODO (Marcin) I just retained this function from some example being my starting point # It seems it sometimes counting time to slow. Consider replacing in future def set_user_time_label(self): # "If": Increment time counter only if simulation is running if self.start_or_stop_action == "STOP!": # indicates what start button was pressed and some process is running self.user_time_counter += 1 # The updates are done smoother if the label is updated here # and not in the separate thread self.labTime.setText("Time (s): " + str(float(self.user_time_counter) / 10.0)) # The actions which has to be taken to properly terminate the application # The method is evoked after QUIT button is pressed # TODO: Can we connect it somehow also the the default cross closing the application? def quit_application(self): # Stops animation (updating changing elements of the Figure) self.anim._stop() # Stops the two threads updating the GUI labels and updating the state of Cart instance self.run_set_labels_thread = False self.terminate_experiment_or_replay_thread = True self.pause_experiment_or_replay_thread = False # Closes the GUI window self.close() # The standard command # It seems however not to be working by its own # I don't know how it works QApplication.quit() # endregion # region Mouse interaction """ These are some methods GUI uses to capture mouse effect while hoovering or clicking over/on the charts """ # Function evoked at a mouse movement # If the mouse cursor is over the lower chart it reads the corresponding value # and updates the slider def on_mouse_movement(self, event): if self.simulator_mode == 'Slider-Controlled Experiment': if event.xdata == None or event.ydata == None: pass else: if event.inaxes == self.fig.AxSlider: self.slider_instant_value = event.xdata if not self.slider_on_click: self.CartPoleInstance.update_slider( mouse_position=event.xdata) # Function evoked at a mouse click # If the mouse cursor is over the lower chart it reads the corresponding value # and updates the slider def on_mouse_click(self, event): if self.simulator_mode == 'Slider-Controlled Experiment': if event.xdata == None or event.ydata == None: pass else: if event.inaxes == self.fig.AxSlider: self.CartPoleInstance.update_slider( mouse_position=event.xdata) # endregion # region Changing "static" options: radio buttons, text boxes, combo boxes, check boxes """ This section collects methods used to change some ''static option'': e.g. change current controller, switch between saving and not saving etc. These are functions associated with radio buttons, check boxes, textfilds etc. The functions of "START! / STOP!" button is much more complex and we put them hence in a separate section. """ # region - Radio buttons # Chose the controller method which should be used with the CartPole def RadioButtons_controller_selection(self): # Change the mode variable depending on the Radiobutton state for i in range(len(self.rbs_controllers)): if self.rbs_controllers[i].isChecked(): self.CartPoleInstance.set_controller(controller_idx=i) # Reset the state of GUI and of the Cart instance after the mode has changed # TODO: Do I need the follwowing lines? self.reset_variables(0) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) self.canvas.draw() self.open_additional_controller_widget() # Chose the simulator mode - effect of start/stop button def RadioButtons_simulator_mode(self): # Change the mode variable depending on the Radiobutton state for i in range(len(self.rbs_simulator_mode)): sleep(0.001) if self.rbs_simulator_mode[i].isChecked(): self.simulator_mode = self.available_simulator_modes[i] # Reset the state of GUI and of the Cart instance after the mode has changed # TODO: Do I need the follwowing lines? self.reset_variables(0) self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart, self.fig.AxSlider) self.canvas.draw() # Chose the noise mode - effect of start/stop button def RadioButtons_noise_on_off(self): # Change the mode variable depending on the Radiobutton state if self.rbs_noise[0].isChecked(): self.noise = 'ON' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise elif self.rbs_noise[1].isChecked(): self.noise = 'OFF' self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise else: raise Exception('Something wrong with ON/OFF button for noise') self.open_additional_noise_widget() # endregion # region - Text Boxes # Read speedup provided by user from appropriate GUI textbox def get_speedup(self): """ Get speedup provided by user from appropriate textbox. Speed-up gives how many times faster or slower than real time the simulation or replay should run. The provided values may not always be reached due to computer speed limitation """ speedup = self.tx_speedup.text() if speedup == '': self.speedup = np.inf return True else: try: speedup = float(speedup) except ValueError: self.wrong_speedup_msg.setText( 'You have provided the input for speed-up which is not convertible to a number' ) x = self.wrong_speedup_msg.exec_() return False if speedup == 0.0: self.wrong_speedup_msg.setText( 'You cannot run an experiment with 0 speed-up (stopped time flow)' ) x = self.wrong_speedup_msg.exec_() return False else: self.speedup = speedup return True # endregion # region - Combo Boxes # Select how to interpolate between turning points of randomly chosen target positions def cb_interpolation_selectionchange(self, i): """ Select interpolation type for random target positions of randomly generated experiment """ self.CartPoleInstance.interpolation_type = self.cb_interpolation.currentText( ) # endregion # region - Check boxes # Action toggling between saving and not saving simulation results def cb_save_history_f(self, state): if state: self.save_history = 1 else: self.save_history = 0 if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # Action toggling between saving and not saving simulation results def cb_show_experiment_summary_f(self, state): if state: self.show_experiment_summary = 1 else: self.show_experiment_summary = 0 if self.save_history or self.show_experiment_summary: self.CartPoleInstance.save_data_in_cart = True else: self.CartPoleInstance.save_data_in_cart = False # Action toggling between stopping (or not) the pole if it reaches 90 deg def cb_stop_at_90_deg_f(self, state): if state: self.CartPoleInstance.stop_at_90 = True else: self.CartPoleInstance.stop_at_90 = False # Action toggling between updating CarPole slider value on click or by hoovering over it def cb_slider_on_click_f(self, state): if state: self.slider_on_click = True else: self.slider_on_click = False # endregion # region - Additional GUI Popups def open_additional_controller_widget(self): # Open up additional options widgets depending on the controller type if self.CartPoleInstance.controller_name == 'mppi': self.optionsControllerWidget = MPPIOptionsWindow() else: try: self.optionsControllerWidget.close() except: pass self.optionsControllerWidget = None def open_additional_noise_widget(self): # Open up additional options widgets depending on the controller type if self.noise == 'ON': self.optionsNoiseWidget = NoiseOptionsWindow() else: try: self.optionsNoiseWidget.close() except: pass self.optionsNoiseWidget = None # endregion # region - Sliders setting initial position and angle of the CartPole def update_initial_position(self, value: str): self.initial_state[POSITION_IDX] = float(value) / 1000.0 def update_initial_angle(self, value: str): self.initial_state[ANGLE_IDX] = float(value) / 100.0 # endregion # region - Slider setting latency of the controller def update_latency(self, value: str): latency_slider = float(value) latency = latency_slider * self.CartPoleInstance.LatencyAdderInstance.max_latency / self.LATENCY_SLIDER_RANGE_INT # latency in seconds self.CartPoleInstance.LatencyAdderInstance.set_latency(latency) self.labLatency.setText('{:.1f} ms'.format(latency * 1000.0)) # latency in ms # endregion # region Buttons for providing a kick to the pole def kick_pole(self): if self.sender().text() == "Left": self.CartPoleInstance.s[ANGLED_IDX] += .6 elif self.sender().text() == "Right": self.CartPoleInstance.s[ANGLED_IDX] -= .6
class UI(QWidget): update_signal = pyqtSignal(str) show_qr_signal = pyqtSignal(bytes) finish_signal = pyqtSignal() close_qr_signal = pyqtSignal() def __init__(self): super().__init__() self.logger = logging.getLogger(__name__) formatter = logging.Formatter( fmt="%(asctime)s-%(levelname)s-%(message)s", datefmt="%Y-%m-%d %H:%M:%S") filehandler = logging.FileHandler(filename="logs.log", mode="w", encoding="utf-8") handler = QLogger(update_signal=self.update_signal) handler.setLevel(logging.INFO) filehandler.setLevel(logging.INFO) self.logger.setLevel(logging.INFO) if os.path.exists("config.json") == False: self.gen_conf() with open(file="config.json", mode="r", encoding="utf-8") as conf_reader: conf = json.loads(conf_reader.read()) debug = bool(conf["debug"]) if debug == True: handler.setLevel(logging.DEBUG) filehandler.setLevel(logging.DEBUG) self.logger.setLevel(logging.DEBUG) handler.setFormatter(formatter) filehandler.setFormatter(formatter) self.logger.addHandler(handler) self.logger.addHandler(filehandler) self.logger.debug("当前调试状态:%s" % debug) self.resize(1024, 768) self.setWindowOpacity(0.9) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setWindowFlag(Qt.WindowFlags.FramelessWindowHint) self.setAutoFillBackground(True) self.work = Work(show_qr_signal=self.show_qr_signal, finish_signal=self.finish_signal, close_qr_signal=self.close_qr_signal) self.work_thread = QThread() self.work.moveToThread(self.work_thread) self.main_layout = QGridLayout() self.setLayout(self.main_layout) self.title = QLabel("ChinaUniOnlineGUI") self.title.setStyleSheet( "QLabel{border:none;border-radius:5px;background:transparent;color:#9AD3BC;font-size:60px;}" ) self.title.setAlignment(Qt.Alignment.AlignCenter) handler.widget.setStyleSheet( "QPlainTextEdit{font-family:Microsoft YaHei;background:#F3EAC2;border:none;border-radius:5px;}QScrollBar:vertical,QScrollBar::handle:vertical{background:#F3EAC2;border:none;border-radius:8px;width:16px;}QScrollBar::handle:vertical:hover{background:#F5B461;}QScrollBar::add-page:vertical,QScrollBar::sub-page:vertical{background:#FFFDF9;border:none;border-radius:8px;width:16px;}QScrollBar::down-arrow:vertical,QScrollBar::up-arrow:vertical{background:#F5B461;border:none;border-radius:8px;width:16px;height:16px;}QScrollBar::sub-line:vertical,QScrollBar::add-line:vertical{background:transparent;border:none;}" ) self.control = QVBoxLayout() self.control_close = QPushButton() self.control_close.setToolTip("关闭") self.control_close.setStyleSheet( "QPushButton{background:#FFE3ED;border-radius:5px;border:none;}QPushButton:hover{background:#EC524B;}" ) self.contron_max = QPushButton() if self.isMaximized() == False: self.contron_max.setToolTip("最大化") else: self.contron_max.setToolTip("还原") self.contron_max.setStyleSheet( "QPushButton{background:#FFFDF9;border-radius:5px;border:none;}QPushButton:hover{background:#F5B461;}" ) self.control_min = QPushButton() self.control_min.setToolTip("最小化") self.control_min.setStyleSheet( "QPushButton{background:#BEEBE9;border-radius:5px;border:none;}QPushButton:hover{background:#F3EAC2;}" ) self.start_button = QPushButton("开始(&S)") self.start_button.setStyleSheet( "QPushButton{background:#9BE3DE;border:none;border-radius:5px;font-size:20px;font-family:DengXian;}QPushButton:hover{background:#9AD3BC;}" ) self.start_button.setToolTip("开始") self.start_button.setFixedSize(120, 60) self.start_button.setDefault(True) setting_button = QPushButton("设置") setting_button.setToolTip("设置") setting_button.setFixedSize(60, 60) setting_button.setStyleSheet( "QPushButton{background:#9BE3DE;border:none;border-radius:5px;font-size:20px;font-family:DengXian;}QPushButton:hover{background:#9AD3BC;}" ) setting_button.clicked.connect(self.setting_callback) start = QHBoxLayout() start.addWidget(self.start_button, 2) start.addWidget(setting_button, 1) self.control_close.clicked.connect(self.close) self.control_min.clicked.connect(self.min_callback) self.contron_max.clicked.connect(self.max_callback) self.start_button.clicked.connect(self.start_callback) self.work_thread.started.connect(self.work.start) self.finish_signal.connect(self.finish_callback) self.close_qr_signal.connect(self.close_qr) self.control.addWidget(self.control_min) self.control.addWidget(self.contron_max) self.control.addWidget(self.control_close) self.main_layout.addLayout(self.control, 0, 0) self.main_layout.addWidget(self.title, 0, 1) self.main_layout.addLayout(start, 0, 2) self.main_layout.addWidget(handler.widget, 1, 1) self.update_signal.connect(handler.widget.appendPlainText) handler.widget.textChanged.connect(handler.scroll_widget_to_bottom) self.show_qr_signal.connect(self.show_qr) self.logger.debug("已初始化UI") def min_callback(self): if self.isMinimized() == False: self.showMinimized() def max_callback(self): if self.isMaximized() == False: self.showMaximized() self.contron_max.setToolTip("还原") else: self.showNormal() self.contron_max.setToolTip("最大化") def start_callback(self): self.start_time = time.time() self.work_thread.start() self.start_button.setEnabled(False) self.start_button.setText("执行中...") def finish_callback(self): self.start_button.setEnabled(True) self.start_button.setText("开始") passed_time = time.time() - self.start_time mins, secs = divmod(passed_time, 60) hours, mins = divmod(mins, 60) self.logger.info("执行完成,共计用时 {:0>2d}:{:0>2d}:{:0>2d}".format( int(hours), int(mins), int(secs))) def show_qr(self, qr: bytes): title_label = QLabel("请使用微信扫描小程序码完成登陆") title_label.setStyleSheet( "QLabel{color:#ffe3ed;border:none;background-color:transparent;border-radius:5px;}" ) title_label.setAlignment(Qt.Alignment.AlignCenter) title_label.setFixedHeight(20) qr_label = QLabel() pixmap = QPixmap() pixmap.loadFromData(qr) qr_label.setPixmap(pixmap) qr_label.setStyleSheet( "QLabel{color:#ffe3ed;border:none;background-color:transparent;border-radius:5px;}" ) layout_ = QVBoxLayout() layout_.addWidget(title_label, 1) layout_.addWidget(qr_label, 9) self.qr_dialog = QWidget(self) self.qr_dialog.setLayout(layout_) self.main_layout.addWidget(self.qr_dialog, 1, 1, Qt.Alignment.AlignCenter) self.qr_dialog.show() def close_qr(self): self.qr_dialog.close() def setting_callback(self): setting = SettingWindow(parent=self) setting.setStyleSheet( "QDialog{border:none;border-radius:5px;background:#F3EAC2;}") setting.show() def gen_conf(self): default_conf = { "debug": False, "hero": { "title": "英雄篇", "enabled": True, "times": 1 }, "revival": { "title": "复兴篇", "enabled": True, "times": 1 }, "creation": { "title": "创新篇", "enabled": True, "times": 1 }, "belief": { "title": "信念篇", "enabled": True, "times": 1 }, "limit_time": { "title": "限时赛", "enabled": True, "times": 1 }, "rob": { "title": "抢十赛", "enabled": True, "times": 1 } } with open(file="config.json", mode="w", encoding="utf-8") as conf_writer: conf_writer.write( json.dumps(default_conf, indent=4, sort_keys=True, ensure_ascii=False)) self.logger.info("已生成默认配置文件") def mousePressEvent(self, event: QMouseEvent): self.logger.debug("触发鼠标按压事件") super().mousePressEvent(event) self.setFocus() self.m_flag = True if event.button() == Qt.MouseButtons.LeftButton and self.isMaximized( ) == False and self.hasFocus() == True: self.old_pos = event.globalPosition() #获取鼠标相对窗口的位置 self.logger.debug("已获取鼠标位置") self.setCursor(QtGui.QCursor( Qt.CursorShape.SizeAllCursor)) #更改鼠标图标 def mouseMoveEvent(self, event: QMouseEvent): self.logger.debug("触发鼠标移动事件") super().mouseMoveEvent(event) if self.m_flag == True: delta_x = int(event.globalPosition().x() - self.old_pos.x()) delta_y = int(event.globalPosition().y() - self.old_pos.y()) self.move(self.x() + delta_x, self.y() + delta_y) #更改窗口位置 self.logger.debug("已更改窗口位置") self.old_pos = event.globalPosition() def mouseReleaseEvent(self, event: QMouseEvent): self.logger.debug("触发鼠标释放事件") super().mouseReleaseEvent(event) self.m_flag = False self.setCursor(QtGui.QCursor(Qt.CursorShape.ArrowCursor))
class YTdownloader(QWidget): def __init__(self): super().__init__() # setup some flags self.isFetching = False self.isDownloading = False # default output path self.outputPath = f'{QDir.homePath()}/videos' # setup some window specific things self.setWindowTitle('YouTube Downloader') self.setWindowIcon(QIcon('assets/yt-icon.ico')) self.setFixedSize(705, 343) # parent layout layout = QVBoxLayout() layout.setContentsMargins(15, 15, 15, 10) self.setLayout(layout) # top bar layout topBar = QHBoxLayout() # detail section detailSec = QHBoxLayout() metaSec = QVBoxLayout() # download section downloadSec = QHBoxLayout() downloadBtn = QVBoxLayout() # output path link button self.outputBtn = QPushButton('📂 Output Path') self.outputBtn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.outputBtn.setToolTip(self.outputPath) self.outputBtn.clicked.connect(self.setOutputPath) # status bar self.statusBar = QStatusBar() # message box self.message = QMessageBox() # setting up widgets self.urlBox = QLineEdit() self.urlBox.setFocusPolicy(Qt.FocusPolicy.ClickFocus or Qt.FocusPolicy.NoFocus) self.urlBox.setPlaceholderText('🔍 Enter or paste video URL...') self.button = QPushButton('Get') self.button.setDefault(True) self.button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.button.clicked.connect(self.getDetails) # thumbnail pixmap = QPixmap('assets\placeholder.jpg') self.thumb = QLabel() self.thumb.setFixedSize(250, 141) self.thumb.setScaledContents(True) self.thumb.setPixmap(pixmap) # detail widgets self.title = QLabel('Title: ') self.author = QLabel('Author: ') self.length = QLabel('Duration: ') self.publish_date = QLabel('Published: ') # progress bar self.progress_bar = QProgressBar() # download options self.download = QComboBox() self.download.setPlaceholderText('Download Video') self.download.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.download.activated.connect(lambda: self.getContent(0)) self.download.setEnabled(False) # download audio button self.download_audio = QPushButton('Download Audio') self.download_audio.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.download_audio.clicked.connect(lambda: self.getContent(1)) self.download_audio.setEnabled(False) # add widgets and layouts topBar.addWidget(self.urlBox) topBar.addWidget(self.button) # detail section metaSec.addWidget(self.title) metaSec.addWidget(self.author) metaSec.addWidget(self.length) metaSec.addWidget(self.publish_date) detailSec.addWidget(self.thumb) detailSec.addSpacing(20) detailSec.addLayout(metaSec) # download section downloadBtn.addWidget(self.download) downloadBtn.addWidget(self.download_audio) downloadSec.addWidget(self.progress_bar) downloadSec.addSpacing(10) downloadSec.addLayout(downloadBtn) # status bar self.statusBar.setSizeGripEnabled(False) self.statusBar.addPermanentWidget(self.outputBtn) # add content to parent layout layout.addLayout(topBar) layout.addSpacing(20) layout.addLayout(detailSec) layout.addSpacing(5) layout.addLayout(downloadSec) layout.addWidget(self.statusBar) # setup a connection thread to keep checking internet connectivity self.connection = ConnectionThread() self.connection.start() # catch the connection response signal self.connection.con_response.connect(self.connection_slot) # connection slot def connection_slot(self, status): curMsg = self.statusBar.currentMessage() # connection succeeded if status: if curMsg == '🔴 Disconnected': self.statusBar.showMessage('🟢 Connection restored!', 3000) elif curMsg != '🟢 Connected': self.statusBar.showMessage('🟢 Connected') # connection failed elif curMsg == '🟢 Connected': self.statusBar.showMessage('🔴 Connection interrupted!', 3000) elif curMsg != '🔴 Disconnected': self.statusBar.showMessage('🔴 Disconnected') # set output path slot def setOutputPath(self): # update the output path path = str(QFileDialog.getExistingDirectory(self, "Select Output Directory")) if path: self.outputPath = path # update tooltip self.outputBtn.setToolTip(path) # get button slot def getDetails(self): curMsg = self.statusBar.currentMessage() if curMsg == '🔴 Disconnected' or curMsg == '🔴 Connection interrupted!': self.message.critical( self, 'Error', 'Connection failed!\nAre you sure you\'re connected to the internet ? ' ) elif self.button.text() == 'Get': self.button.setText('Stop') # indicate progress bar as busy self.progress_bar.setRange(0, 0) # set fetching flag self.isFetching = True # setup a worker thread to keep UI responsive self.worker = WorkerThread(self.urlBox.text()) self.worker.start() # catch the finished signal self.worker.finished.connect(self.finished_slot) # catch the response signal self.worker.worker_response.connect(self.response_slot) # catch the error signal self.worker.worker_err_response.connect(self.err_slot) elif self.button.text() == 'Stop': if self.isFetching: # stop worker thread self.worker.terminate() # set back the button text self.button.setText('Get') elif self.isDownloading: # stop download thread self.download_thread.terminate() # show the warning message self.message.information( self, 'Interrupted', 'Download interrupted!\nThe process was aborted while the file was being downloaded... ' ) # reset pogress bar self.progress_bar.reset() # download options slot def getContent(self, id): if self.isFetching: # show the warning message self.message.warning( self, 'Warning', 'Please wait!\nWait while the details are being fetched... ' ) else: # disable the download options self.download.setDisabled(True) self.download_audio.setDisabled(True) # set downloading flag self.isDownloading = True # set button to stop self.button.setText('Stop') # setup download thread if id == 0: self.download_thread = DownloadThread(self.yt, self.download.currentText()[:4], self.outputPath) else: self.download_thread = DownloadThread(self.yt, 'audio', self.outputPath) # start the thread self.download_thread.start() # catch the finished signal self.download_thread.finished.connect(self.download_finished_slot) # catch the response signal self.download_thread.download_response.connect(self.download_response_slot) # catch the complete signal self.download_thread.download_complete.connect(self.download_complete_slot) # catch the error signal self.download_thread.download_err.connect(self.download_err_slot) # finished slot def finished_slot(self): # remove progress bar busy indication self.progress_bar.setRange(0, 100) # unset fetching flag self.isFetching = False # response slot def response_slot(self, res): # set back the button text self.button.setText('Get') # save the yt object for speeding up download self.yt = res[0] # set the actual thumbnail of requested video self.thumb.setPixmap(res[1]) # slice the title if it is more than the limit if len(res[2]) > 50: self.title.setText(f'Title: {res[2][:50]}...') else: self.title.setText(f'Title: {res[2]}') # set leftover details self.author.setText(f'Author: {res[3]}') self.length.setText(f'Duration: {timedelta(seconds=res[4])}') self.publish_date.setText(f'Published: {res[5].strftime("%d/%m/%Y")}') # clear any previous items if any self.download.clear() # add resolutions as items to the download button and enable them self.download.addItems([item for item in res[6]]) self.download.setDisabled(False) self.download_audio.setDisabled(False) # error slot def err_slot(self): # show the warning message self.message.warning( self, 'Warning', 'Something went wrong!\nProbably a broken link or some restricted content... ' ) # set back the button text self.button.setText('Get') # download finished slot def download_finished_slot(self): # set back the button text self.button.setText('Get') # now enable the download options self.download.setDisabled(False) self.download_audio.setDisabled(False) # unset downloading flag self.isDownloading = False # reset pogress bar self.progress_bar.reset() # download response slot def download_response_slot(self, per): # update progress bar self.progress_bar.setValue(per) # adjust the font color to maintain the contrast if per > 52: self.progress_bar.setStyleSheet('QProgressBar { color: #fff }') else: self.progress_bar.setStyleSheet('QProgressBar { color: #000 }') # download complete slot def download_complete_slot(self, location): # use native separators location = QDir.toNativeSeparators(location) # show the success message if self.message.information( self, 'Downloaded', f'Download complete!\nFile was successfully downloaded to :\n{location}\n\nOpen the downloaded file now ?', QMessageBox.StandardButtons.Open, QMessageBox.StandardButtons.Cancel ) is QMessageBox.StandardButtons.Open: subprocess.Popen(f'explorer /select,{location}') # download error slot def download_err_slot(self): # show the error message self.message.critical( self, 'Error', 'Error!\nSomething unusual happened and was unable to download...' )
class B23Download(QWidget): def __init__(self): super(B23Download, self).__init__() # setup some flags self.is_fetching = False self.is_downloading = False # default output path basepath = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(basepath, "videos") self.output_path = path # setup some window specific things self.setWindowTitle("Bilibili Favorite Downloader") self.setWindowIcon(QIcon("images/icon_bilibili.ico")) self.setFixedSize(705, 343) # parent layout main_layout = QVBoxLayout() main_layout.setContentsMargins(15, 15, 15, 10) self.setLayout(main_layout) # top bar layout top_layout = QHBoxLayout() # detail section mid_main_layout = QHBoxLayout() mid_right_layout = QVBoxLayout() # download section bottom_main_layout = QHBoxLayout() bottom_right_layout = QVBoxLayout() # output path link button self.output_btn = QPushButton("📂 Output Path") self.output_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.output_btn.setToolTip(self.output_path) self.output_btn.clicked.connect(self.set_output_path) # status bar self.status_bar = QStatusBar() # message box self.message_box = QMessageBox() # setting up widgets self.url_edit = QLineEdit() self.url_edit.setPlaceholderText("🔍 Enter or paste favorite URL...") self.get_btn = QPushButton("Get") self.get_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.get_btn.clicked.connect(self.get_details) # thumbnail pixmap = QPixmap("images/placeholder.png") self.thumb = QLabel() self.thumb.setFixedSize(250, 141) self.thumb.setScaledContents(True) self.thumb.setPixmap(pixmap) # detail widgets self.title = QLabel("Title: ") self.author = QLabel("Author: ") self.length = QLabel("Videos: ") self.publish_date = QLabel("Published: ") # progress bar self.progress_bar = QProgressBar() # download options self.download_btn = QPushButton(" Download Videos ") self.download_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.download_btn.clicked.connect(self.get_content) self.download_btn.setEnabled(False) self.download_btn.setShortcut("Ctrl+Return") self.download_btn.setMinimumWidth(200) # add widgets and layouts top_layout.addWidget(self.url_edit) top_layout.addWidget(self.get_btn) # detail section mid_right_layout.addWidget(self.title) mid_right_layout.addWidget(self.author) mid_right_layout.addWidget(self.length) mid_right_layout.addWidget(self.publish_date) mid_main_layout.addWidget(self.thumb) mid_main_layout.addSpacing(20) mid_main_layout.addLayout(mid_right_layout) # download section bottom_right_layout.addWidget(self.download_btn) bottom_main_layout.addWidget(self.progress_bar) bottom_main_layout.addSpacing(10) bottom_main_layout.addLayout(bottom_right_layout) # status bar self.status_bar.setSizeGripEnabled(False) self.status_bar.addPermanentWidget(self.output_btn) # add content to parent layout main_layout.addLayout(top_layout) main_layout.addSpacing(20) main_layout.addLayout(mid_main_layout) main_layout.addSpacing(5) main_layout.addLayout(bottom_main_layout) main_layout.addWidget(self.status_bar) # set output path slot def set_output_path(self): # update the output path path = str( QFileDialog.getExistingDirectory(self, "Select Output Directory")) if path: self.output_path = path # update tooltip self.output_btn.setToolTip(path) # get button slot def get_details(self): text = self.url_edit.text().strip() if not text: return if text.find("fid") < 0: self.message_box.warning( self, "Error", ("Input a correct favorite URL!\n" "For example: https://space.bilibili.com/xxx/favlist?fid=xxx..." ), ) return if self.get_btn.text() == "Get": self.get_btn.setText("Stop") # indicate progress bar as busy self.progress_bar.setRange(0, 0) # set fetching flag self.is_fetching = True # setup a worker thread to keep UI responsive self.media_id = text.split("fid=")[-1].split("&")[0] self.worker = WorkerThread(self.media_id) self.worker.start() # catch the finished signal self.worker.finished.connect(self.finished_slot) # catch the response signal self.worker.worker_response.connect(self.response_slot) # catch the error signal self.worker.worker_err_response.connect(self.err_slot) elif self.get_btn.text() == "Stop": if self.is_fetching: # stop worker thread self.worker.terminate() # set back the get_btn text self.get_btn.setText("Get") elif self.is_downloading: # stop download thread self.download_thread.terminate() # show the warning message_box self.message_box.information( self, "Interrupted", "Download interrupted!\nThe process was aborted while the file was being downloaded... ", ) # reset progress bar self.progress_bar.reset() # download options slot def get_content(self): if self.is_fetching: # show the warning message self.message_box.critical( self, "Error", "Please wait!\nWait while the details are being fetched... ", ) else: # disable the download options self.download_btn.setDisabled(True) # set downloading flag self.is_downloading = True # set button to stop self.get_btn.setText("Stop") self.download_thread = DownloadThread( self.media_id, self.media_counts, self.first_page_medias, self.output_path, ) # start the thread self.download_thread.start() # catch the finished signal self.download_thread.finished.connect(self.download_finished_slot) # catch the response signal self.download_thread.download_response.connect( self.download_response_slot) # catch the complete signal self.download_thread.download_complete.connect( self.download_complete_slot) # catch the error signal self.download_thread.download_err.connect(self.download_err_slot) # handling enter key for get/stop button def keyPressEvent(self, event): self.url_edit.setFocus() if (event.key() == Qt.Key.Key_Enter.value or event.key() == Qt.Key.Key_Return.value): self.get_details() # finished slot def finished_slot(self): # remove progress bar busy indication self.progress_bar.setRange(0, 100) # unset fetching flag self.is_fetching = False # response slot def response_slot(self, res): # set back the button text self.get_btn.setText("Get") # set the actual thumbnail of requested video self.thumb.setPixmap(res.thumb_img) # slice the title if it is more than the limit if len(res.title) > 50: self.title.setText(f"Title: {res.title[:50]}...") else: self.title.setText(f"Title: {res.title}") # cache first page medias self.first_page_medias = res.medias self.media_counts = res.media_counts # set leftover details self.author.setText(f"Author: {res.author}") self.length.setText(f"Videos: {res.media_counts}") self.publish_date.setText( f'Published: {time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(res.publish_date))}' ) self.download_btn.setDisabled(False) # error slot def err_slot(self): # show the warning message self.message_box.warning( self, "Warning", "Something went wrong!\nProbably a broken link or some restricted content... ", ) # set back the button text self.get_btn.setText("Get") # download finished slot def download_finished_slot(self): # set back the button text self.get_btn.setText("Get") # now enable the download options self.download_btn.setDisabled(False) # unset downloading flag self.is_downloading = False # reset pogress bar self.progress_bar.reset() # download response slot def download_response_slot(self, per): # update progress bar self.progress_bar.setValue(per) # adjust the font color to maintain the contrast if per > 52: self.progress_bar.setStyleSheet("QProgressBar { color: #fff }") else: self.progress_bar.setStyleSheet("QProgressBar { color: #000 }") # download complete slot def download_complete_slot(self, location): # use native separators location = QDir.toNativeSeparators(location) # show the success message if (self.message_box.information( self, "Downloaded", f"Download complete!\nFile was successfully downloaded to :\n{location}\n\nOpen the downloaded file now ?", QMessageBox.StandardButtons.Open, QMessageBox.StandardButtons.Cancel, ) is QMessageBox.StandardButtons.Open): subprocess.Popen(f"explorer /select,{location}") # download error slot def download_err_slot(self): # show the error message self.message_box.critical( self, "Error", "Error!\nSomething unusual happened and was unable to download...", )
class UIComicInfoWidget(QWidget): load_chapter_list_signa = QtCore.pyqtSignal(ChapterInfo) load_download_task_signa = QtCore.pyqtSignal(DownloadTask) def __init__(self, comic_info: ComicInfo, down_v_box_layout: QVBoxLayout): super().__init__() self.comic_info = comic_info self.down_v_box_layout = down_v_box_layout self.img_label = QLabel(self) self.img_label.setScaledContents(True) img = QImage.fromData(comic_info.cover) w, h = image_resize(comic_info.cover, width=200) self.img_label.resize(QtCore.QSize(w, h)) self.img_label.setGeometry(10, 10, w, h) self.img_label.setPixmap(QPixmap.fromImage(img)) # self.img_label.setPixmap(QtGui.QPixmap("/Users/bo/my/tmp/老夫子2/第1卷/1.jpg")) self.title = QLabel(self) self.title.setGeometry(220, 10, 100, 40) title_font = QtGui.QFont() title_font.setPointSize(16) title_font.setBold(True) title_font.setUnderline(True) self.title.setFont(title_font) self.title.setText(comic_info.title) self.title.setWordWrap(True) info_font = QtGui.QFont() info_font.setPointSize(14) # 作者 self.author = QLabel(self) self.author.setText("作者 : " + comic_info.author) self.author.setGeometry(220, 50, 150, 40) self.author.setWordWrap(True) self.author.setFont(info_font) # 状态 self.status = QLabel(self) self.status.setText("更新状态 : " + comic_info.status) self.status.setGeometry(220, 90, 150, 40) self.status.setFont(info_font) # 热度 self.heat = QLabel(self) self.heat.setText("热度 : " + str(comic_info.heat)) self.heat.setGeometry(220, 130, 150, 40) self.heat.setFont(info_font) # 类型 self.tip = QLabel(self) self.tip.setText("类型 : " + comic_info.tip) self.tip.setGeometry(220, 170, 150, 40) self.tip.setWordWrap(True) self.tip.setFont(info_font) # web self.domain = QLabel(self) self.domain.setText(f"查看原网页 : {comic_info.domain}") self.domain.setText(f'查看原网页 : <a href="{comic_info.url}">{comic_info.domain}</a>') self.domain.setGeometry(220, 210, 150, 40) self.domain.setOpenExternalLinks(True) self.domain.setFont(info_font) # 描述 self.describe = QLabel(self) self.describe.setText(" " + comic_info.describe) self.describe.setGeometry(10, 320, 350, 330) self.describe.setWordWrap(True) # 对齐方式 self.describe.setAlignment( QtCore.Qt.AlignmentFlag.AlignLeading | QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop) # 章节列表,创建一个区域 self.searchHBoxLayout = QHBoxLayout() # self.searchHBoxLayout.addSpacing() self.searchGroupBox = QGroupBox() self.searchGroupBox.setLayout(self.searchHBoxLayout) self.searchScroll = QScrollArea(self) self.searchScroll.setGeometry(370, 10, 574, 590) self.searchScroll.setWidget(self.searchGroupBox) self.searchScroll.setWidgetResizable(True) # 全选 self.check_all = QCheckBox(self) self.check_all.setText("全选") self.check_all.setGeometry(700, 610, 100, 20) self.check_all.stateChanged.connect(self.check_all_fun) # 下载 self.down_button = QPushButton(self) self.down_button.setText("下载") self.down_button.setGeometry(780, 605, 50, 30) self.down_button.clicked.connect(self.download_button_click) self.load_chapter_list_signa.connect(self.load_chapter) self.load_download_task_signa.connect(self.download_callback) # 调用对应的service的接口,获取章节列表 constant.SERVICE.chapter(comic_info, self.load_chapter_list_signa.emit) i = 0 searchVBoxLayout: QVBoxLayout check_box_list: List[QCheckBox] = [] def check_all_fun(self): for check_box in self.check_box_list: check_box.setChecked(self.check_all.isChecked()) def download_callback(self, task: DownloadTask): widget = DownLoadTaskWidget(task) self.down_v_box_layout.addWidget(widget) def download_button_click(self): flag = False for check_box in self.check_box_list: if check_box.isChecked(): constant.SERVICE.parse_image(self.comic_info, check_box.property("chapter_info"), self.load_download_task_signa.emit) if not flag: QMessageBox.information(self, "下载通知", "正在解析选中章节", QMessageBox.StandardButton.Yes) flag = True def load_chapter(self, chapter_info: ChapterInfo): if self.i % 26 == 0: self.searchVBoxLayout = QVBoxLayout() self.searchVBoxLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) # 对齐方式,研究了3个小时 o(╥﹏╥)o self.searchHBoxLayout.addLayout(self.searchVBoxLayout) check_box = QCheckBox() self.check_box_list.append(check_box) check_box.setText(chapter_info.title) check_box.setProperty("chapter_info", chapter_info) task = constant.downloaded_task_map.get(chapter_info.url) if task and task.status == -1: check_box.setStyleSheet('color:red') check_box.setChecked(True) self.searchVBoxLayout.addWidget(check_box) self.i += 1
def __init__(self, main_window: QWidget): super().__init__() self.search_callback = None # 主题空间 子组件都放这个Widget里 self.centralWidget = QtWidgets.QWidget(main_window) self.centralWidget.setObjectName("centralWidget") # 搜索框 self.souInput = QtWidgets.QLineEdit(self.centralWidget) self.souInput.setGeometry(QtCore.QRect(40, 30, 800, 30)) font = QtGui.QFont() font.setPointSize(22) font.setKerning(True) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) self.souInput.setFont(font) self.souInput.setObjectName("souInput") self.souInput.setText("龙珠") self.modBox = CheckableComboBox(self.centralWidget) self.modBox.setGeometry(QtCore.QRect(850, 30, 120, 30)) for k in constant.mod_dist.keys(): if k in constant.mod_list: self.modBox.addItem(QtCore.Qt.CheckState.Checked, k) else: self.modBox.addItem(QtCore.Qt.CheckState.Unchecked, k) # QTabWidget tab页签 self.tabWidget = QtWidgets.QTabWidget(self.centralWidget) self.tabWidget.setGeometry(QtCore.QRect(40, 70, 944, 668)) self.tabWidget.setTabsClosable(True) self.tabWidget.setObjectName("tabWidget") # 下载页面 self.down_tab = QtWidgets.QWidget() self.down_tab.setStatusTip("") self.down_tab.setAutoFillBackground(False) self.down_tab.setObjectName("down_tab") self.tabWidget.addTab(self.down_tab, "下载列表") # 书架页面 self.bookshelf_tab = QtWidgets.QWidget() self.bookshelf_tab.setObjectName("bookshelf_tab") self.tabWidget.addTab(self.bookshelf_tab, "书架") # 搜索结果页面 self.search_tab = QtWidgets.QWidget() self.search_tab.setObjectName("search_tab") self.tabWidget.addTab(self.search_tab, "搜索结果") # None 空按钮,tab签右侧按钮,设置到前面 tbr = self.tabWidget.tabBar().tabButton(0, QTabBar.ButtonPosition.RightSide) self.tabWidget.tabBar().setTabButton(0, QTabBar.ButtonPosition.LeftSide, tbr) self.tabWidget.tabBar().setTabButton(1, QTabBar.ButtonPosition.LeftSide, tbr) self.tabWidget.tabBar().setTabButton(2, QTabBar.ButtonPosition.LeftSide, tbr) # 启用关闭页签的功能 self.tabWidget.tabCloseRequested.connect(self.tab_close) # 默认打开到书架 self.tabWidget.setCurrentIndex(1) # 主体的centralWidget 放到主窗口中 main_window.setCentralWidget(self.centralWidget) # 书架页 self.bookshelfVBoxLayout = QVBoxLayout() self.bookshelfGroupBox = QGroupBox() self.bookshelfScroll = QScrollArea() self.bookshelfLayout = QVBoxLayout(self.bookshelf_tab) self.bookshelfVBoxLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) self.bookshelfGroupBox.setLayout(self.bookshelfVBoxLayout) self.bookshelfScroll.setWidget(self.bookshelfGroupBox) self.bookshelfScroll.setWidgetResizable(True) self.bookshelfLayout.addWidget(self.bookshelfScroll) # 搜索页 self.searchVBoxLayout = QVBoxLayout() self.searchGroupBox = QGroupBox() self.searchScroll = QScrollArea() self.searchLayout = QVBoxLayout(self.search_tab) self.searchVBoxLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) self.searchGroupBox.setLayout(self.searchVBoxLayout) self.searchScroll.setWidget(self.searchGroupBox) self.searchScroll.setWidgetResizable(True) self.searchLayout.addWidget(self.searchScroll) # 下载页 self.downVBoxLayout = QVBoxLayout() self.downGroupBox = QGroupBox() self.downScroll = QScrollArea() self.downLayout = QVBoxLayout(self.down_tab) self.downVBoxLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) self.downGroupBox.setLayout(self.downVBoxLayout) self.downScroll.setWidget(self.downGroupBox) self.downScroll.setWidgetResizable(True) self.downLayout.addWidget(self.downScroll) down_button_layout = QHBoxLayout() self.downLayout.addLayout(down_button_layout) down_button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) all_start = QPushButton() all_start.setText("全部开始") all_stop = QPushButton() all_stop.setText("全部停止") clear_done = QPushButton() clear_done.setText("清理已完成") down_button_layout.addWidget(all_start) down_button_layout.addWidget(all_stop) down_button_layout.addWidget(clear_done) self.souInput.returnPressed.connect(self.input_return_pressed) # 回车搜索 self.load_comic_list_signa.connect(self.search_load_comic_list) # 更新ui的插槽 self.bookshelf_load_comic_list() self.download_callback()
class CalibrateWizard(QWizard): def __init__(self, parent): super().__init__(parent) self.parent = parent self.setWizardStyle(QWizard.WizardStyle.ModernStyle) # CREATE PAGE 1, LINE EDIT, TITLES buttons_layout = [QWizard.WizardButton.NextButton] self.page1 = QWizardPage() self.page1.setTitle('Select the exercises you wish to do later') self.page1.setSubTitle( 'Below are listed all the available and selected exercises by you.' ) self.listSelection = TwoListSelection() # listSelection.addAvailableItems(["item-{}".format(i) for i in range(5)]) hLayout1 = QHBoxLayout(self.page1) hLayout1.addWidget(self.listSelection) # CREATE PAGE 2, LABEL, TITLES self.page2 = QWizardPage() self.page2.setFinalPage(True) self.setButtonLayout(buttons_layout) self.page2.setTitle('Calibrate every exercise') self.page2.setSubTitle( 'Do every exercise once, record after pressing button.') self.contentLayout = QVBoxLayout(self.page2) self.hLayout2 = QHBoxLayout() # Create progress bar, buttons self.actionsLayout = QHBoxLayout() self.finishButton = QPushButton('Ready') self.finishButton.setStyleSheet(CustomQStyles.buttonStyle) self.finishButton.setFixedSize(120, 35) self.progress = QProgressBar() self.progress.setRange(0, 1) self.actionsLayout.addWidget(self.progress) self.actionsLayout.setAlignment(self.progress, Qt.Alignment.AlignBottom) self.actionsLayout.addWidget(self.finishButton) self.actionsLayout.setAlignment(self.finishButton, Qt.Alignment.AlignBottom) self.contentLayout.addLayout(self.hLayout2) self.contentLayout.addLayout(self.actionsLayout) self.actionsLayout.setContentsMargins(15, 35, 15, 0) itemsTextList = [ str(self.listSelection.mInput.item(i).text()) for i in range(self.listSelection.mInput.count()) ] print("items:", itemsTextList) self.button(QWizard.WizardButton.NextButton).clicked.connect( self.onWizardNextButton) self.finishButton.clicked.connect(self.onWizardFinishButton) self.addPage(self.page1) self.addPage(self.page2) # Recording data self.buttons = [] self.images = [] self.labels = [] self.exerciseLayouts = [] self.recordReady = [] self.recordThread = RecordThread(self.parent.classifyExercises) # Training recorded data self.trained = False self.trainThread = TrainThread(self.parent.classifyExercises) self.trainThread.taskFinished.connect(self.onTrainFinished) # Send list to next page def onWizardNextButton(self): self.setPage(1, self.page1) self.trained = False itemsTextList = [ str(self.listSelection.mInput.item(i).text()) for i in range(self.listSelection.mInput.count()) ] # Update list if self.parent.classifyExercises is not None: self.parent.classifyExercises.UpdateExerciseList(itemsTextList) # Set elements on UI self.setMinimumWidth(len(itemsTextList) * 200) self.deleteItemsOfLayout(self.hLayout2) self.images.clear() self.labels.clear() self.buttons.clear() self.recordReady.clear() for x, i in zip(itemsTextList, range(len(itemsTextList))): self.exerciseLayouts.append(QVBoxLayout()) self.buttons.append(QPushButton('Record')) self.recordReady.append(False) image = QLabel() image.setPixmap( QPixmap(os.getcwd() + "/resources/images/" + itemsTextList[i] + ".png")) self.labels.append(QLabel(itemsTextList[i])) self.images.append(image) self.buttons[i].setFixedSize(100, 35) self.buttons[i].clicked.connect( functools.partial(self.onRecordExerciseButtonClicked, x, i)) self.buttons[i].setStyleSheet(CustomQStyles.outlineButtonStyle) self.exerciseLayouts[i].addWidget(self.labels[i]) self.exerciseLayouts[i].addWidget(self.images[i]) self.exerciseLayouts[i].addWidget(self.buttons[i]) self.exerciseLayouts[i].setAlignment(self.labels[i], Qt.Alignment.AlignCenter) self.exerciseLayouts[i].setAlignment(self.images[i], Qt.Alignment.AlignCenter) self.exerciseLayouts[i].setAlignment(self.buttons[i], Qt.Alignment.AlignCenter) self.hLayout2.addLayout(self.exerciseLayouts[i]) def onRecordExerciseButtonClicked(self, exercise, ind): print("Recording - ", exercise) if self.parent.classifyExercises is not None: self.recordThread.exercise = exercise self.recordThread.taskFinished.connect( functools.partial(self.recordFinished, exercise, ind), Qt.ConnectionType.SingleShotConnection) self.recordThread.start() self.recordReady[ind] = False self.buttons[ind].setStyleSheet(CustomQStyles.recordButtonStyle) self.images[ind].setPixmap( QPixmap(os.getcwd() + "/resources/images/" + exercise + ".png")) def recordFinished(self, exercise, index): imagePath = os.getcwd() + "/resources/images/" + exercise + ".png" if self.recordThread.result == 0: imagePath = os.getcwd( ) + "/resources/images/" + exercise + "-fail.png" elif self.recordThread.result == 1: imagePath = os.getcwd( ) + "/resources/images/" + exercise + "-success.png" self.recordReady[index] = True else: print("None.") self.images[index].setPixmap(QPixmap(imagePath)) self.buttons[index].setStyleSheet(CustomQStyles.outlineButtonStyle) print(self.recordReady) def onWizardFinishButton(self): if all(x == True for x in self.recordReady): print("All recorded!") if not self.trained: if self.parent.classifyExercises is not None: self.progress.setRange(0, 0) # indefinite progress bar self.parent.classifyExercises.SaveProcessedData() self.parent.classifyExercises.SavePatientData() self.parent.ui.loadPatientList() self.trainThread.start() else: self.close() else: print("Not all recorded!") def onTrainFinished(self): self.progress.setRange(0, 1) self.progress.setValue(1) self.trained = True CustomMessage.showDialog("Message", "Training model finished!", QMessageBox.StandardButtons.Ok) self.finishButton.setText('Finish') def deleteItemsOfLayout(self, layout): if layout is not None: while layout.count(): item = layout.takeAt(0) widget = item.widget() if widget is not None: widget.setParent(None) else: self.deleteItemsOfLayout(item.layout())
class Madoka(QWidget): global serialPort, baud, fmt def __init__(self): global serialPort, baud, fmt self.serialLogState = False self.fmts = ['bin', 'oct', 'dec', 'hex', 'csv', 'csv+', 'ascii'] self.baudList = [ 4800, 7200, 9600, 14400, 19200, 28800, 38400, 57600, 76800, 115200, 230400, 460800, 921600, 1000000, 2000000, 4000000 ] fmt = self.fmts.index(fmt) super().__init__() layoutA = QHBoxLayout() self.baudBox = QComboBox() self.getBaud() layoutA.addWidget(self.baudBox) self.formatBox = QComboBox() self.getFormat() layoutA.addWidget(self.formatBox) layoutB = QHBoxLayout() self.portBox = QComboBox() self.getPorts() layoutB.addWidget(self.portBox) parentLayout = QVBoxLayout() parentLayout.addLayout(layoutB) parentLayout.addLayout(layoutA) self.setLayout(parentLayout) self.portBox.currentTextChanged.connect(self.portSelected) self.portBox.setCurrentIndex(len(self.ports) - 1) self.baudBox.currentTextChanged.connect(self.baudSelected) try: self.baudBox.setCurrentIndex(self.baudList.index(baud)) except: self.baudBox.setCurrentIndex(-1) self.formatBox.currentTextChanged.connect(self.formatSelected) self.formatBox.setCurrentIndex(fmt) self.button1 = QPushButton() self.button1.setText("start") self.button1.released.connect(self.btn1Clicked) layoutA.addWidget(self.button1) def portSelected(self, text): global serialPort if (text != ''): serialPort = self.ports[self.portsIndex.index(text)] # print(serialPort) # self.erandano.setText(self.port) def baudSelected(self, text): global baud if (text != ''): baud = text # print(baud) def formatSelected(self, text): global fmt if (text != ''): fmt = self.fmts.index(text) # print(text) def getPorts(self): global serialPort # sys.stderr.write("\n--- Available ports:\n") self.ports = [] self.portsIndex = [] for n, (port, desc, devid) in enumerate(sorted(comports()), 1): # sys.stderr.write("--- {:2}: {:20} {!r} \n".format(n, port, desc)) self.ports.append(port) print(port) self.portsIndex.append("{:2}: {:20} {!r}".format(n, port, desc)) try: serialPort = self.ports.index(serialPort) except: serialPort = self.ports[len(self.ports) - 1] for s in self.portsIndex: self.portBox.addItem(s) def getBaud(self): for b in self.baudList: self.baudBox.addItem(str(b)) def getFormat(self): global fmt for f in self.fmts: self.formatBox.addItem(f) def btn1Clicked(self): if (self.button1.text() == 'start'): self.baudBox.setDisabled(True) self.formatBox.setDisabled(True) self.button1.setText('stop') self.serialLogState = True th = threading.Thread(target=printData, daemon=True) th.start() elif (self.button1.text() == 'stop'): self.button1.setText('start') self.baudBox.setDisabled(False) self.formatBox.setDisabled(False) self.serialLogState = False
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("📸 Random Image Tools V1.2 🔨") self.image_list = [] # ToDo: Add a "rename?" flag. Like rename image files # to store the directory path for later use self.directory_path_name = "/" # Set up the layouts layer_one = QHBoxLayout() # Select a folder, selected directory layer_one_and_a_half = QHBoxLayout() # Selected directory contents layer_two = QHBoxLayout() # Second line of buttons layer_two_vertical_one = QVBoxLayout() # Store the first column w/checkbox and "Convert" layer_two_vertical_two = QVBoxLayout() # Store the second column w/checkbox and "Open in File Browser" layer_three = QHBoxLayout() # Conversion process state vertical_layout_parent = QVBoxLayout() # Parent widget widget = QWidget() # Displays selected directory self.directory_label = QLabel() self.directory_label.setText("Directory to be worked on will show here ") self.directory_label.show() # Displays "Select folder" button self.select_a_folder_button = QPushButton() self.select_a_folder_button.setText("Select a folder:") self.select_a_folder_button.clicked.connect(self.select_folder_prompt) self.select_a_folder_button.show() # Displays the image contents of the selected folder self.image_paths_list_widget = QListWidget() self.image_paths_list_widget.show() # Displays button to initiate image conversion self.convert_to_png_button = QPushButton() self.convert_to_png_button.setText("Convert to PNG") self.convert_to_png_button.clicked.connect(self.convert_folder_to_png) self.convert_to_png_button.show() # Check boxes for "Create new folder for PNGs" and "Delete original files after converting" self.create_new_folder_checkbox = QCheckBox() self.create_new_folder_checkbox.setText("Create new folder to store converted PNG's?") self.create_new_folder_checkbox.show() self.delete_original_files_checkbox = QCheckBox() self.delete_original_files_checkbox.setText("Delete original files after converting them to PNG?") self.create_new_folder_checkbox.show() # Displays button to open selected directory in the file browser self.show_folder_button = QPushButton() self.show_folder_button.setText("Open selected folder in file browser") self.show_folder_button.clicked.connect(self.open_folder) self.show_folder_button.show() # Displays label when conversion is finished, and the corresponding progress bar self.conversion_finished_or_error_label = QLabel() self.conversion_finished_or_error_label.setText("👀 waiting for you to press \"Convert to PNG\" ") # Put the find folder button and folder selected button together layer_one.addWidget(self.select_a_folder_button) layer_one.addWidget(self.directory_label) # Image paths of selected folder layer_one_and_a_half.addWidget(self.image_paths_list_widget) # Put the convert button and open-in-finder button together layer_two_vertical_one.addWidget(self.convert_to_png_button) layer_two_vertical_one.addWidget(self.delete_original_files_checkbox) layer_two.addLayout(layer_two_vertical_one) layer_two_vertical_two.addWidget(self.show_folder_button) layer_two_vertical_two.addWidget(self.create_new_folder_checkbox) layer_two.addLayout(layer_two_vertical_two) # Label and progress bar layer_three.addWidget(self.conversion_finished_or_error_label) layer_three.setAlignment(Qt.AlignmentFlag.AlignHCenter) # Put the "convert to png" button beneath vertical_layout_parent.addLayout(layer_one) vertical_layout_parent.addLayout(layer_one_and_a_half) vertical_layout_parent.addLayout(layer_two) vertical_layout_parent.addLayout(layer_three) widget.setLayout(vertical_layout_parent) self.setCentralWidget(widget) # Prompts user to select a folder, stores the given folder path and displays chosen path to user def select_folder_prompt(self): # Clear self.image_list and QListWidget to prepare for newly selected folder. self.image_list.clear() self.image_paths_list_widget.clear() # Append a "/" otherwise it will mix the folder name and containing image file together directory = str(QFileDialog.getExistingDirectory(self, "Select Directory")) + "/" # Update QLabel to new directory, and store it in self for future use self.directory_label.setText(directory) self.directory_path_name = directory # Update self.image_list field image_list = self.scan_for_jpg_file_paths() self.image_list = image_list # Populated the QListWindow() with the update self.image_list field self.image_paths_list_widget.addItems(self.image_list) # Given a path name, will open it in the Folder browser app def open_folder(self): subprocess.call(["open", "-R", self.directory_path_name]) # Given the current state of the directory_path_name folder, will scan for image files in that folder def scan_for_jpg_file_paths(self): image_list = [] for root, dirs, files in os.walk(self.directory_path_name, topdown=True): for filename in files: if '.jpeg' or '.jpg' or '.webp' or '.gif' or '.icns' in filename: if '.png' not in filename: absolute_path = self.directory_path_name + filename # Avoid adding duplicates if absolute_path not in image_list: image_list.append(absolute_path) return image_list # Given a non-empty folder path, converts all jpg images in it to png. # Todo: Store png images in a new folder? # ToDo: Add a "Delete images after converting?" # ToDo: Add functionality for checkboxes # Ok so that the QListWidget can update, I'm going to have to get the image_list before calling this function. # In other words, image_list is a field in the MainWindow subclass, and is update after selecting the folder, and # called before calling this function. def convert_folder_to_png(self): self.conversion_finished_or_error_label.setText("Converting") # Progress bar depends on independent variable, length of image list = x if len(self.image_list) > 0: self.convert_images_to_png() self.conversion_finished_or_error_label.setText("Conversion finished") if len(self.image_list) <= 0: self.conversion_finished_or_error_label.setText("There are no image files in this folder") # This will be called after a folder has been selected and the "Convert to PNG" button has been pressed def convert_images_to_png(self): self.conversion_finished_or_error_label.setText("...") # Convert images # ToDo : Put the checkbox logic here # Check if user wanted images stored in a new folder # Check if user wanted to delete original images after conversion # Placeholder for converted_PNG folder user_wanted_png_in_new_folder = self.create_new_folder_checkbox.isChecked() user_wanted_unconverted_image_deleted = self.delete_original_files_checkbox.isChecked() converted_png_folder_name = "" # Create folder if user wanted converted images stored in a new folder if user_wanted_png_in_new_folder: converted_png_folder_name = self.directory_path_name + "Converted PNG Files/" os.mkdir(converted_png_folder_name) for image_path in self.image_list: # Get absolute path absolute_image_path = os.path.abspath(image_path) if ".DS_Store" not in absolute_image_path: # Mac adds .DS_Store, just a way to ignore these pesky files unconverted_image = Image.open(absolute_image_path) # Image object if user_wanted_png_in_new_folder: # Get just the image name and extension name_and_extension_of_image = extract_image_name_and_extension(absolute_image_path) # Append image name and extension to the converted image folder path image_path_stored_in_converted_png_folder = converted_png_folder_name + name_and_extension_of_image # Store in converted image folder path unconverted_image.save(rename_image_path_to_png(image_path_stored_in_converted_png_folder)) else: unconverted_image.save(rename_image_path_to_png(absolute_image_path)) if user_wanted_unconverted_image_deleted: os.remove(absolute_image_path)
class VideoWindow(QMainWindow): def __init__(self, parent=None): super(VideoWindow, self).__init__(parent) self.setWindowTitle("StudioProject") self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.gScene = QGraphicsScene(self) self.gView = GraphicView(self.gScene, self) self.gView.viewport().setAttribute( Qt.WidgetAttribute.WA_AcceptTouchEvents, False) # self.gView.setBackgroundBrush(QBrush(Qt.black)) self.videoStartDatetime = None self.videoCurrentDatetime = None self.projectFile = '' self.graphicsFile = '' self.videoFile = '' self.obsTb = ObsToolbox(self) # # ===================== Setting video item ============================== # self.videoItem = QGraphicsVideoItem() # self.videoItem.setAspectRatioMode(Qt.KeepAspectRatio) # self.gScene.addItem(self.videoItem) # self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent self.mediaPlayer = QMediaPlayer(self) # self.mediaPlayer.setVideoOutput(self.videoItem) self.mediaPlayer.playbackStateChanged.connect(self.mediaStateChanged) self.mediaPlayer.positionChanged.connect(self.positionChanged) self.mediaPlayer.durationChanged.connect(self.durationChanged) self.mediaPlayer.errorOccurred.connect(self.handleError) # self.mediaPlayer.setMuted(True) # self.mediaPlayer.setNotifyInterval(100) self.playButton = QPushButton() self.playButton.setEnabled(False) self.playButton.setIcon(self.style().standardIcon( QStyle.StandardPixmap.SP_MediaPlay)) self.playButton.clicked.connect(self.play) self.changePlayRateBtn = QPushButton('1x') self.changePlayRateBtn.setFixedWidth(40) # self.incrPlayRateBtn.setEnabled(False) self.changePlayRateBtn.clicked.connect(self.changePlayRate) self.positionSlider = QSlider(Qt.Orientation.Horizontal) self.positionSlider.setRange(0, 0) self.positionSlider.sliderMoved.connect(self.setPosition) self.timerLabel = QLabel() self.timerLabel.setText('--:--:--') self.timerLabel.setFixedWidth(58) self.dateLabel = QLabel() self.dateLabel.setText('Video date: --') self.statusBar.addPermanentWidget(self.dateLabel) # Create open action self.openVideoAction = QAction(QIcon('icons/video-file.png'), 'Open video', self) self.openVideoAction.setShortcut('Ctrl+O') self.openVideoAction.setStatusTip('Open video file') self.openVideoAction.triggered.connect(self.openVideoFile) # Create observation action obsTbAction = QAction(QIcon('icons/checklist.png'), 'Observation toolbox', self) obsTbAction.setStatusTip('Open observation toolbox') obsTbAction.triggered.connect(self.openObsToolbox) self.drawPointAction = QAction(QIcon('icons/drawPoint.png'), 'Draw point', self) self.drawPointAction.setStatusTip('Draw point over the video') self.drawPointAction.setCheckable(True) self.drawPointAction.setEnabled(False) self.drawPointAction.triggered.connect(self.drawingClick) self.drawLineAction = QAction(QIcon('icons/drawLine.png'), 'Draw line', self) self.drawLineAction.setStatusTip('Draw line over the video') self.drawLineAction.setCheckable(True) self.drawLineAction.setEnabled(False) self.drawLineAction.triggered.connect(self.drawingClick) self.drawZoneAction = QAction(QIcon('icons/drawZone.png'), 'Draw zone', self) self.drawZoneAction.setStatusTip('Draw zone over the video') self.drawZoneAction.setCheckable(True) self.drawZoneAction.setEnabled(False) self.drawZoneAction.triggered.connect(self.drawingClick) self.maskGenAction = QAction(QIcon('icons/mask.png'), 'Generate mask file', self) self.maskGenAction.setStatusTip( 'Generate mask file for TrafficIntelligence') self.maskGenAction.setCheckable(True) self.maskGenAction.setEnabled(False) self.maskGenAction.triggered.connect(self.generateMask) actionGroup = QActionGroup(self) actionGroup.addAction(self.drawPointAction) actionGroup.addAction(self.drawLineAction) actionGroup.addAction(self.drawZoneAction) openProjectAction = QAction(QIcon('icons/open-project.png'), 'Open project', self) openProjectAction.setStatusTip('Open project') openProjectAction.triggered.connect(self.openProject) self.saveProjectAction = QAction(QIcon('icons/save-project.png'), 'Save project', self) self.saveProjectAction.setStatusTip('Save project') self.saveProjectAction.setEnabled(False) self.saveProjectAction.triggered.connect(self.saveProject) self.saveGraphAction = QAction(QIcon('icons/save-graphics.png'), 'Save graphics', self) self.saveGraphAction.setStatusTip('Save graphics to database') self.saveGraphAction.setEnabled(False) self.saveGraphAction.triggered.connect(self.saveGraphics) self.loadGraphAction = QAction(QIcon('icons/folders.png'), 'Load graphics', self) self.loadGraphAction.setStatusTip('Load graphics from database') self.loadGraphAction.setEnabled(False) self.loadGraphAction.triggered.connect(self.loadGraphics) # Create exit action exitAction = QAction(QIcon('icons/close.png'), 'Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(self.exitCall) # self.exitCall # Create menu bar and add action # menuBar = self.menuBar() # menuBar.setNativeMenuBar(False) # fileMenu = menuBar.addMenu('&File') # fileMenu.addAction(openVideoAction) # fileMenu.addAction(obsTbAction) # fileMenu.addAction(exitAction) self.toolbar = self.addToolBar('Tools') self.toolbar.setIconSize(QSize(24, 24)) self.toolbar.addAction(openProjectAction) self.toolbar.addAction(self.saveProjectAction) self.toolbar.addAction(self.openVideoAction) # self.toolbar.insertSeparator(self.loadGraphAction) # self.toolbar.addAction(self.loadGraphAction) # self.toolbar.addAction(self.saveGraphAction) # self.toolbar.addAction(self.drawPointAction) # self.toolbar.addAction(self.drawLineAction) # self.toolbar.addAction(self.drawZoneAction) self.toolbar.addAction(self.maskGenAction) # self.toolbar.insertSeparator(self.drawPointAction) self.toolbar.insertSeparator(obsTbAction) self.toolbar.addAction(obsTbAction) self.toolbar.insertSeparator(exitAction) self.toolbar.addAction(exitAction) # Create a widget for window contents wid = QWidget(self) self.setCentralWidget(wid) # Create layouts to place inside widget controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) # controlLayout.addWidget(self.decrPlayRateBtn) controlLayout.addWidget(self.playButton) controlLayout.addWidget(self.changePlayRateBtn) controlLayout.addWidget(self.timerLabel) controlLayout.addWidget(self.positionSlider) # controlLayout.addWidget(self.durationLabel) layout = QVBoxLayout() layout.addWidget(self.gView) layout.addLayout(controlLayout) # Set widget to contain window contents wid.setLayout(layout) # def showEvent(self, event): # self.gView.fitInView(self.videoItem, Qt.KeepAspectRatio) def openVideoFile(self): # self.mediaPlayer.setMedia(QMediaContent()) if self.sender() == self.openVideoAction: self.videoFile, _ = QFileDialog.getOpenFileName( self, "Open video", QDir.homePath()) # if self.videoFile != '': # self.setWindowTitle('{} - {}'.format(os.path.basename(self.videoFile), # os.path.basename(self.projectFile))) if self.videoFile != '': self.setWindowTitle('{} - {}'.format( os.path.basename(self.videoFile), os.path.basename(self.projectFile))) self.saveProjectAction.setEnabled(True) self.maskGenAction.setEnabled(True) # self.loadGraphAction.setEnabled(True) # self.saveGraphAction.setEnabled(True) # self.drawPointAction.setEnabled(True) # self.drawLineAction.setEnabled(True) # self.drawZoneAction.setEnabled(True) creation_datetime, width, height = getVideoMetadata(self.videoFile) self.videoStartDatetime = self.videoCurrentDatetime = creation_datetime self.dateLabel.setText(creation_datetime.strftime('%a, %b %d, %Y')) self.gView.setSceneRect(0, 0, width, height) self.videoItem = QGraphicsVideoItem() self.videoItem.setAspectRatioMode( Qt.AspectRatioMode.KeepAspectRatio) self.gScene.addItem(self.videoItem) self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent self.videoItem.setSize(QSizeF(width, height)) self.mediaPlayer.setVideoOutput(self.videoItem) self.mediaPlayer.setSource(QUrl.fromLocalFile(self.videoFile)) self.gView.labelSize = width / 50 self.playButton.setEnabled(True) # self.gView.setViewport(QOpenGLWidget()) self.mediaPlayer.pause() def exitCall(self): # sys.exit(app.exec()) # self.mediaPlayer.pause() self.close() def play(self): # self.gView.fitInView(self.videoItem, Qt.KeepAspectRatio) if self.mediaPlayer.playbackState( ) == QMediaPlayer.PlaybackState.PlayingState: self.mediaPlayer.pause() else: self.mediaPlayer.play() def changePlayRate(self): if self.mediaPlayer.playbackRate() < 2: r = self.mediaPlayer.playbackRate() + 0.5 self.mediaPlayer.setPlaybackRate(r) self.changePlayRateBtn.setText('{:g}x'.format(r)) self.statusBar.showMessage('Play back rate = {:g}x'.format(r), 2000) elif self.mediaPlayer.playbackRate() == 2: self.mediaPlayer.setPlaybackRate(1) self.changePlayRateBtn.setText('{}x'.format(1)) self.statusBar.showMessage('Play back rate = {}x'.format(1), 2000) def mediaStateChanged(self, state): if self.mediaPlayer.playbackState( ) == QMediaPlayer.PlaybackState.PlayingState: self.playButton.setIcon(self.style().standardIcon( QStyle.StandardPixmap.SP_MediaPause)) else: self.playButton.setIcon(self.style().standardIcon( QStyle.StandardPixmap.SP_MediaPlay)) def positionChanged(self, position): self.positionSlider.setValue(position) s, m, h = self.convertMillis(position) self.videoCurrentDatetime = self.videoStartDatetime + \ timedelta(hours=h, minutes=m, seconds=s) self.timerLabel.setText('{:02d}:{:02d}:{:02d}'.format( self.videoCurrentDatetime.time().hour, self.videoCurrentDatetime.time().minute, self.videoCurrentDatetime.time().second)) def durationChanged(self, duration): self.positionSlider.setRange(0, duration) # s, m, h = self.convertMillis(duration) # self.durationLabel.setText('{:02d}:{:02d}'.format(m, s)) def setPosition(self, position): self.mediaPlayer.setPosition(position) def handleError(self): self.playButton.setEnabled(False) # self.errorLabel.setText("Error: " + self.mediaPlayer.errorString()) def openObsToolbox(self): if not self.obsTb.isVisible(): self.obsTb.show() def drawingClick(self): # if self.sender() == self.drawLineAction: # self.labelingAction.setChecked(False) # else: # self.drawLineAction.setChecked(False) cursor = QCursor(Qt.CursorShape.CrossCursor) self.gView.setCursor(cursor) def generateMask(self): if not self.sender().isChecked(): self.gView.unsetCursor() return cursor = QCursor(Qt.CursorShape.CrossCursor) self.gView.setCursor(cursor) # dbfilename = self.obsTb.dbFilename # if dbfilename != None: # self.session = connectDatabase(dbfilename) # else: # msg = QMessageBox() # msg.setIcon(QMessageBox.Information) # msg.setText('The database file is not defined.') # msg.setInformativeText('In order to set the database file, open the Observation Toolbox') # msg.setIcon(QMessageBox.Critical) # msg.exec_() # return # if self.gView.unsavedLines == [] and self.gView.unsavedZones == [] and \ # self.gView.unsavedPoints == []: # QMessageBox.information(self, 'Save', 'There is no polygon to generate mask!') # return def saveMaskFile(self): creation_datetime, width, height = getVideoMetadata(self.videoFile) item = self.gView.gPolyItem #self.gView.unsavedZones[0] mask_polygon = item.polygon() xy = [] for p in mask_polygon: xy.append((p.x(), p.y())) img = Image.new('RGB', (width, height), color='black') img1 = ImageDraw.Draw(img) img1.polygon(xy, fill="white", outline="white") fileName, _ = QFileDialog.getSaveFileName(self, "Open database file", QDir.homePath(), "PNG files (*.png)") if fileName != '': img.save(fileName) self.gView.scene().removeItem(item) self.gView.unsavedZones = [] def openProject(self): self.projectFile, _ = QFileDialog.getOpenFileName( self, "Open project file", QDir.homePath(), "Project (*.prj)") if self.projectFile == '': return self.saveProjectAction.setEnabled(True) self.maskGenAction.setEnabled(True) # self.loadGraphAction.setEnabled(True) # self.saveGraphAction.setEnabled(True) # self.drawPointAction.setEnabled(True) # self.drawLineAction.setEnabled(True) # self.drawZoneAction.setEnabled(True) tree = ET.parse(self.projectFile) root = tree.getroot() gItems = [] for elem in root: subEelTexts = {} for subelem in elem: subEelTexts[subelem.tag] = subelem.text gItems.append([elem.tag, subEelTexts]) for key in gItems: if key[0] == 'database': item = key[1] if item['fileName'] is not None: self.obsTb.dbFilename = item['fileName'] self.obsTb.opendbFile() elif key[0] == 'video': item = key[1] if item['fileName'] is not None: self.videoFile = item['fileName'] self.openVideoFile() self.mediaPlayer.setPosition(int(item['sliderValue'])) if item['fileName'] is not None: self.loadGraphics() elif key[0] == 'trajectory': item = key[1] if item['metadata'] != None: self.obsTb.mdbFileLedit.setText(item['metadata']) self.obsTb.openMdbFile() self.obsTb.siteNameCombobx.setCurrentIndex( int(item['site'])) self.obsTb.camViewCombobx.setCurrentIndex( int(item['cam_view'])) self.obsTb.trjDbCombobx.setCurrentIndex( int(item['traj_db'])) elif key[0] == 'window': item = key[1] x, y = item['mainWin_pos'].split(',') w, h = item['mainWin_size'].split(',') self.setGeometry(int(x), int(y), int(w), int(h)) if item['obsTbx_open'] == 'True': self.obsTb.show() x, y = item['obsTbx_pos'].split(',') w, h = item['obsTbx_size'].split(',') self.obsTb.setGeometry(int(x), int(y), int(w), int(h)) # self.setWindowTitle('{} - {}'.format(os.path.basename(self.videoFile), # os.path.basename(self.projectFile))) def saveProject(self): if self.projectFile == '': fileDir = QDir.homePath() else: fileDir = self.projectFile self.projectFile, _ = QFileDialog.getSaveFileName( self, "Save project file", fileDir, "Project (*.prj)") # fileName = "/Users/Abbas/project.xml" if self.projectFile == '': return file = QFile(self.projectFile) if (not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text)): return xmlWriter = QXmlStreamWriter(file) xmlWriter.setAutoFormatting(True) xmlWriter.writeStartDocument() xmlWriter.writeStartElement('project') xmlWriter.writeStartElement('database') xmlWriter.writeTextElement("fileName", self.obsTb.dbFilename) xmlWriter.writeEndElement() xmlWriter.writeStartElement('video') xmlWriter.writeTextElement( "fileName", self.videoFile) #mediaPlayer.media().canonicalUrl().path()) xmlWriter.writeTextElement("sliderValue", str(self.mediaPlayer.position())) xmlWriter.writeEndElement() xmlWriter.writeStartElement('trajectory') xmlWriter.writeTextElement("metadata", self.obsTb.mdbFileLedit.text()) xmlWriter.writeTextElement( "site", str(self.obsTb.siteNameCombobx.currentIndex())) xmlWriter.writeTextElement( "cam_view", str(self.obsTb.camViewCombobx.currentIndex())) xmlWriter.writeTextElement("traj_db", str(self.obsTb.trjDbCombobx.currentIndex())) xmlWriter.writeEndElement() xmlWriter.writeStartElement('window') xmlWriter.writeTextElement( "mainWin_size", "{},{}".format(int(self.width()), int(self.height()))) xmlWriter.writeTextElement( "mainWin_pos", "{},{}".format(int(self.x()), int(self.y()))) xmlWriter.writeTextElement("obsTbx_open", str(self.obsTb.isVisible())) xmlWriter.writeTextElement( "obsTbx_size", "{},{}".format(int(self.obsTb.width()), int(self.obsTb.height()))) xmlWriter.writeTextElement( "obsTbx_pos", "{},{}".format(int(self.obsTb.x()), int(self.obsTb.y()))) xmlWriter.writeEndElement() xmlWriter.writeEndElement() self.setWindowTitle('{} - {}'.format( os.path.basename(self.videoFile), os.path.basename(self.projectFile))) if self.obsTb.dbFilename != None: self.obsTb.setWindowTitle('{} - {}'.format( os.path.basename(self.obsTb.dbFilename), os.path.basename(self.projectFile))) def saveGraphics(self): dbfilename = self.obsTb.dbFilename if dbfilename != None: self.session = connectDatabase(dbfilename) else: msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText('The database file is not defined.') msg.setInformativeText( 'In order to set the database file, open the Observation Toolbox' ) msg.setIcon(QMessageBox.Critical) msg.exec_() return if self.gView.unsavedLines == [] and self.gView.unsavedZones == [] and \ self.gView.unsavedPoints == []: QMessageBox.information(self, 'Save', 'No new graphical items to save!') return for item in self.gView.unsavedLines: x1 = round(item.line().x1(), 2) y1 = round(item.line().y1(), 2) x2 = round(item.line().x2(), 2) y2 = round(item.line().y2(), 2) line = Line(None, None, x1, y1, x2, y2) self.session.add(line) self.session.flush() label = self.generate_itemGroup([x1, x2], [y1, y2], line.idx) self.gView.scene().addItem(label) for item in self.gView.unsavedZones: xs = [] ys = [] for p in item.polygon(): xs.append(round(p.x(), 2)) ys.append(round(p.y(), 2)) zone = Zone(None, None, xs, ys) self.session.add(zone) self.session.flush() label = self.generate_itemGroup(xs, ys, zone.idx) self.gView.scene().addItem(label) for item in self.gView.unsavedPoints: x = round(item.rect().center().x(), 2) y = round(item.rect().center().y(), 2) point = Point(x, y) self.session.add(point) self.session.flush() label = self.generate_itemGroup([x], [y], point.idx) self.gView.scene().removeItem(item) self.gView.scene().addItem(label) QMessageBox.information( self, 'Save', '{} point(s), {} line(s) and {} zone(s) saved to database successfully!' .format(len(self.gView.unsavedPoints), len(self.gView.unsavedLines), len(self.gView.unsavedZones))) self.gView.unsavedLines = [] self.gView.unsavedZones = [] self.gView.unsavedPoints = [] self.session.commit() def generate_itemGroup(self, xs, ys, label, type): gItemGroup = QGraphicsItemGroup() pointBbx = QRectF() pointBbx.setSize(QSizeF(self.gView.labelSize, self.gView.labelSize)) textLabel = QGraphicsTextItem(label) if len(xs) == 1: pointBbx.moveCenter(QPointF(xs[0], ys[0])) textLabel.setPos(xs[0] - (textLabel.boundingRect().width() / 2), ys[0] - (textLabel.boundingRect().height() / 2)) pointShape = QGraphicsEllipseItem(pointBbx) shapeColor = Qt.GlobalColor.white textColor = Qt.GlobalColor.black tooltip = 'P{}:{}' elif len(xs) == 2: pointBbx.moveCenter(QPointF(xs[1], ys[1])) textLabel.setPos(xs[1] - (textLabel.boundingRect().width() / 2), ys[1] - (textLabel.boundingRect().height() / 2)) r, g, b = np.random.choice(range(256), size=3) line_item = QGraphicsLineItem(xs[0], ys[0], xs[1], ys[1]) line_item.setPen( QPen(QColor(r, g, b, 128), self.gView.labelSize / 6)) gItemGroup.addToGroup(line_item) # line_end = QGraphicsEllipseItem(xs[1], ys[1], # int(self.gView.labelSize/3), int(self.gView.labelSize/3)) # line_end.setPen(QPen(QColor(r, g, b), 0.5)) # line_end.setBrush(QBrush(QColor(r, g, b))) # gItemGroup.addToGroup(line_end) pointShape = QGraphicsEllipseItem(pointBbx) shapeColor = QColor(r, g, b, 128) textColor = Qt.GlobalColor.black tooltip = 'L{}:{}' # textLabel.setRotation(np.arctan((ys[1] - ys[0])/(xs[1] - xs[0]))*(180/3.14)) else: pointBbx.moveCenter(QPointF(np.mean(xs), np.mean(ys))) textLabel.setPos( np.mean(xs) - (textLabel.boundingRect().width() / 2), np.mean(ys) - (textLabel.boundingRect().height() / 2)) points = [QPointF(x, y) for x, y in zip(xs, ys)] polygon = QPolygonF(points) r, g, b = np.random.choice(range(256), size=3) zone_item = QGraphicsPolygonItem(polygon) zone_item.setPen(QPen(QColor(r, g, b), self.gView.labelSize / 10)) zone_item.setBrush(QBrush(QColor(r, g, b, 40))) gItemGroup.addToGroup(zone_item) pointShape = QGraphicsRectItem(pointBbx) shapeColor = Qt.GlobalColor.darkBlue textColor = Qt.GlobalColor.white tooltip = 'Z{}:{}' pointShape.setPen(QPen(Qt.GlobalColor.white, 0.5)) pointShape.setBrush(QBrush(shapeColor)) # self.gView.scene().addEllipse(pointBbx, QPen(Qt.white, 0.5), QBrush(Qt.black)) gItemGroup.setToolTip(tooltip.format(label, type)) gItemGroup.addToGroup(pointShape) labelFont = QFont() labelFont.setPointSize(round(self.gView.labelSize / 2)) labelFont.setBold(True) textLabel.setFont(labelFont) textLabel.setDefaultTextColor(textColor) gItemGroup.addToGroup(textLabel) return gItemGroup def loadGraphics(self): dbfilename = self.obsTb.dbFilename if dbfilename != None: self.session = connectDatabase(dbfilename) else: msg = QMessageBox() # msg.setIcon(QMessageBox.Icon.Information) msg.setText('The database file is not defined.') msg.setInformativeText( 'In order to set the database file, open the Observation Toolbox' ) msg.setIcon(QMessageBox.Icon.Critical) msg.exec() return for gitem in self.gView.scene().items(): if isinstance(gitem, QGraphicsItemGroup): self.gView.scene().removeItem(gitem) q_line = self.session.query(Line) q_zone = self.session.query(Zone) if q_line.all() == [] and q_zone.all() == []: QMessageBox.information(self, 'Warning!', 'There is no graphics to load!') return line_items = [] for line in q_line: p1 = line.points[0] p2 = line.points[1] if line.type != None: lineType = line.type.name else: lineType = None gItmGroup = self.generate_itemGroup([p1.x, p2.x], [p1.y, p2.y], str(line.idx), lineType) self.gScene.addItem(gItmGroup) line_items.append(str(line.idx)) self.obsTb.line_list_wdgt.clear() self.obsTb.line_list_wdgt.addItems(line_items) self.obsTb.line_list_wdgt.setCurrentRow(0) self.obsTb.line_newRecButton.setEnabled(False) self.obsTb.line_saveButton.setEnabled(True) self.obsTb.line_saveButton.setText('Edit line(s)') self.obsTb.line_saveButton.setIcon(QIcon('icons/edit.png')) zone_items = [] for zone in q_zone: if zone.type != None: zoneType = zone.type.name else: zoneType = None gItmGroup = self.generate_itemGroup( [point.x for point in zone.points], [point.y for point in zone.points], str(zone.idx), zoneType) self.gScene.addItem(gItmGroup) zone_items.append(str(zone.idx)) self.obsTb.zone_list_wdgt.clear() self.obsTb.zone_list_wdgt.addItems(zone_items) self.obsTb.zone_list_wdgt.setCurrentRow(0) self.obsTb.zone_newRecButton.setEnabled(False) self.obsTb.zone_saveButton.setEnabled(True) self.obsTb.zone_saveButton.setText('Edit zone(s)') self.obsTb.zone_saveButton.setIcon(QIcon('icons/edit.png')) @staticmethod def convertMillis(millis): seconds = int(millis / 1000) % 60 minutes = int(millis / (1000 * 60)) % 60 hours = int(millis / (1000 * 60 * 60)) % 24 return seconds, minutes, hours
class LoginDialog(QDialog): """登录对话框""" clicked_ok = pyqtSignal() def __init__(self, config): super().__init__() self._cwd = os.getcwd() self._config = config self._cookie_assister = 'login_assister.exe' self._user = "" self._pwd = "" self._cookie = {} self._del_user = "" self.initUI() self.setStyleSheet(dialog_qss_style) self.setMinimumWidth(560) self.name_ed.setFocus() # 信号 self.name_ed.textChanged.connect(self.set_user) self.pwd_ed.textChanged.connect(self.set_pwd) self.cookie_ed.textChanged.connect(self.set_cookie) def update_selection(self, user): """显示已经保存的登录用户信息""" user_info = self._config.get_user_info(user) if user_info: self._user = user_info[0] self._pwd = user_info[1] self._cookie = user_info[2] # 更新控件显示内容 self.name_ed.setText(self._user) self.pwd_ed.setText(self._pwd) try: text = ";".join([f'{k}={v}' for k, v in self._cookie.items()]) except: text = '' self.cookie_ed.setPlainText(text) def initUI(self): self.setWindowTitle("登录蓝奏云") self.setWindowIcon(QIcon(SRC_DIR + "login.ico")) logo = QLabel() logo.setPixmap(QPixmap(SRC_DIR + "logo3.gif")) logo.setStyleSheet("background-color:rgb(0,153,255);") logo.setAlignment(Qt.AlignmentFlag.AlignCenter) self.tabs = QTabWidget() self.auto_tab = QWidget() self.hand_tab = QWidget() # Add tabs self.tabs.addTab(self.auto_tab,"自动获取Cookie") self.tabs.addTab(self.hand_tab,"手动输入Cookie") self.auto_get_cookie_ok = AutoResizingTextEdit("🔶点击👇自动获取浏览器登录信息👇") self.auto_get_cookie_ok.setReadOnly(True) self.auto_get_cookie_btn = QPushButton("自动读取浏览器登录信息") auto_cookie_notice = '支持浏览器:Chrome, Chromium, Opera, Edge, Firefox' self.auto_get_cookie_btn.setToolTip(auto_cookie_notice) self.auto_get_cookie_btn.clicked.connect(self.call_auto_get_cookie) self.auto_get_cookie_btn.setStyleSheet("QPushButton {min-width: 210px;max-width: 210px;}") self.name_lb = QLabel("&U 用户") self.name_lb.setAlignment(Qt.AlignmentFlag.AlignCenter) self.name_ed = QLineEdit() self.name_lb.setBuddy(self.name_ed) self.pwd_lb = QLabel("&P 密码") self.pwd_lb.setAlignment(Qt.AlignmentFlag.AlignCenter) self.pwd_ed = QLineEdit() self.pwd_ed.setEchoMode(QLineEdit.EchoMode.Password) self.pwd_lb.setBuddy(self.pwd_ed) self.cookie_lb = QLabel("&Cookie") self.cookie_ed = QTextEdit() notice = "由于滑动验证的存在,需要输入cookie,cookie请使用浏览器获取\n" + \ "cookie会保存在本地,下次使用。其格式如下:\n ylogin=value1; phpdisk_info=value2" self.cookie_ed.setPlaceholderText(notice) self.cookie_lb.setBuddy(self.cookie_ed) self.show_input_cookie_btn = QPushButton("显示Cookie输入框") self.show_input_cookie_btn.setToolTip(notice) self.show_input_cookie_btn.setStyleSheet("QPushButton {min-width: 110px;max-width: 110px;}") self.show_input_cookie_btn.clicked.connect(self.change_show_input_cookie) self.ok_btn = QPushButton("登录") self.ok_btn.clicked.connect(self.change_ok_btn) self.cancel_btn = QPushButton("取消") self.cancel_btn.clicked.connect(self.change_cancel_btn) lb_line_1 = QLabel() lb_line_1.setText('<html><hr />切换用户</html>') lb_line_2 = QLabel() lb_line_2.setText('<html><hr /></html>') self.form = QFormLayout() self.form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) self.form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) # 覆盖MacOS的默认样式 self.form.addRow(self.name_lb, self.name_ed) self.form.addRow(self.pwd_lb, self.pwd_ed) if is_windows: def set_assister_path(): """设置辅助登录程序路径""" assister_path = QFileDialog.getOpenFileName(self, "选择辅助登录程序路径", self._cwd, "EXE Files (*.exe)") if not assister_path[0]: return None assister_path = os.path.normpath(assister_path[0]) # windows backslash if assister_path == self._cookie_assister: return None self.assister_ed.setText(assister_path) self._cookie_assister = assister_path self.assister_lb = QLabel("登录辅助程序") self.assister_lb.setAlignment(Qt.AlignmentFlag.AlignCenter) self.assister_ed = MyLineEdit(self) self.assister_ed.setText(self._cookie_assister) self.assister_ed.clicked.connect(set_assister_path) self.assister_lb.setBuddy(self.assister_ed) self.form.addRow(self.assister_lb, self.assister_ed) hbox = QHBoxLayout() hbox.addWidget(self.show_input_cookie_btn) hbox.addStretch(1) hbox.addWidget(self.ok_btn) hbox.addWidget(self.cancel_btn) user_box = QHBoxLayout() self.user_num = 0 self.user_btns = {} for user in self._config.users_name: user = str(user) # TODO: 可能需要删掉 self.user_btns[user] = QDoublePushButton(user) self.user_btns[user].setStyleSheet("QPushButton {border:none;}") if user == self._config.name: self.user_btns[user].setStyleSheet("QPushButton {background-color:rgb(0,153,2);}") self.tabs.setCurrentIndex(1) self.user_btns[user].setToolTip(f"点击选中,双击切换至用户:{user}") self.user_btns[user].doubleClicked.connect(self.choose_user) self.user_btns[user].clicked.connect(self.delete_chose_user) user_box.addWidget(self.user_btns[user]) self.user_num += 1 user_box.addStretch(1) self.layout = QVBoxLayout(self) self.layout.addWidget(logo) vbox = QVBoxLayout() if self._config.name: vbox.addWidget(lb_line_1) user_box.setAlignment(Qt.AlignmentFlag.AlignCenter) vbox.addLayout(user_box) vbox.addWidget(lb_line_2) if self.user_num > 1: self.del_user_btn = QPushButton("删除账户") self.del_user_btn.setIcon(QIcon(SRC_DIR + "delete.ico")) self.del_user_btn.setStyleSheet("QPushButton {min-width: 180px;max-width: 180px;}") self.del_user_btn.clicked.connect(self.call_del_chose_user) vbox.addWidget(self.del_user_btn) else: self.del_user_btn = None vbox.addStretch(1) vbox.addLayout(self.form) vbox.addStretch(1) vbox.addLayout(hbox) vbox.setAlignment(Qt.AlignmentFlag.AlignCenter) self.hand_tab.setLayout(vbox) auto_cookie_vbox = QVBoxLayout() auto_cookie_vbox.addWidget(self.auto_get_cookie_ok) auto_cookie_vbox.addWidget(self.auto_get_cookie_btn) auto_cookie_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter) self.auto_tab.setLayout(auto_cookie_vbox) self.layout.addWidget(self.tabs) self.setLayout(self.layout) self.update_selection(self._config.name) def call_del_chose_user(self): if self._del_user: if self._del_user != self._config.name: self.user_num -= 1 self._config.del_user(self._del_user) self.user_btns[self._del_user].close() self._del_user = "" if self.user_num <= 1: self.del_user_btn.close() self.del_user_btn = None return else: title = '不能删除' msg = '不能删除当前登录账户,请先切换用户!' else: title = '请选择账户' msg = '请单击选择需要删除的账户\n\n注意不能删除当前账户(绿色)' message_box = QMessageBox(self) message_box.setIcon(QMessageBox.Icon.Critical) message_box.setStyleSheet(btn_style) message_box.setWindowTitle(title) message_box.setText(msg) message_box.setStandardButtons(QMessageBox.StandardButton.Close) buttonC = message_box.button(QMessageBox.StandardButton.Close) buttonC.setText('关闭') message_box.exec() def delete_chose_user(self): """更改单击选中需要删除的用户""" user = str(self.sender().text()) self._del_user = user if self.del_user_btn: self.del_user_btn.setText(f"删除 <{user}>") def choose_user(self): """切换用户""" user = self.sender().text() if user != self._config.name: self.ok_btn.setText("切换用户") else: self.ok_btn.setText("登录") self.update_selection(user) def change_show_input_cookie(self): row_c = 4 if is_windows else 3 if self.form.rowCount() < row_c: self.org_height = self.height() self.form.addRow(self.cookie_lb, self.cookie_ed) self.show_input_cookie_btn.setText("隐藏Cookie输入框") self.change_height = None self.adjustSize() else: if not self.change_height: self.change_height = self.height() if self.cookie_ed.isVisible(): self.cookie_lb.setVisible(False) self.cookie_ed.setVisible(False) self.show_input_cookie_btn.setText("显示Cookie输入框") start_height, end_height = self.change_height, self.org_height else: self.cookie_lb.setVisible(True) self.cookie_ed.setVisible(True) self.show_input_cookie_btn.setText("隐藏Cookie输入框") start_height, end_height = self.org_height, self.change_height gm = self.geometry() x, y = gm.x(), gm.y() wd = self.width() self.animation = QPropertyAnimation(self, b'geometry') self.animation.setDuration(400) self.animation.setStartValue(QRect(x, y, wd, start_height)) self.animation.setEndValue(QRect(x, y, wd, end_height)) self.animation.start() def set_user(self, user): self._user = user if not user: return None if user not in self._config.users_name: self.ok_btn.setText("添加用户") self.cookie_ed.setPlainText("") elif user != self._config.name: self.update_selection(user) self.ok_btn.setText("切换用户") else: self.update_selection(user) self.ok_btn.setText("登录") def set_pwd(self, pwd): if self._user in self._config.users_name: user_info = self._config.get_user_info(self._user) if pwd and pwd != user_info[1]: # 改变密码,cookie作废 self.cookie_ed.setPlainText("") self._cookie = None if not pwd: # 输入空密码,表示删除对pwd的存储,并使用以前的cookie self._cookie = user_info[2] try: text = ";".join([f'{k}={v}' for k, v in self._cookie.items()]) except: text = '' self.cookie_ed.setPlainText(text) self._pwd = pwd def set_cookie(self): cookies = self.cookie_ed.toPlainText() if cookies: try: self._cookie = {kv.split("=")[0].strip(" "): kv.split("=")[1].strip(" ") for kv in cookies.split(";") if kv.strip(" ") } except: self._cookie = None def change_cancel_btn(self): self.update_selection(self._config.name) self.close() def change_ok_btn(self): if self._user and self._pwd: if self._user not in self._config.users_name: self._cookie = None if self._cookie: up_info = {"name": self._user, "pwd": self._pwd, "cookie": self._cookie, "work_id": -1} if self.ok_btn.text() == "切换用户": self._config.change_user(self._user) else: self._config.set_infos(up_info) self.clicked_ok.emit() self.close() elif USE_WEB_ENG: self.web = LoginWindow(self._user, self._pwd) self.web.cookie.connect(self.get_cookie_by_web) self.web.setWindowModality(Qt.WindowModality.ApplicationModal) self.web.exec() elif os.path.isfile(self._cookie_assister): try: result = os.popen(f'{self._cookie_assister} {self._user} {self._pwd}') cookie = result.read() try: self._cookie = {kv.split("=")[0].strip(" "): kv.split("=")[1].strip(" ") for kv in cookie.split(";")} except: self._cookie = None if not self._cookie: return None up_info = {"name": self._user, "pwd": self._pwd, "cookie": self._cookie, "work_id": -1} self._config.set_infos(up_info) self.clicked_ok.emit() self.close() except: pass else: title = '请使用 Cookie 登录或是选择 登录辅助程序' msg = '没有输入 Cookie,或者没有找到登录辅助程序!\n\n' + \ '推荐使用浏览器获取 cookie 填入 cookie 输入框\n\n' + \ '如果不嫌文件体积大,请下载登录辅助程序:\n' + \ 'https://github.com/rachpt/lanzou-gui/releases' message_box = QMessageBox(self) message_box.setIcon(QMessageBox.Icon.Critical) message_box.setStyleSheet(btn_style) message_box.setWindowTitle(title) message_box.setText(msg) message_box.setStandardButtons(QMessageBox.StandardButton.Close) buttonC = message_box.button(QMessageBox.StandardButton.Close) buttonC.setText('关闭') message_box.exec() def get_cookie_by_web(self, cookie): """使用辅助登录程序槽函数""" self._cookie = cookie self._close_dialog() def call_auto_get_cookie(self): """自动读取浏览器cookie槽函数""" try: self._cookie = get_cookie_from_browser() except Exception as e: logger.error(f"Browser_cookie3 Error: {e}") self.auto_get_cookie_ok.setPlainText(f"❌获取失败,错误信息\n{e}") else: if self._cookie: self._user = self._pwd = '' self.auto_get_cookie_ok.setPlainText("✅获取成功即将登录……") QTimer.singleShot(2000, self._close_dialog) else: self.auto_get_cookie_ok.setPlainText("❌获取失败\n请提前使用支持的浏览器登录蓝奏云,读取前完全退出浏览器!\n支持的浏览器与顺序:\nchrome, chromium, opera, edge, firefox") def _close_dialog(self): """关闭对话框""" up_info = {"name": self._user, "pwd": self._pwd, "cookie": self._cookie} self._config.set_infos(up_info) self.clicked_ok.emit() self.close()