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.

