4
\$\begingroup\$

As a part of bigger project, I want to add tutorial mode after first user login. That mode is simply a QFrame showing description of highlighted element in parent window + Next/Cancel buttons, moving to the next element or stopping tutorial mode completely.

In particular, it works as follows: enter image description here

Main Window is only for illustration purposes, need review only for Tutorial Manager and Hint.

Code is written in Python 3.11.7, lib version - Pyside 6.7.2.

import sys from PySide6.QtCore import Qt, QTimer, QRect, QPoint from PySide6.QtGui import QColor, QPainter from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QLineEdit, QVBoxLayout, QWidget, QHBoxLayout, QFrame) def load_stylesheet(filename): with open(filename, 'r') as f: return f.read() class TutorialHint(QFrame): def __init__(self, text, parent=None): super().__init__(parent) self.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint) self.setAttribute(Qt.WA_TranslucentBackground) self.setStyleSheet(load_stylesheet('tutorial_hint.qss')) layout = QVBoxLayout(self) hint_text = QLabel(text) hint_text.setWordWrap(True) layout.addWidget(hint_text) button_layout = QHBoxLayout() self.next_button = QPushButton("Next") self.stop_button = QPushButton("Stop") button_layout.addWidget(self.next_button) button_layout.addWidget(self.stop_button) layout.addLayout(button_layout) self.target_element = None def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.setBrush(QColor(224, 224, 224)) painter.setPen(Qt.NoPen) painter.drawRoundedRect(self.rect(), 10, 10) def set_target_element(self, element): self.target_element = element self.update_position() def update_position(self): if self.target_element and self.parent(): element_rect = self.target_element.geometry() element_global_rect = QRect(self.parent().mapToGlobal(element_rect.topLeft()), element_rect.size()) hint_pos = element_global_rect.topRight() + QPoint(20, 0) screen_rect = self.screen().geometry() if hint_pos.x() + self.width() > screen_rect.right(): hint_pos.setX(screen_rect.right() - self.width()) if hint_pos.y() + self.height() > screen_rect.bottom(): hint_pos.setY(screen_rect.bottom() - self.height()) self.move(hint_pos) def moveEvent(self, event): super().moveEvent(event) self.update_position() class TutorialManager: def __init__(self, parent, tutorial_steps): self.parent = parent self.tutorial_steps = tutorial_steps self.current_step = 0 self.current_hint = None self.highlight_style = load_stylesheet('highlight.qss') self.original_stylesheet = self.parent.styleSheet() def start_tutorial(self): QTimer.singleShot(500, self.show_tutorial_step) def show_tutorial_step(self): if self.current_step < len(self.tutorial_steps): element, text = self.tutorial_steps[self.current_step] # Remove highlight from previous element if self.current_step > 0: prev_element = self.tutorial_steps[self.current_step - 1][0] prev_element.setGraphicsEffect(None) prev_element.setStyleSheet("") # Apply highlight effect and style element.setStyleSheet(self.highlight_style) # Show hint dialog if self.current_hint: self.current_hint.close() self.current_hint = TutorialHint(text, self.parent) self.current_hint.next_button.clicked.connect(self.next_tutorial_step) self.current_hint.stop_button.clicked.connect(self.end_tutorial) self.current_hint.set_target_element(element) self.current_hint.show() def next_tutorial_step(self): self.current_step += 1 if self.current_step < len(self.tutorial_steps): self.show_tutorial_step() else: self.end_tutorial() def end_tutorial(self): for element, _ in self.tutorial_steps: element.setStyleSheet("") if self.current_hint: self.current_hint.close() self.current_hint = None self.current_step = 0 self.parent.highlighted_element = None self.parent.setStyleSheet(self.original_stylesheet) self.parent.update() def update_hint_position(self): if self.current_hint: self.current_hint.update_position() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PySide6 Example with Tutorial") self.setGeometry(100, 100, 500, 300) self.setStyleSheet(load_stylesheet('start_style.qss')) central_widget = QWidget() main_layout = QHBoxLayout(central_widget) # Left VBox left_vbox = QVBoxLayout() self.label1 = QLabel("Left Label") self.text_box1 = QLineEdit() self.text_box1.setPlaceholderText("Enter text for left label...") self.button1 = QPushButton("Update Left Label") self.button1.clicked.connect(self.update_left_label) left_vbox.addWidget(self.label1) left_vbox.addWidget(self.text_box1) left_vbox.addWidget(self.button1) left_vbox.addStretch() # Right VBox right_vbox = QVBoxLayout() self.label2 = QLabel("Right Label") self.text_box2 = QLineEdit() self.text_box2.setPlaceholderText("Enter text for right label...") self.button2 = QPushButton("Update Right Label") self.button2.clicked.connect(self.update_right_label) right_vbox.addWidget(self.label2) right_vbox.addWidget(self.text_box2) right_vbox.addWidget(self.button2) right_vbox.addStretch() main_layout.addLayout(left_vbox) main_layout.addLayout(right_vbox) self.setCentralWidget(central_widget) tutorial_steps = [ (self.label1, "This is the left label that displays text."), (self.text_box1, "Enter text here to update the left label."), (self.button1, "Click this button to update the left label."), (self.label2, "This is the right label that displays text."), (self.text_box2, "Enter text here to update the right label."), (self.button2, "Click this button to update the right label.") ] self.tutorial_manager = TutorialManager(self, tutorial_steps) self.tutorial_manager.start_tutorial() def update_left_label(self): self.label1.setText(f"Left: {self.text_box1.text()}") def update_right_label(self): self.label2.setText(f"Right: {self.text_box2.text()}") def moveEvent(self, event): super().moveEvent(event) self.tutorial_manager.update_hint_position() def resizeEvent(self, event): super().resizeEvent(event) self.tutorial_manager.update_hint_position() if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) 

