I'm building an object detection app and every time something gets detected, I append text to a widget. I want the most recent text to appear prominently, with older entries gradually fading out. How can I achieve this fading text effect in PyQt6?
This is a great use case for Qt's Model/View architecture. Instead of trying to manually adjust the formatting of text inside a QTextEdit or QTextBrowser every time you add a new line, you can use a QListView backed by a model. The model holds your data, and a custom delegate controls how each item is drawn — including its color and opacity. When a new item is added, the view updates automatically, with the most recent entry appearing fully opaque and older entries fading out.
Let's walk through how to set this up.
Why QListView instead of QTextBrowser?
With a QTextBrowser or QTextEdit, you can change text color using .setTextColor(), but achieving a gradual fade across all previous lines means re-formatting the entire document every time you add a new entry. That gets awkward quickly.
A QListView with a QStandardItemModel is a much better fit. Each detected item becomes a row in the model, and you can apply formatting per-row based on its position. When you add a new entry, all the existing rows shift in visual weight automatically.
Setting up the model
Start with a basic QStandardItemModel connected to a QListView. Each time an object is detected, you append a new QStandardItem to the model.
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtWidgets import QListView
model = QStandardItemModel()
list_view = QListView()
list_view.setModel(model)
# Adding an item
item = QStandardItem("Detected: cat")
model.appendRow(item)
That gives you a working list, but all items look the same. To create the fade effect, you need to control how items are drawn depending on how far they are from the bottom of the list.
Applying the fade with a custom delegate
Qt's delegate system lets you customize how each item in a view is painted. By subclassing QStyledItemDelegate, you can adjust the text color's alpha (transparency) based on each item's position relative to the most recent entry.
The last item in the list (the newest) gets full opacity. Each item above it gets progressively more transparent, fading out over a set number of visible rows.
from PyQt6.QtWidgets import QStyledItemDelegate
from PyQt6.QtGui import QColor
class FadingDelegate(QStyledItemDelegate):
def __init__(self, fade_steps=10, parent=None):
super().__init__(parent)
self.fade_steps = fade_steps
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
model = index.model()
total_rows = model.rowCount()
row = index.row()
# Distance from the last (newest) row
distance = total_rows - 1 - row
# Calculate alpha: 255 for the newest, fading toward 30 for older items
min_alpha = 30
if distance >= self.fade_steps:
alpha = min_alpha
else:
alpha = max(
min_alpha,
255 - int((distance / self.fade_steps) * (255 - min_alpha)),
)
color = QColor(option.palette.color(option.palette.ColorRole.Text))
color.setAlpha(alpha)
option.palette.setColor(option.palette.ColorRole.Text, color)
The fade_steps parameter controls how many rows participate in the fade. Items older than fade_steps rows from the bottom settle at a low alpha value (30 in this case), making them very faint without disappearing entirely.
Connecting it all together
Set the delegate on your QListView, and each time you add an item, the entire list re-renders with the correct fading applied.
delegate = FadingDelegate(fade_steps=10)
list_view.setItemDelegate(delegate)
Whenever the model changes (a new row is appended), the view calls the delegate's paint logic for all visible items. The newest item appears at full opacity, and everything above it gradually fades.
Scrolling to the latest item
Since new detections go at the bottom, you'll want the view to scroll down automatically:
list_view.scrollToBottom()
Call this after each model.appendRow().
Complete working example
Here's a full application you can copy and run. It simulates object detections arriving every second using a QTimer, and displays them in a fading list.
import sys
import random
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QColor, QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import (
QApplication,
QListView,
QMainWindow,
QStyledItemDelegate,
QVBoxLayout,
QWidget,
)
class FadingDelegate(QStyledItemDelegate):
"""Custom delegate that fades older items toward transparency."""
def __init__(self, fade_steps=10, parent=None):
super().__init__(parent)
self.fade_steps = fade_steps
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
model = index.model()
total_rows = model.rowCount()
row = index.row()
# How far this row is from the newest (bottom) row
distance = total_rows - 1 - row
# Full opacity for the newest, fading older items down to a minimum
min_alpha = 30
if distance >= self.fade_steps:
alpha = min_alpha
else:
alpha = max(
min_alpha,
255 - int((distance / self.fade_steps) * (255 - min_alpha)),
)
color = QColor(option.palette.color(option.palette.ColorRole.Text))
color.setAlpha(alpha)
option.palette.setColor(option.palette.ColorRole.Text, color)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Fading Detection Log")
self.resize(400, 500)
# Set up the model and view
self.model = QStandardItemModel()
self.list_view = QListView()
self.list_view.setModel(self.model)
# Apply the fading delegate
self.delegate = FadingDelegate(fade_steps=10)
self.list_view.setItemDelegate(self.delegate)
# Disable editing so it behaves like a log
self.list_view.setEditTriggers(
QListView.EditTrigger.NoEditTriggers
)
# Layout
layout = QVBoxLayout()
layout.addWidget(self.list_view)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
# Simulate detections arriving every second
self.objects = [
"cat", "dog", "person", "car", "bicycle",
"bird", "chair", "bottle", "phone", "laptop",
]
self.timer = QTimer()
self.timer.timeout.connect(self.add_detection)
self.timer.start(1000)
def add_detection(self):
detected = random.choice(self.objects)
item = QStandardItem(f"Detected: {detected}")
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.model.appendRow(item)
self.list_view.scrollToBottom()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this and you'll see a live log where each new detection appears at full intensity, and older entries smoothly fade out above it. The fade_steps=10 means the last 10 entries participate in the gradient — adjust this number to control how quickly things fade.
Customizing the effect
There are several ways to adjust the look:
- Change
fade_steps: A smaller number (like 5) creates a sharper cutoff. A larger number (like 20) gives a more gradual fade. - Change
min_alpha: Setting this to 0 would make old items fully invisible. Setting it to 100 keeps them somewhat readable. - Add bold to the newest item: In
initStyleOption, you could check ifdistance == 0and set the font to bold for the latest entry. - Use color changes: Instead of (or in addition to) transparency, you could shift the hue — for example, newest items in red, fading to gray.
Here's a quick example of making the newest entry bold:
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
model = index.model()
total_rows = model.rowCount()
row = index.row()
distance = total_rows - 1 - row
# Bold the newest item
if distance == 0:
font = option.font
font.setBold(True)
option.font = font
# ... rest of alpha calculation
Using a QListView with a custom delegate gives you a clean, maintainable way to present detection results with visual emphasis on what's new. The model/view pattern keeps your data and presentation separate, so you can change the visual style without touching your detection logic at all. If you're looking to build your own reusable widgets with custom painting, take a look at creating your own custom widgets in PyQt6. For more on how signals like QTimer.timeout connect to slots, see the signals and slots tutorial.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.