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:
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:
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.