.qss styles:

/* start_style.qss */ QMainWindow { background-color: #f0f0f0; } QLabel { font-size: 14px; color: #333333; } QLineEdit { padding: 5px; border: 1px solid #cccccc; border-radius: 4px; background-color: white; font-size: 13px; } QPushButton { background-color: #4CAF50; color: white; padding: 6px 12px; border: none; border-radius: 4px; font-size: 13px; } QPushButton:hover { background-color: #45a049; } QPushButton:pressed { background-color: #3d8b40; } TutorialHint { background-color: #E0E0E0; /* Light gray */ border: 1px solid #BDBDBD; /* Medium gray border */ border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Subtle shadow */ box-sizing: border-box; /* Include border in element's dimensions */ } TutorialHint QLabel { color: #333333; /* Dark gray text */ font-size: 14px; padding: 10px; } TutorialHint QPushButton { background-color: #9E9E9E; /* Medium gray */ color: #FFFFFF; /* White text */ border: none; padding: 8px 12px; border-radius: 4px; font-size: 13px; } TutorialHint QPushButton:hover { background-color: #757575; /* Darker gray on hover */ } TutorialHint QPushButton:pressed { background-color: #616161; /* Even darker when pressed */ } /* highlight.qss */ * { background-color: #FFFACD; /* Light yellow */ border: 1px solid #FF4500; /* OrangeRed */ color: #8B0000; /* Dark red */ } 

Example results:

enter image description here

enter image description here

How can I improve my code for TutorialManager/Hint?

The one thing that actually bothers me, is that Hint frame may be shown out of bounds of parent window (see Example #2). On the other hand, I will need to handle all such cases + do something in case if there's not enough space in parent window to contain Hint frame in full.

\$\endgroup\$

    1 Answer 1

    2
    \$\begingroup\$

    To handle the out of bounds scenarios that you're worried about, you can adjust the position calculation logic to ensure the hint will stay within the visible area of the parent window. Here’s a quick version of your update_position method in the TutorialHint class:

    def update_position(self): if self.target_element and self.parent(): element_rect = self.target_element.geometry() element_global_rect = QRect(self.parent().mapToGlobal(element_rect.topLeft()), element_rect.size()) hint_pos = element_global_rect.topRight() + QPoint(20, 0) # Use parent window's geometry screen_rect = self.parent().geometry() # Adjust position to keep the hint inside the parent window if hint_pos.x() + self.width() > screen_rect.right(): # Add margin to prevent overlapping edge hint_pos.setX(screen_rect.right() - self.width() - 20) if hint_pos.y() + self.height() > screen_rect.bottom(): # Adding margin to prevent overlapping edge hint_pos.setY(screen_rect.bottom() - self.height() - 20) if hint_pos.x() < screen_rect.left(): # Adding margin to prevent overlapping edge hint_pos.setX(screen_rect.left() + 20) if hint_pos.y() < screen_rect.top(): # Adding margin to prevent overlapping edge hint_pos.setY(screen_rect.top() + 20) self.move(hint_pos) 

    Now, if the parent window does not have enough space to display the hint in the desired position, you can probably add a fallback mechanism to position the hint either on the other side of the element or in a different position where it fits better. Something like this:

    def update_position(self): if self.target_element and self.parent(): element_rect = self.target_element.geometry() element_global_rect = QRect(self.parent().mapToGlobal(element_rect.topLeft()), element_rect.size()) hint_pos = element_global_rect.topRight() + QPoint(20, 0) screen_rect = self.parent().geometry() # Use parent window's geometry if hint_pos.x() + self.width() > screen_rect.right(): hint_pos = element_global_rect.topLeft() - QPoint(self.width() + 20, 0) if hint_pos.y() + self.height() > screen_rect.bottom(): hint_pos = element_global_rect.bottomRight() - QPoint(self.width(), self.height() + 20) if hint_pos.x() < screen_rect.left(): hint_pos.setX(screen_rect.left() + 20) if hint_pos.y() < screen_rect.top(): hint_pos.setY(screen_rect.top() + 20) self.move(hint_pos) 

    There is, however, a chance that if the parent window is small, and there's absolutely no room to display the hint, you might need to implement scrolling or resizing of the hint window itself to fit within the available space. Keep this in mind.

    More, instead of using QSS for highlighting, consider using QGraphicsEffect, which provides more flexibility and can be combined with animations to create a smoother user experience:

    from PySide6.QtWidgets import QGraphicsColorizeEffect def apply_highlight_effect(self, element): effect = QGraphicsColorizeEffect() effect.setColor(QColor("#FF4500")) # Highlight color element.setGraphicsEffect(effect) 
    \$\endgroup\$

      Start asking to get answers

      Find the answer to your question by asking.

      Ask question

      Explore related questions

      See similar questions with these tags.